Skip to content

[BUG](system) Model-constraint decorators return the class they decorate - #761

Open
Seth Fitzsimmons (sethfitz) wants to merge 1 commit into
mainfrom
fix-model-constraint-decorator-types
Open

Seth Fitzsimmons (sethfitz) wants to merge 1 commit into
mainfrom
fix-model-constraint-decorator-types

Conversation

@sethfitz

@sethfitz Seth Fitzsimmons (sethfitz) commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Description

The model-constraint decorators were annotated to return type[BaseModel], or Callable[[type[BaseModel]], type[BaseModel]] for the factories. pyright and ty honor a class decorator's return type, so every decorated model, Division and Names among them, became plain BaseModel and lost its fields in hover, completion and attribute access. mypy ignores class-decorator return types, so make check passed.

The fix reuses ModelT, the TypeVar create_model already declares, to carry the class through. ModelConstraint.decorate and no_extra_fields take and return type[ModelT]. require_if, forbid_if, require_any_of, require_any_true, radio_group and min_fields_set return Callable[[type[ModelT]], type[ModelT]]. create_model returns type[ModelT] for a type[ModelT] base, so decorate returns its result without a cast. Runtime behavior is unchanged.

@scoped in overture-schema-common is out of scope; #760 says why.

Reference

  1. Closes [BUG](system) Model-constraint decorators erase the decorated class under pyright and ty #760.

Testing

test_decorator_types.py calls each decorator as a function under assert_type(..., type[Model]). mypy checks an ordinary call's return type even though it skips decorator syntax, so this puts all eight sites in front of the mypy gate. Against the old annotations it reports eight assert-type errors. It is the only added test.

pyright and ty are not in the tree. Revealed type of a field on a model decorated at each of the eight sites:

before after
pyright 1.1.414 Unknown, "Cannot access attribute" the field's type
ty 0.0.81 Unknown, invalid-type-form the field's type
mypy 2.3.1 the field's type; decorate() called as a function gave BaseModel the field's type

On the published models, reveal_type(d.names) for d: Division goes from Unknown to Names under pyright.

Checklist

Checklist of tasks commonly-associated with schema pull requests. Please review the relevant checklists and ensure you do all the tasks that are required for the change you made.

  1. Add relevant examples.
  2. Add relevant counterexamples.
  3. Update any counterexamples that became obsolete. For example, if a counterexample uses property A but is not intended to test property A's validity, and you made a schema change that invalidates property A in that counterexample, fix the counterexample to align it with your schema change.
  4. Update in-schema documentation using plain English written in complete sentences, if an update is required.
  5. Update Docusaurus documentation, if an update is required.
  6. Review change with Overture technical writer to ensure any advanced documentation needs will be taken care of, unless the change is trivial and would not affect the documentation.

Documentation website

Update the hyperlink below to put the pull request number in.

[Docs preview for this PR.](https://dfhx9f55j8eg5.cloudfront.net/pr/<PUT THE PR # HERE>)

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🗺️ Schema reference docs preview is live!

🌍 Preview https://staging.overturemaps.org/schema/pr/761/schema/index.html
🕐 Updated Sep 23, 2026 22:50 UTC
📝 Commit 4c7fbac
🔧 env SCHEMA_PREVIEW true

Note

♻️ This preview updates automatically with each push to this PR.

@RoelBollens-TomTom

Copy link
Copy Markdown
Collaborator

I'd suggest reusing ModelT from create_model.py as type[ModelT] instead of a new M bound to type[BaseModel]. Same fix, matches Pydantic's way, and the cast in decorate becomes unnecessary because create_model already returns type[ModelT].

@sethfitz

Copy link
Copy Markdown
Collaborator Author

Roel Bollens (@RoelBollens-TomTom) thanks for the pointer. Updated!

Every model-constraint decorator was annotated to return
type[BaseModel], and every decorator factory to return
Callable[[type[BaseModel]], type[BaseModel]]. pyright and ty honor a
class decorator's return type, so each decorated model became plain
BaseModel: its fields dropped out of hover and completion, attribute
access reported an error, and ty rejected the model in annotations as
invalid-type-form. mypy ignores class-decorator return types, so the
gate passed.

ModelT, the TypeVar create_model already declares, now carries the
decorated class through. ModelConstraint.decorate and no_extra_fields
take and return type[ModelT], and require_if, forbid_if, require_any_of,
require_any_true, radio_group and min_fields_set return
Callable[[type[ModelT]], type[ModelT]]. create_model returns
type[ModelT] for a type[ModelT] base, so decorate returns its result
without a cast.

mypy does check an ordinary call. test_decorator_types.py calls each
decorator as a function under assert_type(..., type[Model]), which puts
all eight return types in front of the mypy gate. Against the old
annotations it reports eight assert-type errors.

pyright and ty are not in the tree, so their results are recorded here.
Revealed type of a field on a model decorated at each of the eight
sites:

               before                                after
    pyright    Unknown, "Cannot access attribute"    the field's type
    ty         Unknown, invalid-type-form            the field's type
    mypy       the field's type; decorate() called   the field's type
               as a function gave BaseModel

pyright 1.1.414, ty 0.0.81, mypy 2.3.1, pydantic 2.13.5.

Closes #760

Signed-off-by: Seth Fitzsimmons <seth@mojodna.net>

This branch was successfully deployed

1 active deployment
staging — 4c7fbac5 Deployed Sep 23, 2026 by sethfitz via Deploy #575
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change type - minor 🤏 Minor schema change. See https://lf-overturemaps.atlassian.net/wiki/x/GgDa

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG](system) Model-constraint decorators erase the decorated class under pyright and ty

2 participants