Skip to content

Commit 1743e65

Browse files
committed
fix(signatures): raise clear error for field names that shadow SignatureMeta properties
Declaring a Signature field whose name collides with a SignatureMeta property (e.g. `instructions`, which is a class-level property on the metaclass for reading/writing the docstring) crashed signature class construction with an opaque internal error: class MySig(dspy.Signature): instructions: str = dspy.InputField() answer: str = dspy.OutputField() # TypeError: 'NoneType' object is not subscriptable Pydantic detects the name collision (it emits its own "shadows an attribute in parent" UserWarning) and, for that field only, produces a plain FieldInfo with json_schema_extra=None instead of preserving the InputField()/OutputField() instance from the class body. SignatureMeta already has validation for exactly this shape of problem - _validate_fields() raises a clear TypeError when a field's json_schema_extra doesn't carry the expected __dspy_field_type key - but it ran too late: SignatureMeta.__new__ computes default instructions first when no docstring is given, via _default_instructions(cls) -> cls.input_fields -> _get_fields_with_type(), which indexes json_schema_extra directly and crashes before _validate_fields() ever gets a chance to run. Move the existing _validate_fields() call to run immediately after Pydantic builds the class, before anything reads json_schema_extra. No new validation logic - just correct ordering of what was already there. Fixes #1710.
1 parent 822f393 commit 1743e65

2 files changed

Lines changed: 23 additions & 3 deletions

File tree

‎dspy/signatures/signature.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,14 @@ def __new__(mcs, signature_name, bases, namespace, **kwargs):
184184
# Let Pydantic do its thing
185185
cls = super().__new__(mcs, signature_name, bases, namespace, **kwargs)
186186

187+
# Ensure all fields are declared with InputField or OutputField. This must run before
188+
# any code below that reads `field.json_schema_extra` (e.g. via input_fields/output_fields),
189+
# since a field whose name collides with a SignatureMeta property (e.g. `instructions`)
190+
# gets a plain pydantic FieldInfo with json_schema_extra=None instead of the one from
191+
# InputField()/OutputField() - which would otherwise surface as an opaque internal
192+
# TypeError instead of this validation's clear message.
193+
cls._validate_fields()
194+
187195
# If we don't have instructions, it might be because we are a derived generic type.
188196
# In that case, we should inherit the instructions from the base class.
189197
if cls.__doc__ is None:
@@ -198,9 +206,6 @@ def __new__(mcs, signature_name, bases, namespace, **kwargs):
198206
if cls.__doc__ is None:
199207
cls.__doc__ = _default_instructions(cls)
200208

201-
# Ensure all fields are declared with InputField or OutputField
202-
cls._validate_fields()
203-
204209
# Ensure all fields have a prefix
205210
for name, field in cls.model_fields.items():
206211
if "prefix" not in field.json_schema_extra:

‎tests/signatures/test_signature.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ class TestSignature(Signature):
4141
input1: str = pydantic.Field()
4242

4343

44+
def test_field_name_shadowing_signaturemeta_property_raises_clear_error():
45+
"""
46+
Regression test for https://github.com/stanfordnlp/dspy/issues/1710
47+
48+
A field name that collides with a SignatureMeta property (e.g. `instructions`) must raise
49+
the existing `_validate_fields` TypeError, not crash with an opaque internal TypeError from
50+
`_get_fields_with_type` deep inside `input_fields`/`output_fields`.
51+
"""
52+
with pytest.raises(TypeError, match="must be declared with InputField or OutputField"):
53+
54+
class TestSignature(Signature):
55+
instructions: str = InputField()
56+
answer: str = OutputField()
57+
58+
4459
def test_all_fields_have_prefix():
4560
class TestSignature(Signature):
4661
input = InputField(prefix="Modified:")

0 commit comments

Comments
 (0)