Implement ParamModel: A Parameterized subclass based on type annotations - #1133
philippjfr wants to merge 9 commits into
Conversation
|
Some suggestions instead of
|
|
|
|
Alternatively |
|
And if we're already riffing on more succinct naming for the Parameter factory, then I'd also suggest |
|
Okay, my favorite proposal now is |
I guess it would literally be ParameterizedFromTypeDeclarations?
I don't personally love Model since it is used to mean so many things that it's lost all meaning for me, but I don't object since Pydantic uses that term. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1133 +/- ##
==========================================
- Coverage 88.61% 88.61% -0.01%
==========================================
Files 9 10 +1
Lines 6072 6227 +155
==========================================
+ Hits 5381 5518 +137
- Misses 691 709 +18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…4 gaps - Annotation-only fields (and Field()-without-default fields) are now enforced as required at __init__ time via a _param_model_required set accumulated across the MRO, instead of silently resolving to each Parameter subclass's placeholder default (e.g. '', 0, []). Literal-derived Selector fields remain exempt since Selector already derives a sensible default from objects[0]. - Bare (unsubscripted) list/dict/tuple/set annotations now map to List/Dict/Tuple/ClassSelector like their subscripted forms, instead of falling through to an unvalidated Parameter. - ParamField(parameter=...) overrides now preserve allow_None inferred from Optional[T]/T | None unless explicitly overridden. - _extract_namespace_annotations no longer swallows genuine annotation evaluation errors (e.g. broken forward references) on Python 3.14's deferred annotation path; only the annotationlib import itself is guarded. Adds regression tests for all of the above.
- Typing.ipynb: add an 'Annotation-first classes with ParamModel' section demonstrating type inference (including Literal, which the existing Selector workaround can't narrow), required-field errors, and ParamField overrides. Update Limitations/Future direction/Practical recommendations to reflect that ParamModel is now available rather than purely aspirational. - getting_started.md: add a short ParamModel/ParamField example showing the dataclass-like shorthand and required-field behavior, linking to the Typing guide for details. - user_guide/index.md: mention ParamModel in the Typing guide summary.
Based on discussion in #1066 I prototyped
ParamModel.Summary
Adds
ParamModel, a newParameterizedsubclass that synthesisesParameterinstances automatically from PEP 526 type annotations. This gives users a dataclass-like authoring experience without requiring manualParameterinstantiation for common cases.Motivation
After investigating
@dataclass_transform, it became clear that the decorator cannot bind RHSParameterdescriptor types to LHS attribute types in a way that satisfies type checkers, you'd still need to repeat yourself (string_param: str = String()) and even then checkers would seeString[str], notstr. Additionally,dataclass_transformrequires every field specifier to be enumerated upfront, which is impossible atParameterizedMetaclassdefinition time because not allParametersubclasses exist yet.'ParamField',sidesteps these constraints entirely: the metaclass handles synthesis at class creation time, and a dedicatedParamField()factory covers the cases where users need to supply explicit parameter configuration.Changes
ParamModel(decorated with@dataclass_transform(field_specifiers=(ParamField,))) overrides__new__to walk the class__annotations__dict at class creation time. For each non-private, non-ClassVarannotation it calls_annotation_parameter_factoryto map the annotation to an appropriateParametersubclass and infer constructor kwargs, then calls_build_parameter_from_fieldto construct the finalParameterobject. String annotations areeval'd against the module globals to supportfrom __future__ import annotations._annotation_parameter_factoryhandles the following mappings:bool→Booleanint→Integerfloat→Numberstr→Stringlist[T]→List(item_type=T)tuple[...]→Tuple(length=N)(fixed-length) orTuple(variable)dict[...]→Dictset→ClassSelector(class_=set)Literal[...]→Selector(objects=[...])T | None/Optional[T]→ wraps the inner type withallow_None=TrueAnnotated[T, {...}]→ passes the mapping as extra kwargs to the parameter constructorAny/object→ bareParameterParamField()is a typed factory (three overloads) that returns a_FieldSpecsentinel. It acceptsdefault,default_factory, aparameteroverride (class, callable, or instance), and arbitrary kwargs forwarded to the parameter constructor. Type checker overloads ensure thatdefault: FTanddefault_factory: Callable[[], FT]both propagateFTto the attribute type, matching dataclass semantics._build_parameter_from_fieldmerges the inferred factory and kwargs with anything supplied viaField, giving explicitFieldvalues precedence. If aParameterinstance is passed as theparameterargument it is shallow-copied and mutated rather than re-instantiated.Usage