-
Notifications
You must be signed in to change notification settings - Fork 11
Pydantic package organization episode 4: "Scope is in (scope)" #412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
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.
mojodna
left a comment
There was a problem hiding this 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" |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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")) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
| >>> 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), | ||
| ... ] | ||
| ... ) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
]There was a problem hiding this comment.
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 = Nonebut 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.
| 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." | ||
| ) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
packages/overture-schema-core/src/overture/schema/core/scoping/scoped.py
Show resolved
Hide resolved
| At least one `Scope` must be given either in `optional` or `required` or both. A scope may be | ||
| optional or required, but not both. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
tl;dr
This is part
#4. Part#5is 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 (
@scopeddecorator) 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
pydanticbranch rather than squashed.Journey
Destination
pyproject.tomlwill be standardized.make check.pydocstylewith the NumPy convention. (As of now, the tool reports too many complaints to add it intomake check.)pydocstylewill be enforced bymake check.docformatterwill be run bymake check.__init__.pydocstring.doctarget in theMakefilethat doesuv run pdocto a useful place.perspectives.