Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
78af0b5
Use correct field name in tests
sdruskat Aug 7, 2025
4cb2af0
Mark test as expectedly failing due to #419
sdruskat Aug 7, 2025
32c41f1
Make passing xfail tests fail test suite
sdruskat Aug 7, 2025
9e7ecf5
Mark another test as expectedly failing due to #419
sdruskat Aug 7, 2025
d4e2366
Parametrize xfailing test getting CodeMeta items, re-format
sdruskat Aug 7, 2025
1f4501c
Test correct inputs for getting expanded terms from prefixed vocabula…
sdruskat Aug 7, 2025
5512f7e
Test raises when prefix doesn't exist for compacted input
sdruskat Aug 7, 2025
3262691
Test raises when term doesn't exist for compacted input
sdruskat Aug 7, 2025
241f914
Remove duplicate tests, and not-to-be-tested parameters
sdruskat Aug 7, 2025
ebe9200
Improve reporting unexpected exceptions and add tests for expanded in…
sdruskat Aug 7, 2025
de0f24a
Add test for unimplemented and undecided functionality
sdruskat Aug 7, 2025
3793e7c
Report unexpectedly raised exception
sdruskat Aug 7, 2025
ad4221b
Merge branch 'refactor/define-model-errors' into refactor/384-mark-ex…
sdruskat Aug 7, 2025
6c4a523
Raise new error when getting term whose prafix is not in context
sdruskat Aug 7, 2025
1242311
Test that non-existent prefix raises error on getting item
sdruskat Aug 7, 2025
9621e77
Add xfailing test for returning only existing terms from given vocabu…
sdruskat Aug 7, 2025
9a980b4
Test raising context errors where implemented
sdruskat Aug 7, 2025
b808152
Raise context error on empty string, reformat
sdruskat Aug 7, 2025
0d61163
Pacify flake8
sdruskat Aug 7, 2025
e9ca175
Satisfy REUSE
sdruskat Aug 7, 2025
c72353e
Merge branch 'refactor/data-model' into refactor/384-mark-expected-fa…
SKernchen Dec 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ flake8 = "poetry run flake8 ./test/ ./src/ --count --select=E9,F63,F7,F82 --stat


[tool.pytest.ini_options]
minversion = "6.0"
norecursedirs = "docs/*"
xfail_strict = true
testpaths = [
"test"
]
Expand Down
54 changes: 36 additions & 18 deletions src/hermes/model/types/ld_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# SPDX-FileContributor: Michael Meinel
# SPDX-FileContributor: Stephan Druskat <stephan.druskat@dlr.de>

from hermes.model.error import HermesContextError

CODEMETA_PREFIX = "https://doi.org/10.5063/schema/codemeta-2.0"
CODEMETA_CONTEXT = [CODEMETA_PREFIX]
Expand All @@ -15,16 +16,26 @@
PROV_PREFIX = "http://www.w3.org/ns/prov#"
PROV_CONTEXT = [{"prov": PROV_PREFIX}]

HERMES_RT_PREFIX = 'https://schema.software-metadata.pub/hermes-runtime/1.0/'
HERMES_RT_CONTEXT = [{'hermes-rt': HERMES_RT_PREFIX}]
HERMES_CONTENT_CONTEXT = [{'hermes': 'https://schema.software-metadata.pub/hermes-content/1.0/'}]
HERMES_RT_PREFIX = "https://schema.software-metadata.pub/hermes-runtime/1.0/"
HERMES_RT_CONTEXT = [{"hermes-rt": HERMES_RT_PREFIX}]
HERMES_CONTENT_CONTEXT = [
{"hermes": "https://schema.software-metadata.pub/hermes-content/1.0/"}
]

HERMES_CONTEXT = [{**HERMES_RT_CONTEXT[0], **HERMES_CONTENT_CONTEXT[0]}]

HERMES_BASE_CONTEXT = [*CODEMETA_CONTEXT, {**SCHEMA_ORG_CONTEXT[0], **HERMES_CONTENT_CONTEXT[0]}]
HERMES_PROV_CONTEXT = [{**SCHEMA_ORG_CONTEXT[0], **HERMES_RT_CONTEXT[0], **PROV_CONTEXT[0]}]
HERMES_BASE_CONTEXT = [
*CODEMETA_CONTEXT,
{**SCHEMA_ORG_CONTEXT[0], **HERMES_CONTENT_CONTEXT[0]},
]
HERMES_PROV_CONTEXT = [
{**SCHEMA_ORG_CONTEXT[0], **HERMES_RT_CONTEXT[0], **PROV_CONTEXT[0]}
]

