fix: Element.attrs returns all values of a multivalued attribute (0.2.0) - #10
Conversation
attrs walked the element's triples assigning attrs[name] = value, so each repeated value overwrote the last. A caller reading an attribute declared multiple=True got one arbitrary value with no indication the others existed, and which one it got depended on triple-store iteration order. An attribute declared multiple=True now comes back as a sorted list of its values, even when it currently holds one, so the type does not depend on how many values happen to be recorded. Every other attribute is unchanged. Sorting is what makes repeated reads agree, the store having no inherent order of its own. Adds SchemaBuilder.multivalued_attributes(type_name), since describe_type returns repr strings and nothing else exposed the descriptors. Version 0.2.0 rather than 0.1.1: main already carries a breaking change since the 0.1.0 release, the core namespace having moved from https://example.org/kc# to https://w3id.org/kc#. A downstream SHACL constraint written against the wrong namespace matches nothing rather than failing, so every edge silently appears untyped.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Element.attrs currently catches Exception broadly, which can mask unexpected failures and should be narrowed to the expected error cases.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
This PR fixes Element.attrs so schema-declared multivalued attributes (multiple=True) are returned as a complete, deterministic collection (sorted list) instead of an arbitrary single value, and exposes schema metadata to let callers distinguish multivalued attributes when reading elements back.
Changes:
- Update
Element.attrsto aggregate and sort values for multivalued attributes while preserving single-valued behavior for others. - Add
SchemaBuilder.multivalued_attributes(type_name)to report which attributes are declaredmultiple=True(including inheritance). - Add regression tests and tutorial documentation describing the new behavior; bump package version to
0.2.0.
| File | Description |
|---|---|
knowledgecomplex/graph.py |
Aggregates repeated attribute triples into sorted lists for schema-declared multivalued attributes. |
knowledgecomplex/schema.py |
Adds schema API to enumerate multivalued attributes for a given type (incl. inherited). |
tests/test_element.py |
Adds regression tests covering multivalued aggregation, stable typing, and schema reporting. |
docs/tutorial.md |
Documents attrs multivalued behavior and the new schema helper API. |
pyproject.toml |
Bumps project version to 0.2.0. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| try: | ||
| multivalued = self._kc._schema.multivalued_attributes(self.type) | ||
| except Exception: | ||
| # An element whose type is unregistered or absent still has attributes | ||
| # worth reading; fall back to treating them all as single-valued. | ||
| multivalued = frozenset() |
Catching Exception could mask a real failure inside multivalued_attributes and report it as "this type has no multivalued attributes", which would silently return the very collapsed values this change set out to fix. Only two errors are expected: ValueError from .type when the element carries no user type, and SchemaError when that type is not registered. Both mean the same thing — the schema cannot say which attributes are multivalued — and the attributes are still worth reading, so the fallback stands for those and for nothing else. Two tests, one for each side of the boundary: an element with no registered type still reads, and an unexpected error is not swallowed.
|
Addressed in f38214b. Narrowed to The catch was worth making specific for a reason beyond hygiene: a failure inside Added a test for each side of the boundary — an element with no registered type still reads its attributes, and an unexpected error surfaces rather than being swallowed. Full suite: 404 passed, 6 skipped. |

What was wrong
Element.attrswalked the element's triples assigningattrs[name] = value, so each repeated value overwrote the last. An attribute declaredmultiple=Truecame back as one arbitrary value, with no indication the others existed — and which one you got depended on triple-store iteration order.Found while building a downstream model where
sourceandfactsare both multivalued: provenance was being silently dropped on read.What changed
multiple=Truereturns a sorted list, even when it currently holds one value — so the type doesn't depend on how many values happen to be recorded. Every other attribute is unchanged and still returns a string.SchemaBuilder.multivalued_attributes(type_name).describe_typeonly returns repr strings, so nothing else exposed the descriptors to a caller that needed to read an element back faithfully.Why 0.2.0 and not 0.1.1
mainalready carries a breaking change since the 0.1.0 release: the core namespace moved fromhttps://example.org/kc#tohttps://w3id.org/kc#(f4a04a8). That is worth calling out, because a downstream SHACL constraint written against the wrong namespace matches nothing rather than failing — every edge silently appears untyped. I hit exactly this pinning a dependent repo toknowledgecomplex>=0.1.0and resolving the stale PyPI wheel.Verification
402 passed, 6 skipped— full suite, no regressions.