From 16ee91600255d6f6bab27f0edb06ba19e5f6e728 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 19 Oct 2026 16:09:00 +0200 Subject: [PATCH 1/2] fix(bindings): compare serialized values in matches_with Literal[T] and Datatype fields hold plain Python values, so calling .n3() on them raised AttributeError. Comparing model_dump() output applies each field's own serializer, keeping all field types consistent. Fixes #76 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/knowledge_mapper/ke/models.py | 11 ++++- tests/test_bindings.py | 68 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/knowledge_mapper/ke/models.py b/src/knowledge_mapper/ke/models.py index 4c04ec0..c38f1ab 100644 --- a/src/knowledge_mapper/ke/models.py +++ b/src/knowledge_mapper/ke/models.py @@ -219,11 +219,18 @@ def dump_partial_binding(self, exclude_none: bool = True) -> dict[str, Any]: return self.model_dump(by_alias=True, exclude_none=exclude_none) def matches_with(self, **kwargs) -> bool: + """Return whether every set field equals the given N3-encoded value. + + Fields are compared in their serialized (N3) form, so ``Literal[T]`` and + ``Datatype`` fields compare the same way as ``Uri`` and ``RdfLiteral``. + Unset (``None``) fields match anything. + """ + own_values = self.model_dump() for key, other_value in kwargs.items(): - own_value = getattr(self, key) + own_value = own_values[key] if own_value is None: continue - if own_value.n3() == other_value: + if own_value == other_value: continue return False return True diff --git a/tests/test_bindings.py b/tests/test_bindings.py index a14b7ad..8878ccd 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -302,3 +302,71 @@ class Binding(BindingModel): assert binding.remark == RDFLiteral("foo", lang="de") with pytest.raises(ValidationError, match="datatype"): binding.temperature = '"12.5"^^' + + +# ---------- matches_with + + +class MatchBinding(BindingModel): + person: Uri + age: Literal[int] + name: Literal[str] + born: Literal[datetime] + remark: RdfLiteral + temperature: Annotated[Literal[float], Datatype(CELSIUS)] + + +FULL_MATCH_BINDING = MatchBinding( + person=URIRef("http://example.org/a"), + age=3, + name="Alice", + born=datetime(2020, 1, 2, 3, 4, 5), + remark=RDFLiteral("foo", lang="de"), + temperature=12.5, +) + + +@pytest.mark.parametrize( + ("field", "matching", "different"), + [ + ("person", "", ""), + ( + "age", + '"3"^^', + '"4"^^', + ), + ("name", '"Alice"', '"Bob"'), + ( + "born", + '"2020-01-02T03:04:05"^^', + '"2021-01-02T03:04:05"^^', + ), + ("remark", '"foo"@de', '"foo"@en'), + ( + "temperature", + '"12.5"^^', + '"12.5"^^', + ), + ], +) +def test_matches_with_per_field_type(field, matching, different): + assert FULL_MATCH_BINDING.matches_with(**{field: matching}) + assert not FULL_MATCH_BINDING.matches_with(**{field: different}) + + +def test_matches_with_ignores_unset_fields(): + binding = MatchBinding(person=URIRef("http://example.org/a")) + + assert binding.matches_with( + person="", + age='"4"^^', + temperature='"1.0"^^', + ) + + +def test_matches_with_binding_for_all_field_types(): + partial = MatchBinding(age=3, name="Alice", temperature=12.5) + other = MatchBinding(age=4, name="Alice", temperature=12.5) + + assert FULL_MATCH_BINDING.matches_with_binding(partial) + assert not FULL_MATCH_BINDING.matches_with_binding(other) From 111486fbbc88ddb2d6a1c91ed4167c38b7eb2908 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 27 Oct 2026 14:26:53 +0100 Subject: [PATCH 2/2] style(tests): remove trailing whitespace from merge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_bindings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_bindings.py b/tests/test_bindings.py index 72f37f4..b3ccf1e 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -372,8 +372,8 @@ def test_matches_with_binding_for_all_field_types(): assert FULL_MATCH_BINDING.matches_with_binding(partial) assert not FULL_MATCH_BINDING.matches_with_binding(other) - - + + # ---------- UNDEFINED sentinels UNDEFINED_N3 = ""