Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 108 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,3 +661,111 @@ def test_performance_add_multiple_accessed_with_paths(
by_id = {r["trov:arrangement"]["@id"]: r for r in accessed}
assert by_id["arrangement/0"]["trov:boundTo"] == "/mnt/a"
assert "trov:boundTo" not in by_id["arrangement/1"]


class TestExtraContextCLI:
"""Tests for --extra-context CLI option."""

def test_extra_context_prefix_mapping(
self, runner, tmp_path, temp_workspace, trs_profile
):
"""--extra-context PREFIX=URI adds a prefix mapping dict to @context."""
tro_file = tmp_path / "tro.jsonld"
result = runner.invoke(
cli,
[
"--declaration",
str(tro_file),
"--profile",
trs_profile,
"--extra-context",
"ex=http://example.org/",
"arrangement",
"add",
"--comment",
"test",
str(temp_workspace),
],
)
assert result.exit_code == 0, result.output
with open(tro_file) as f:
data = json.load(f)
assert {"ex": "http://example.org/"} in data["@context"]

def test_extra_context_multiple(
self, runner, tmp_path, temp_workspace, trs_profile
):
"""Multiple --extra-context PREFIX=URI flags all appear in @context."""
tro_file = tmp_path / "tro.jsonld"
result = runner.invoke(
cli,
[
"--declaration",
str(tro_file),
"--profile",
trs_profile,
"--extra-context",
"ex=http://example.org/",
"--extra-context",
"foaf=http://xmlns.com/foaf/0.1/",
"arrangement",
"add",
str(temp_workspace),
],
)
assert result.exit_code == 0, result.output
with open(tro_file) as f:
data = json.load(f)
assert {"ex": "http://example.org/"} in data["@context"]
assert {"foaf": "http://xmlns.com/foaf/0.1/"} in data["@context"]

def test_extra_context_invalid_format(
self, runner, tmp_path, temp_workspace, trs_profile
):
"""--extra-context value that is not PREFIX=URI produces an error."""
tro_file = tmp_path / "tro.jsonld"
result = runner.invoke(
cli,
[
"--declaration",
str(tro_file),
"--profile",
trs_profile,
"--extra-context",
"https://example.org/vocab.jsonld",
"arrangement",
"add",
str(temp_workspace),
],
)
assert result.exit_code != 0

def test_extra_context_base_context_preserved(
self, runner, tmp_path, temp_workspace, trs_profile
):
"""The standard base @context entry is always present even with extra context."""
tro_file = tmp_path / "tro.jsonld"
runner.invoke(
cli,
[
"--declaration",
str(tro_file),
"--profile",
trs_profile,
"--extra-context",
"ex=http://example.org/",
"arrangement",
"add",
str(temp_workspace),
],
)
with open(tro_file) as f:
data = json.load(f)
base = data["@context"][0]
assert "trov" in base

def test_cli_help_shows_extra_context(self, runner):
"""--extra-context option is visible in CLI help output."""
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "extra-context" in result.output
85 changes: 85 additions & 0 deletions tests/test_tro_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2363,3 +2363,88 @@ def test_verification_result_is_valid_property(self):

bad = VerificationResult(files_missing_in_arrangement=["x"])
assert not bad.is_valid


class TestExtraContext:
"""Tests for extra vocabulary support in @context."""

def test_extra_context_default_empty(self):
"""TransparentResearchObject.extra_context defaults to an empty list."""
tro = TransparentResearchObject()
assert tro.extra_context == []

def test_extra_context_dict_in_jsonld(self):
"""A dict extra_context entry appears in the serialised @context."""
tro = TransparentResearchObject()
tro.extra_context = [{"ex": "http://example.org/"}]
data = tro.to_jsonld()
assert {"ex": "http://example.org/"} in data["@context"]

def test_extra_context_multiple_entries(self):
"""Multiple extra_context dict entries all appear in @context."""
tro = TransparentResearchObject()
tro.extra_context = [
{"ex": "http://example.org/"},
{"foaf": "http://xmlns.com/foaf/0.1/"},
]
data = tro.to_jsonld()
assert {"ex": "http://example.org/"} in data["@context"]
assert {"foaf": "http://xmlns.com/foaf/0.1/"} in data["@context"]

def test_extra_context_base_context_still_present(self):
"""The standard base context is always the first entry in @context."""
tro = TransparentResearchObject()
tro.extra_context = [{"ex": "http://example.org/"}]
data = tro.to_jsonld()
base = data["@context"][0]
assert "trov" in base
assert "rdf" in base

def test_extra_context_roundtrip(self, tmp_path):
"""Extra context dict entries survive a save/load roundtrip."""
tro = TransparentResearchObject()
tro.extra_context = [
{"ex": "http://example.org/"},
{"foaf": "http://xmlns.com/foaf/0.1/"},
]
filepath = tmp_path / "tro_with_extra_ctx.jsonld"
tro.save(str(filepath))

loaded = TransparentResearchObject.load(str(filepath))
assert {"ex": "http://example.org/"} in loaded.extra_context
assert {"foaf": "http://xmlns.com/foaf/0.1/"} in loaded.extra_context

def test_extra_context_via_tro_facade(self, tmp_path):
"""TRO facade propagates extra_context to the underlying model."""
tro_file = tmp_path / "tro.jsonld"
tro = TRO(
filepath=str(tro_file),
extra_context=[{"ex": "http://example.org/"}],
)
tro.save()

with open(tro_file) as f:
data = json.load(f)
assert {"ex": "http://example.org/"} in data["@context"]

def test_extra_context_via_tro_facade_extends_existing(self, tmp_path):
"""TRO facade appends extra_context to any context already in the file."""
tro_file = tmp_path / "tro.jsonld"
# First save with one extra context entry
tro = TRO(
filepath=str(tro_file),
extra_context=[{"ex": "http://example.org/"}],
)
tro.save()

# Re-open and add a second entry
tro2 = TRO(
filepath=str(tro_file),
extra_context=[{"foaf": "http://xmlns.com/foaf/0.1/"}],
)
tro2.save()

with open(tro_file) as f:
data = json.load(f)
assert {"ex": "http://example.org/"} in data["@context"]
assert {"foaf": "http://xmlns.com/foaf/0.1/"} in data["@context"]
37 changes: 37 additions & 0 deletions tro_utils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ def _parse_arrangement_ref(value: str) -> tuple[str, str | None]:
return (arrangement_id, path or None)


def _parse_extra_context_value(ctx, param, value):
"""Parse ``--extra-context`` values into JSON-LD context dicts.

Each value must be in ``PREFIX=URI`` form (e.g. ``ex=http://example.org/``).
"""
result = []
for v in value:
if "=" not in v:
raise click.BadParameter(
f"{v!r} is not a valid PREFIX=URI mapping.",
param=param,
ctx=ctx,
)
prefix, _, uri = v.partition("=")
result.append({prefix: uri})
return result


_TEMPLATES = {
"default": {
"description": "Default pretty template by Craig Willis",
Expand Down Expand Up @@ -104,6 +122,20 @@ def convert(self, value, param, ctx):
required=False,
help="TRO description (only used when creating a new TRO)",
)
@click.option(
"--extra-context",
"-c",
envvar="TRO_EXTRA_CONTEXT",
type=click.STRING,
required=False,
multiple=True,
callback=_parse_extra_context_value,
is_eager=False,
help=(
"Extra PREFIX=URI vocabulary mapping to append to @context "
"(e.g. ex=http://example.org/). May be repeated."
),
)
def cli(
declaration,
profile,
Expand All @@ -112,6 +144,7 @@ def cli(
tro_creator,
tro_name,
tro_description,
extra_context,
):
pass

Expand Down Expand Up @@ -308,6 +341,7 @@ def add(ctx, directory, ignore_dir, comment, from_snapshot):
tro_name = ctx.params.get("tro_name")
tro_description = ctx.params.get("tro_description")
tro_creator = ctx.params.get("tro_creator")
extra_context = ctx.params.get("extra_context") or []
tro = TRO(
filepath=declaration,
gpg_fingerprint=gpg_fingerprint,
Expand All @@ -316,6 +350,7 @@ def add(ctx, directory, ignore_dir, comment, from_snapshot):
tro_creator=tro_creator,
tro_name=tro_name,
tro_description=tro_description,
extra_context=extra_context or None,
)
if from_snapshot:
tro.add_arrangement_from_snapshot(from_snapshot, comment=comment)
Expand Down Expand Up @@ -430,11 +465,13 @@ def performance_add(ctx, comment, start, end, attribute, accessed, modified):
gpg_fingerprint = ctx.params.get("gpg_fingerprint")
gpg_passphrase = ctx.params.get("gpg_passphrase")
profile = ctx.params.get("profile")
extra_context = ctx.params.get("extra_context") or []
tro = TRO(
filepath=declaration,
gpg_fingerprint=gpg_fingerprint,
gpg_passphrase=gpg_passphrase,
profile=profile,
extra_context=extra_context or None,
)
tro.add_performance(
start,
Expand Down
7 changes: 6 additions & 1 deletion tro_utils/models/tro.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class TransparentResearchObject(TROVModel):
arrangements: list[ArtifactArrangement] = field(default_factory=list)
performances: list[TrustedResearchPerformance] = field(default_factory=list)
attributes: list[TROAttribute] = field(default_factory=list)
extra_context: list[dict] = field(default_factory=list)

# ------------------------------------------------------------------
# File I/O
Expand Down Expand Up @@ -326,7 +327,7 @@ def to_jsonld(self) -> dict[str, Any]:
graph_node["trov:hasPerformance"][i].update(extra)

return {
"@context": _JSONLD_CONTEXT,
"@context": _JSONLD_CONTEXT + self.extra_context,
"@graph": [graph_node],
}

Expand All @@ -345,6 +346,9 @@ def from_jsonld(cls, data: dict[str, Any]) -> "TransparentResearchObject":
"""
graph = data["@graph"][0]

raw_context = data.get("@context", [])
extra_context = list(raw_context[1:]) if isinstance(raw_context, list) else []

vocab_version = Version(graph.get("trov:vocabularyVersion", "0.0.1"))
if vocab_version < TROV_VOCABULARY_VERSION:
raise RuntimeError(
Expand Down Expand Up @@ -417,4 +421,5 @@ def from_jsonld(cls, data: dict[str, Any]) -> "TransparentResearchObject":
arrangements=arrangements,
performances=performances,
attributes=attributes,
extra_context=extra_context,
)
4 changes: 4 additions & 0 deletions tro_utils/tro_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(
tro_creator=None,
tro_name=None,
tro_description=None,
extra_context=None,
):
if filepath is None:
self.basename = "some_tro"
Expand Down Expand Up @@ -80,6 +81,9 @@ def __init__(
else:
self._model = TransparentResearchObject.load(self.tro_filename)

if extra_context:
self._model.extra_context.extend(extra_context)

self.gpg = gnupg.GPG(gnupghome=GPG_HOME, verbose=False)
if gpg_fingerprint:
self.gpg_key_id = self.gpg.list_keys().key_map[gpg_fingerprint]["keyid"]
Expand Down
Loading