diff --git a/pyproject.toml b/pyproject.toml index d28f60fd..49b64ee7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" ] diff --git a/src/hermes/model/types/ld_context.py b/src/hermes/model/types/ld_context.py index 9bb8209a..3d60bb41 100644 --- a/src/hermes/model/types/ld_context.py +++ b/src/hermes/model/types/ld_context.py @@ -5,6 +5,7 @@ # SPDX-FileContributor: Michael Meinel # SPDX-FileContributor: Stephan Druskat +from hermes.model.error import HermesContextError CODEMETA_PREFIX = "https://doi.org/10.5063/schema/codemeta-2.0" CODEMETA_CONTEXT = [CODEMETA_PREFIX] @@ -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: @@ -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 @@ -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: """ @@ -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) diff --git a/test/hermes_test/model/types/test_ld_context.py b/test/hermes_test/model/types/test_ld_context.py index 03fd1cf3..2c155b6f 100644 --- a/test/hermes_test/model/types/test_ld_context.py +++ b/test/hermes_test/model/types/test_ld_context.py @@ -10,6 +10,8 @@ ALL_CONTEXTS, ) +from hermes.model.error import HermesContextError + @pytest.fixture def ctx(): @@ -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" @@ -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( @@ -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]