ALL_CONTEXTS = [*CODEMETA_CONTEXT, {**SCHEMA_ORG_CONTEXT[0], **PROV_CONTEXT[0], **HERMES_CONTEXT[0]}]
ALL_CONTEXTS = [
*CODEMETA_CONTEXT,
{**SCHEMA_ORG_CONTEXT[0], **PROV_CONTEXT[0], **HERMES_CONTEXT[0]},
]


class ContextPrefix:
Expand All @@ -37,6 +48,7 @@ class ContextPrefix:
arbitrary strings used to prefix terms from a specific vocabulary to their respective vocabulary IRI strings.;
- as a dict mapping prefixes to vocabulary IRIs, where the default vocabulary has a prefix of None.
"""

def __init__(self, vocabularies: list[str | dict]):
"""
@param vocabularies: A list of linked data vocabularies. Items can be vocabulary base IRI strings and/or
Expand All @@ -54,11 +66,13 @@ def __init__(self, vocabularies: list[str | dict]):
if isinstance(vocab, str):
vocab = {None: vocab}

self.context.update({
prefix: base_iri
for prefix, base_iri in vocab.items()
if isinstance(base_iri, str)
})
self.context.update(
{
prefix: base_iri
for prefix, base_iri in vocab.items()
if isinstance(base_iri, str)
}
)

def __getitem__(self, compressed_term: str | tuple) -> str:
"""
Expand All @@ -84,17 +98,21 @@ def __getitem__(self, compressed_term: str | tuple) -> str:
"""
if not isinstance(compressed_term, str):
prefix, term = compressed_term
elif ':' in compressed_term:
prefix, term = compressed_term.split(':', 1)
if term.startswith('://'):
elif ":" in compressed_term:
prefix, term = compressed_term.split(":", 1)
if term.startswith("://"):
prefix, term = True, compressed_term
else:
elif compressed_term != "":
prefix, term = None, compressed_term
else:
raise HermesContextError(compressed_term)

if prefix in self.context:
iri = self.context[prefix] + term
try:
base_iri = self.context[prefix]
except KeyError as ke:
raise HermesContextError(prefix) from ke

return iri
return base_iri + term


iri_map = ContextPrefix(ALL_CONTEXTS)
153 changes: 122 additions & 31 deletions test/hermes_test/model/types/test_ld_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
ALL_CONTEXTS,
)

from hermes.model.error import HermesContextError


@pytest.fixture
def ctx():
Expand All @@ -18,18 +20,29 @@ def ctx():

def test_ctx():
ctx = ContextPrefix(["u1", {"2": "u2"}])
assert ctx.prefix[None] == "u1"
assert ctx.prefix["2"] == "u2"
assert ctx.context[None] == "u1"
assert ctx.context["2"] == "u2"


@pytest.mark.xfail(
raises=AssertionError,
reason="Currently, the wrong CodeMeta IRI is used in the implementation: "
"https://github.com/softwarepub/hermes/issues/419",
)
def test_codemeta_prefix(ctx):
"""Default vocabulary in context has the correct base IRI."""
assert ctx.prefix[None] == "https://codemeta.github.io/terms/"
assert ctx.context[None] == "https://codemeta.github.io/terms/"


def test_get_codemeta_item(ctx):
@pytest.mark.xfail(
raises=AssertionError,
reason="Currently, the wrong CodeMeta IRI is used in the implementation, so expanding terms doesn't work correctly,"
" see https://github.com/softwarepub/hermes/issues/419",
)
@pytest.mark.parametrize("compacted", ["maintainer", (None, "maintainer")])
def test_get_item_from_default_vocabulary_pass(ctx, compacted):
"""Context returns fully expanded terms for default vocabulary in the context."""
item = ctx["maintainer"]
item = ctx[compacted]
assert item == "https://codemeta.github.io/terms/maintainer"


Expand All @@ -41,37 +54,104 @@ def test_get_codemeta_item(ctx):
"hermes:semanticVersion",
"https://schema.software-metadata.pub/hermes-content/1.0/semanticVersion", # TODO: Change on #393 fix
),
(("schema", "Organization"), "http://schema.org/Organization"),
(
("hermes", "semanticVersion"),
"https://schema.software-metadata.pub/hermes-content/1.0/semanticVersion",
), # TODO: Change on #393 fix
],
)
def test_get_prefixed_items(ctx, compacted, expanded):
"""Context returns fully expanded terms for prefixed vocabularies in the context."""
def test_get_item_from_prefixed_vocabulary_pass(ctx, compacted, expanded):
"""
Context returns fully expanded terms for prefixed vocabularies in the context,
for all accepted parameter formats.
"""
item = ctx[compacted]
assert item == expanded


def test_get_protocol_items_pass(ctx):
item = ctx["https://schema.org/Organisation"]
assert item == "https://schema.org/Organisation"
@pytest.mark.parametrize(
"prefix,not_exist",
[
("foobar", item)
for item in [
"foobar:baz",
("foobar", "baz"),
]
],
)
def test_get_item_from_prefixed_vocabulary_raises_on_prefix_not_exist(
ctx, prefix, not_exist
):
"""
Tests that an exception is raised when trying to get compacted items for which there is no
prefixed vocabulary in the context.
"""
with pytest.raises(HermesContextError) as hce:
_ = ctx[not_exist]
assert str(hce.value) == prefix


def test_get_protocol_items_fail(ctx):
with pytest.raises(Exception) as e:
ctx["https://foo.bar/baz"]
assert "cannot access local variable" not in str(e.value) # FIXME: Replace with custom error
@pytest.mark.parametrize(
"term,not_exist",
[
("baz", item)
for item in [
"baz",
"hermes:baz",
"schema:baz",
(None, "baz"),
("hermes", "baz"),
("schema", "baz"),
]
],
)
@pytest.mark.xfail(
raises=NotImplementedError,
reason="Not yet implemented/decided: Check if terms exist in given vocabulary.",
)
def test_get_item_from_prefixed_vocabulary_raises_on_term_not_exist(
ctx, term, not_exist
):
"""
Tests that an exception is raised when trying to get compacted items for which the vocabulary exists,
but doesn't contain the requested term.
"""
with pytest.raises(HermesContextError) as hce:
_ = ctx[not_exist]
with pytest.raises(Exception):
assert str(hce.value) == term
raise NotImplementedError


@pytest.mark.parametrize(
"compacted,expanded",
"expanded",
[
([None, "maintainer"], "https://codemeta.github.io/terms/maintainer"),
(["schema", "Organization"], "http://schema.org/Organization"),
((None, "maintainer"), "https://codemeta.github.io/terms/maintainer"),
(("schema", "Organization"), "http://schema.org/Organization"),
"https://codemeta.github.io/terms/maintainer",
"https://schema.org/Organisation",
"https://schema.software-metadata.pub/hermes-content/1.0/semanticVersion",
],
)
def test_get_valid_non_str_items(ctx, compacted, expanded):
"""Context returns fully expanded terms for valid non-string inputs."""
assert ctx[compacted] == expanded
@pytest.mark.xfail(
raises=NotImplementedError,
reason="Passing back expanded terms on their input if they are valid in the context "
"is not yet implemented (or decided).",
)
def test_get_item_from_expanded_pass(ctx, expanded):
"""
Tests that getting items via their fully expanded terms works as expected.
"""
with pytest.raises(Exception):
assert ctx[expanded] == expanded
raise NotImplementedError


def test_get_item_from_expanded_fail(ctx):
"""
Tests that context raises on unsupported expanded term input.
"""
with pytest.raises(HermesContextError):
ctx["https://foo.bar/baz"]


@pytest.mark.parametrize(
Expand All @@ -88,20 +168,31 @@ def test_get_non_str_item_fail(ctx, non_str, error_type):
"item",
[
"",
"fooBar",
pytest.param(
"fooBar",
marks=pytest.mark.xfail(
reason="Not yet implemented/decided: Check if terms exist in given vocabulary."
),
),
[0, "foo"],
(0, "foo"),
{"foo": "bar", "baz": "foo"},
"schema:fooBar",
"hermes:fooBar",
pytest.param(
"schema:fooBar",
marks=pytest.mark.xfail(
reason="Not yet implemented/decided: Check if terms exist in given vocabulary."
),
),
pytest.param(
"hermes:fooBar",
marks=pytest.mark.xfail(
reason="Not yet implemented/decided: Check if terms exist in given vocabulary."
),
),
"codemeta:maintainer", # Prefixed CodeMeta doesn't exist in context
# Even a dict with valid terms should fail, as it is unclear what to expect
{None: "maintainer", "schema": "Organization"},
],
)
def test_get_item_validate_fail(ctx, item):
"""Context raises on terms that don't exist in the context."""
with pytest.raises(
Exception
): # FIXME: Replace with custom error, e.g., hermes.model.errors.InvalidTermException
"""Context raises on theoretically valid compressed terms that don't exist in the context."""
with pytest.raises(HermesContextError):
ctx[item]
Loading