From 108079ba16b865ab11d069be6496042dac294fa2 Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Sun, 27 Sep 2026 19:40:29 +0200 Subject: [PATCH] fix: restore required semantics for a bare Link[T] Reverts the optionality change shipped in 2.0.0. #159 records a preliminary decision for option 2, then a counter-proposal keeping the bare form required and raising the unresolved chain-resolution question. The design is not settled, so the library should not have adopted one side of it. The emitter conformance work from the same pull request stays. --- src/oold/model/_descriptor.py | 20 ++++++-------------- tests/test_notation.py | 18 +++++++----------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/oold/model/_descriptor.py b/src/oold/model/_descriptor.py index 94b5314..c31a49a 100644 --- a/src/oold/model/_descriptor.py +++ b/src/oold/model/_descriptor.py @@ -189,20 +189,12 @@ def _neutralised(info: Any) -> Any: namespace[field_name] = _neutralised(info) continue if field_name not in namespace and _is_link_annotation(annotation): - # `manager: Link[Org]` with nothing assigned. It still needs a field - # carrying default=None - a link cannot be required at the pydantic - # level, because its value never reaches validation, and left alone - # every construction failed with a misleading "Field required" about - # a value that had in fact been supplied. - # - # It is *not* required in the OO-LD sense: that is what - # OoldField(required=True) says. Requiredness propagates into - # resolution - resolving a link constructs the target - so a - # required link makes every stored document lacking it - # unconstructible, and a self-referential link could never be - # satisfied by a real dataset. Links are declared far more often - # than they are required, so the terse form is the common case. - namespace[field_name] = OoldField() + # `manager: Link[Org]` with nothing assigned. Read as Python reads + # it - no default means required - but a link cannot be required as + # a pydantic field, because its value never reaches validation. + # Left alone, every construction failed with a misleading + # "Field required" about a value that had in fact been supplied. + namespace[field_name] = OoldField(required=True) continue # A Field() living in Annotated metadata rather than as the assigned # value was never seen here, so its default survived and was evaluated diff --git a/tests/test_notation.py b/tests/test_notation.py index 421aa1c..1fde8da 100644 --- a/tests/test_notation.py +++ b/tests/test_notation.py @@ -301,15 +301,11 @@ class Old(OoldModel): assert Old.__link_fields__["manager"].required_iri is True -def test_a_link_annotation_without_a_default_is_optional(): - """Links are declared far more often than they are required, so the terse - form is the common case. - - Requiredness is explicit because it propagates into resolution: resolving a - link constructs the target, so a required link makes every stored document - lacking it unconstructible - and a self-referential link, `father`, could - then never be satisfied by a real dataset. - """ +def test_a_link_annotation_without_a_default_is_required(): + """No default means required, as it does anywhere else in Python. A link is + never required at the pydantic level - its value is routed out before + validation - so this used to fail with a misleading "Field required" about + a value that had in fact been supplied.""" class Bare(OoldModel): id: str @@ -317,6 +313,6 @@ class Bare(OoldModel): manager: Link["Org"] Bare.model_rebuild() - assert Bare(id="ex:b").link_iris("manager") is None # constructs unset assert Bare(id="ex:b", manager="ex:acme").link_iris("manager") == "ex:acme" - assert Bare.__link_fields__["manager"].required_iri is False + with pytest.raises(ValueError, match="manager is required"): + Bare(id="ex:b")