Skip to content

Conversation

@vcschapp
Copy link
Collaborator

@vcschapp vcschapp commented Oct 28, 2025

tl;dr

This is part #4. Part #5 is hoped for in the next week or two if all goes according to plan.

Done in this part: re-organized the addresses and places packages to their (hopefully) final form, finish dynamic scoping implementation (@scoped decorator) and apply it to names and transportation.

Merge guidance: Rebase

Unlike parts 1, 2, and 3, this iteration has a well-written commit history. It should ideally be rebase-merged onto the pydantic branch rather than squashed.

Journey

Destination

  1. Packages.
  2. Documentation.
    • ✅ All doctests will pass and automatically be enforced to pass by make check.
    • ▶️ All documentation will pass pydocstyle with the NumPy convention. (As of now, the tool reports too many complaints to add it into make check.)
    • ⬜️ pydocstyle will be enforced by make check.
    • ⬜️ docformatter will be run by make check.
    • ▶️ Package-level README content will migrate into the package's __init__.py docstring.
    • ⬜️ There will be a doc target in the Makefile that does uv run pdoc to a useful place.
  3. Modules.
    • ▶️ Re-exports will be standardized and everything of value will be re-exported into the main module.
    • ▶️ In theme packages, each feature type will be within its own non-exported module within the package root.
    • ⬜️ In theme packages, if a field contains a theme-specific "deep model" then it will be given its own module, e.g. perspectives.

Frozen-ness:

- The frozen/immutable characteristics of the parent model are
  perpetuated into the "when" clause.
- I initially thought this was needed to make the parent model behave
  as expected when contained in a list with a `UniqueItemsConstraint`,
  but this is not so because `UniqueItemsConstraint` is well-written
  and can handle non-hashable items.
- But it is still needed for correctness: if a model is marked as
  frozen then it will only be truly so if the sub-models that compose
  it are also frozen.
@vcschapp vcschapp added the change type - cosmetic 🌹 Cosmetic change label Oct 28, 2025
@vcschapp vcschapp requested a review from mojodna October 28, 2025 03:41
@vcschapp vcschapp changed the title Pydantic package organization episode 4: "Subtitle required" Pydantic package organization episode 4: "Scope is in (scope)" Oct 28, 2025
Copy link
Collaborator

@mojodna mojodna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking, but I have lots of questions about scopes (surprise, surprise).


[project.entry-points."overture.models"]
"places.place" = "overture.schema.places.place.models:Place"
"places.place" = "overture.schema.places:Place"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I made a similar change in the CLI PR. In that case, it may refer to an indirect alias, but either way, the intent is to register the shortest useful import for the class (because that path will be displayed when listing available types).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for updating this!

...
>>> hotspots=[
... WiFiHotspot(ssid="always_on"),
... WiFiHotspot(ssid="daytime_use_only", when=WiFiHotspot.When(during="06:00-22:00"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is confusing. Where does WiFiHotspot.When() come from? (Update: found it below)

Reading the description above, I would expect this to be:

WiFiHotspot(ssid="daytime_use_only", when=dict(during="06:00-22:00"))

Geometric range scope (linear events):
--------------------------------------
Geometric range scoping allows a value to be tied to a sub-segment of a linear path using linear
referencing. When a model is decorated with geometric range scoping, a `between` field is
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are point and linear events and side treated differently from other scopes? Specifically, at / between / side at the top level where the others are all nested under when.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historical reasons.

The scoping concept came together somewhat organically (inductive reasoning). The existence of when can be explained by the fact that some fool thought "it read nicer in JSON", i.e. { "between": [0, 0.5], "when": { "mode": ["foot"] } }...

In retrospect, the existence of when seems regrettable. We should probably phase it out. But for now, it is what it is...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to phrase it, but we should create an issue (and probably a label for breaking changes we want to make generally) to capture phasing this out, even if we don't have a good idea for how to migrate it yet.

Comment on lines +231 to +241
>>> island = BusLoadingIsland(
... id='island_001',
... theme='bus_terminal',
... type='loading_island',
... version=1,
... geometry=Geometry.from_wkt('LINESTRING (0 0, 1 1)'),
... stops=[
... BusStop(route='15', side=Side.LEFT, at=0.10),
... BusStop(route='B-Line', side=Side.RIGHT, at=0.25),
... ]
... )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking through how I'd expand on this by modeling standalone bus stops... It seems like I'd need a different, non-scoped BusStop that extends from OvertureFeature if I want to model them directly. (I might mix in fields that are common to all bus stops, like route.)

How would I model bus stops along road segments (without changing the RoadSegment model, unless there's a generic point_events field or similar to capture these)? I suppose I could reference the road segment with an at (to indicate where on the road segment the stop is). It would kind of look like the equivalent of a point event even though it's not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modeling them along road segments might be done in several ways but they're all variations of two options.

If you can change the road segment model then you probably add a bus_stops field similar to how it's down in the example.

If you can't change the road segment model, it probably means that bus stops are independent point features. If we want them to reference the road network you end up with something like (maybe):

@scoped(Scope.GEOMETRIC_POSITION)
class BusStop(OvertureFeature[Literal["transportation"], Literal["bus_stop"]]):
    geometry: Annotated[
        Geometry,
        GeometryTypeConstraint(GeometryType.POINT)
    ]

    road_segment_id: Annotated[
        Id | None, 
        Reference(Relationship.CONNECTS_TO, RoadSegment)
    ]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I was envisioning, but I was thinking 1 step further: a linear reference to the road segment where it not only describes what it connects to, but where.

We could do something like this:

@scoped(Scope.GEOMETRIC_POSITION)
class BusStop(OvertureFeature[Literal["transportation"], Literal["bus_stop"]]):
    geometry: Annotated[
        Geometry,
        GeometryTypeConstraint(GeometryType.POINT)
    ]

    road_segment_id: Annotated[
        Id | None, 
        Reference(Relationship.CONNECTS_TO, RoadSegment)
    ]

    road_segment_at: LinearlyReferencedPosition | None = None

but the combination of the CONNECTS_TO and linear references implies to me that some form of nested structure would be valuable, especially if both are required at the same time.

Comment on lines +232 to +236
return (
"The linearly-referenced position on the geometry, "
"specified as a percentage displacement from the start "
f"of the geometry, that the containing {parent} applies to."
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can / should these pull from enum docstrings or related doc properties? It would help centralize the strings.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These get added as Pydantic descriptions on the fields that are generated by the @scoped decorator. So code generation software just has to traverse the model as it normally does and it'll see the descriptions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking more about the maintenance benefits of centralization than code generation.

Comment on lines +312 to +313
At least one `Scope` must be given either in `optional` or `required` or both. A scope may be
optional or required, but not both.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does one indicate that a scope must be provided without indicating which one?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's currently not possible.

I didn't think it was needed for any existing use case, but if I'm wrong let me know so I can fix it.

(Assuming we don't use that construct today: I can imagine it's something we'll need in the future. The best thing to do would be to drop when and just use @require_any_of to indicate this constraint. With when in the mix, it's messy.)

Copy link
Collaborator

@mojodna mojodna Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SpeedLimitWhenClause is what I was thinking of, with @min_fields_set(1)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Yeah, that was a faithful modeling of this hand-written JSON Schema. At least for now it's still just trying to block multiple ways of the when clause saying the same thing.

@vcschapp vcschapp merged commit 4315747 into pydantic Oct 31, 2025
6 checks passed
@vcschapp vcschapp deleted the pp4 branch October 31, 2025 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants