Python semantic layer: Injected marker and the @measure decorator - #247
Open
jat255 wants to merge 8 commits into
Open
Python semantic layer: Injected marker and the @measure decorator#247jat255 wants to merge 8 commits into
jat255 wants to merge 8 commits into
Conversation
jat255
force-pushed
the
jat255/wwmt-measure-basics
branch
from
September 2, 2026 03:14
064a319 to
52af05b
Compare
jat255
marked this pull request as ready for review
September 2, 2026 04:00
jat255
force-pushed
the
jat255/wwmt-measure-basics
branch
2 times, most recently
from
September 2, 2026 22:25
064a319 to
1f56886
Compare
…otated_attribute Replaced deprecated pydantic API with the supported path for building FieldInfo from annotations with defaults. This also merges all field constraints (e.g. Field(gt=0)) rather than silently dropping them. Added test to verify constraint merging.
FieldInfo.from_annotated_attribute overwrites a Field's default= with PydanticUndefined whenever the signature omits its own default, silently making the parameter required in the schema while Python's call convention still requires it. Raise TypeError at decoration time instead, naming the parameter and telling the author to move the default into the signature.
Route direct as_measure() dereferences through a shared _as_measure() helper so pyrefly sees Measure instead of Measure | None.
The fail-closed check in 75914f5 only inspected the FieldInfo that _described_field() returns, the one carrying the description. An annotation can carry more than one Field(...), and a default declared in a separate one slipped through, producing a required schema field whose default Python never applies. Scan all FieldInfo metadata on the annotation instead of just the described one.
…guments; name unresolvable annotations get_type_hints() resolves every annotation, so a measure whose Injected[T] names a type imported only under `if TYPE_CHECKING:` (the case the feature exists for) failed with a bare NameError naming nothing, fifteen frames deep in typing internals. Catch it and re-raise a TypeError naming the measure, the unresolved name, and the two ways out: import it unconditionally, or use Injected[Any]. Also: reject a positional-only parameter, which builds a schema but can never actually be called since callers pass arguments by keyword; reject async def, since nothing here can call a measure yet and shipping a coroutine silently is worse than rejecting it fail-closed; and reject a parameter marked both Injected and Field(description=...), which previously vanished from the schema silently, contradicting what the module docstring already claims the design prevents.
inspect.iscoroutinefunction() does not recognize an async def containing yield; that makes it an async generator function, a different kind, and it slipped through the async rejection to return an async generator instead of a result. Check inspect.isasyncgenfunction() too, with the same message.
jat255
force-pushed
the
jat255/wwmt-measure-basics
branch
from
September 3, 2026 00:31
1f56886 to
48a4d74
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First of four stacked PRs building the Python semantic layer (M3). This one adds the measure record and the rule that tells the two kinds of measure argument apart.
These are implemented in
pkg-py/src/commons/_measures.pyand corresponding tests.Two types of measures:
Annotated[T, Field(description=...)]Injected[T]and they never reach the model.A parameter that is neither raises
TypeErrorat decoration time. This came from decision D9, and it is stricter than the R package, where a forgotten@paramsilently hides an argument from the model.Some API notes
@measurereturns the function unchanged and attaches the record as an attribute, so measures and the helpers they call stay ordinary callables.The schema is not built through chatlas, because
Tool.from_func()rejects any model whose fields do not cover every function parameter, which won't work on injected parameters. Measures are never registered with the provider in any case, since the provider sees onlycall_measure.Argument validation uses
pydantic'smodel_validateon anextra="forbid"model. That covers unknown arguments, required arguments, enum vocabularies, and coercion in one call, so there is no need for a hand-written validator. Its error text differs from the R package's, which is accepted because the text goes back to the model as a tool error rather than to a user.One caveat worth noting: An
AnnotatedFieldcarryingdefault=ordefault_factory=is a decoration error, and the default belongs in the signature instead.For example, the following will not work:
validate_argsomits unset arguments so that Python applies the default at call time. Accepting aFielddefault would make the schema optional while the signature still required the argument, and the call would then fail mid-conversation.Not in this PR:
semantic_layer(), injection resolution, and the public exports. Those are the PRs above it in the stack.Four signature mistakes are rejected at decoration rather than left to fail later, because each one produces a measure that can never run. A parameter that is neither described nor injected, a positional-only parameter (the caller passes arguments by keyword), an
async defor async generator (it would hand a coroutine to the model), and a parameter marked both described and injected, which would otherwise vanish from the schema in silence.One error deserves its own mention. Annotating
Injected[Engine]and importingEngineunderif TYPE_CHECKING:is the way this feature is meant to be used, andget_type_hints()would fail it with a bareNameError: name 'Engine' is not definedunder a stack of typing internals. It now raises naming the measure, the unresolved name, and the two ways out.