Skip to content

fix(core): return a list from load_entity and pair iris by title - #208

Merged
LukasGold merged 2 commits into
mainfrom
fix/198-load-entity-result-shape
Sep 23, 2026
Merged

LukasGold merged 2 commits into
mainfrom
fix/198-load-entity-result-shape

Conversation

@LukasGold

Copy link
Copy Markdown
Contributor

Closes #198.

Changes

  • LoadEntityResult.entities is now List[model.OswBaseModel] instead of
    Union[model.OswBaseModel, List[model.OswBaseModel]]
    (src/osw/core.py:1292).
  • OswDefaultBackend.resolve() builds a {full_title: entity} lookup and
    iterates request.iris, instead of zip-ping the two lists
    (src/osw/core.py:271-283).
  • An iri with no matching entity is logged with _logger.warning and left
    out of nodes.
  • export_entity_jsonld drops its isinstance(entities, list) branch
    (src/osw/service/ops/entities.py:94).
  • New offline test file tests/test_load_entity_result_shape.py.

Rationale

The empty result was not a list. pydantic v1 tries the first member of a
Union first, and OswBaseModel.validate([]) succeeds because it falls back
to dict([]) == {}. A load that produced no entity therefore returned
OswBaseModel(), which is truthy, not subscriptable and has no length.
Reproduced in the project environment. load_entity is the only place that
constructs LoadEntityResult and always passes a list, so the Union had no
other purpose. A non-empty list was never affected, and the element subclass
still survives validation.

resolve() paired iris with the wrong entities. load_entity skips a
page whose schema is missing (src/osw/core.py:1387) or whose construction
fails (src/osw/core.py:1407). The returned list is then shorter than the
requested titles, so zip moved every later iri onto the entity of a
following page. load_entity already sets entity.meta.wiki_page.namespace
and .title, which is what get_full_title reads, so the title is available
for an exact pairing.

The isinstance branch in export_entity_jsonld wrapped the non-list result
back into a list, so its if not entities check never raised NotFound. With
the field typed as a list the branch is dead code.

Verification

  • Both defect tests were run against the unfixed source first and fail there:
    test_load_entity_result_keeps_empty_list_as_list and
    test_resolve_pairs_iris_by_title_not_position.
  • pytest tests -q: 800 passed.
  • make check: pre-commit, ruff, ty and deptry all pass.

Note

#166 changes the class
selection inside the same function but touches neither of these two
behaviours, so the two branches do not overlap in substance.

- LoadEntityResult.entities is now List[OswBaseModel]; pydantic v1 tried
  the Union's first member, so an empty result became a truthy
  OswBaseModel that is neither subscriptable nor sized
- resolve() keys the loaded entities by full page title instead of
  zipping them against request.iris; load_entity skips pages it cannot
  build, which shifted every later iri onto the wrong entity
- an iri with no matching entity is logged and left out of nodes
- export_entity_jsonld drops the now dead isinstance branch that hid the
  empty result from its NotFound check

Closes #198
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Release preview

Merging this PR would release v2.8.1 (current: v2.8.0).

Changelog preview (truncated)
## v2.8.1 (2026-09-23)

### Bug Fixes

- **core**: Map an unresolved iri to None instead of omitting it
  ([`19130e6`](https://github.com/OpenSemanticLab/osw-python/commit/19130e64d6ed56527ef240d8eef901169211e680))

- **core**: Return a list from load_entity and pair iris by title
  ([`6c6ee6d`](https://github.com/OpenSemanticLab/osw-python/commit/6c6ee6da03467fafc71a54a0caf3dbf4a3d51eb2))

### Documentation

- Point repo links at the versioned /latest/ docs paths
  ([#206](https://github.com/OpenSemanticLab/osw-python/pull/206),
  [`c21ad24`](https://github.com/OpenSemanticLab/osw-python/commit/c21ad241078e64d05171b9ae2907c007590384b1))

Preview via python-semantic-release and conventional commits.

oold types ResolveResult.nodes as Dict[str, Union[None, ...]] and indexes
it by iri in three places without checking for the key, so omitting an
unresolved iri turns a skipped page into a KeyError.
@LukasGold

Copy link
Copy Markdown
Contributor Author

Follow-up commit 19130e6: an unresolved iri now maps to None in
ResolveResult.nodes instead of being left out.

oold types the field as Dict[str, Union[None, GenericLinkedBaseModel]]
(oold/backend/interface.py:56) and indexes it by iri without checking for
the key in three places (oold/model/v1/__init__.py:381, :386, :571).
Omitting the key therefore turned a skipped page into a KeyError with only
the iri as its message. Two of those three call sites already test the value
for truthiness, which is the contract None satisfies.

The warning log is unchanged. The test now asserts
result.nodes["Item:OSWAlignBad"] is None, and still fails against the
unfixed source.

@LukasGold
LukasGold merged commit a30dae1 into main Sep 23, 2026
12 checks passed
@LukasGold
LukasGold deleted the fix/198-load-entity-result-shape branch September 23, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

load_entity: an empty result is not a list, and resolve() pairs IRIs with the wrong entities

2 participants