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
8 changes: 4 additions & 4 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ def test_extra_context_prefix_mapping(
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 data["@context"]["ex"] == "http://example.org/"

def test_extra_context_multiple(
self, runner, tmp_path, temp_workspace, trs_profile
Expand All @@ -716,8 +716,8 @@ def test_extra_context_multiple(
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"]
assert data["@context"]["ex"] == "http://example.org/"
assert data["@context"]["foaf"] == "http://xmlns.com/foaf/0.1/"

def test_extra_context_invalid_format(
self, runner, tmp_path, temp_workspace, trs_profile
Expand Down Expand Up @@ -761,7 +761,7 @@ def test_extra_context_base_context_preserved(
)
with open(tro_file) as f:
data = json.load(f)
base = data["@context"][0]
base = data["@context"]
assert "trov" in base

def test_cli_help_shows_extra_context(self, runner):
Expand Down
56 changes: 28 additions & 28 deletions tests/test_extra_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,82 +12,82 @@ class TestExtraContext:
"""Tests for extra vocabulary support in @context."""

def test_extra_context_default_empty(self):
"""TransparentResearchObject.extra_context defaults to an empty list."""
"""TransparentResearchObject.extra_context defaults to an empty dict."""
tro = TransparentResearchObject()
assert tro.extra_context == []
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/"}]
tro.extra_context = {"ex": "http://example.org/"}
data = tro.to_jsonld()
assert {"ex": "http://example.org/"} in data["@context"]
assert data["@context"]["ex"] == "http://example.org/"

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

def test_extra_context_base_context_still_present(self):
"""The standard base context is always the first entry in @context."""
"""The standard base context keys are always present in @context."""
tro = TransparentResearchObject()
tro.extra_context = [{"ex": "http://example.org/"}]
tro.extra_context = {"ex": "http://example.org/"}
data = tro.to_jsonld()
base = data["@context"][0]
base = data["@context"]
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."""
"""Extra context entries survive a save/load roundtrip."""
tro = TransparentResearchObject()
tro.extra_context = [
{"ex": "http://example.org/"},
{"foaf": "http://xmlns.com/foaf/0.1/"},
]
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
assert loaded.extra_context["ex"] == "http://example.org/"
assert loaded.extra_context["foaf"] == "http://xmlns.com/foaf/0.1/"

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/"}],
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"]
assert data["@context"]["ex"] == "http://example.org/"

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 facade merges extra_context into 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/"}],
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/"}],
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"]
assert data["@context"]["ex"] == "http://example.org/"
assert data["@context"]["foaf"] == "http://xmlns.com/foaf/0.1/"
6 changes: 3 additions & 3 deletions tro_utils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ def _parse_arrangement_ref(value: str) -> tuple[str, str | None]:


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

Each value must be in ``PREFIX=URI`` form (e.g. ``ex=http://example.org/``).
"""
result = []
result = {}
for v in value:
if "=" not in v:
raise click.BadParameter(
Expand All @@ -35,7 +35,7 @@ def _parse_extra_context_value(ctx, param, value):
ctx=ctx,
)
prefix, _, uri = v.partition("=")
result.append({prefix: uri})
result[prefix] = uri
return result


Expand Down
31 changes: 19 additions & 12 deletions tro_utils/models/tro.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,12 @@

TROV_VOCABULARY_VERSION = Version("0.1")

_JSONLD_CONTEXT = [
{
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"trov": f"https://w3id.org/trace/trov/{TROV_VOCABULARY_VERSION}#",
"schema": "https://schema.org",
}
]
_JSONLD_CONTEXT = {
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"trov": f"https://w3id.org/trace/trov/{TROV_VOCABULARY_VERSION}#",
"schema": "https://schema.org/",
}


@dataclass
Expand All @@ -56,7 +54,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)
extra_context: dict = field(default_factory=dict)

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

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

Expand All @@ -346,8 +344,17 @@ 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 []
raw_context = data.get("@context", {})
if isinstance(raw_context, list):
# backward compat: merge list of dicts into a single dict
merged: dict = {}
for item in raw_context:
if isinstance(item, dict):
merged.update(item)
raw_context = merged
extra_context = {
k: v for k, v in raw_context.items() if k not in _JSONLD_CONTEXT
}

vocab_version = Version(graph.get("trov:vocabularyVersion", "0.0.1"))
if vocab_version < TROV_VOCABULARY_VERSION:
Expand Down
2 changes: 1 addition & 1 deletion tro_utils/tro_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def __init__(
self._model = TransparentResearchObject.load(self.tro_filename)

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

self.gpg = gnupg.GPG(gnupghome=GPG_HOME, verbose=False)
if gpg_fingerprint:
Expand Down
Loading