digitalbazaar / digitalbazaar/pyld
Default @direction is not inherited across context layers — _clone_active_context omits @direction
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 683
- Forks
- 138
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 15
Description
Summary
The active-context clone used by context processing copies @base, @language, @vocab, and previousContext, but not @direction. As a result the default base direction is silently dropped whenever a new context layer is processed on top of an existing active context — a second document-level @context layer, a property-scoped context, a type-scoped context, an embedded node @context, or a remote context. The default language is inherited correctly in all of these cases; only @direction is lost.
This is the same omission as digitalbazaar/jsonld.js#586 — PyLD's _clone_active_context mirrors jsonld.js's _cloneActiveContext, so the two implementations should probably fix it together.
Reproduced on PyLD 3.3.0 (Python 3.12); _clone_active_context on current master has the same omission.
Reproduction
import json
from pyld import jsonld
# 1. Control: a single context layer sets both defaults — both apply.
print(json.dumps(jsonld.expand({
'@context': {'@language': 'en', '@direction': 'rtl'},
'http://ex/p': 'v'
})))
# 2. A second document-level context layer: @language survives, @direction is lost.
print(json.dumps(jsonld.expand({
'@context': [{'@language': 'en', '@direction': 'rtl'}, {'dummy': 'http://ex/d'}],
'http://ex/p': 'v'
})))
# 3. A property-scoped context: @language is inherited into the scope, @direction is lost.
print(json.dumps(jsonld.expand({
'@context': {
'@language': 'en',
'@direction': 'rtl',
'thing': {'@id': 'http://ex/thing', '@context': {'other': 'http://ex/other'}}
},
'thing': {'http://ex/label': 'hello'}
})))
# 4. to_rdf with rdfDirection: the divergence reaches N-Quads.
print(jsonld.to_rdf({
'@context': {
'@language': 'en',
'@direction': 'rtl',
'thing': {'@id': 'http://ex/thing', '@context': {'other': 'http://ex/other'}}
},
'@id': 'http://ex/x',
'http://ex/out': 'outside',
'thing': {'http://ex/label': 'hello'}
}, {'format': 'application/n-quads', 'rdfDirection': 'i18n-datatype'}))
Actual output (PyLD 3.3.0):
[{"http://ex/p": [{"@language": "en", "@direction": "rtl", "@value": "v"}]}]
[{"http://ex/p": [{"@language": "en", "@value": "v"}]}]
[{"http://ex/thing": [{"http://ex/label": [{"@language": "en", "@value": "hello"}]}]}]
<http://ex/x> <http://ex/out> "outside"^^<https://www.w3.org/ns/i18n#en_rtl> .
<http://ex/x> <http://ex/thing> _:b0 .
_:b0 <http://ex/label> "hello"@en .
Expected (per spec): case 2 keeps "@direction": "rtl"; case 3's hello carries "@direction": "rtl"; case 4 emits "hello"^^<https://www.w3.org/ns/i18n#en_rtl> for the in-scope literal. Note case 4: within one document, the top-level string gets the i18n datatype while the string inside the scope silently degrades to a plain language-tagged literal.
(jsonld.js 9.0.0 produces byte-identical output for all four cases — the two implementations share the bug.)
Root cause
_clone_active_context in lib/pyld/jsonld.py:
def _clone_active_context(self, active_ctx):
"""
Clones an active context, creating a child active context.
:param active_ctx: the active context to clone.
:return: a clone (child) of the active context.
"""
child = {'mappings': dict(active_ctx['mappings'])}
if '@base' in active_ctx:
child['@base'] = active_ctx['@base']
if 'previousContext' in active_ctx:
child['previousContext'] = active_ctx['previousContext']
if '@language' in active_ctx:
child['@language'] = active_ctx['@language']
if '@vocab' in active_ctx:
child['@vocab'] = active_ctx['@vocab']
return child
@direction is never copied. Context processing starts each layer with rval = self._clone_active_context(rval), so any layer processed after the one that set @direction loses it (unless that layer restates it — explicitly setting @direction works, because the '@direction' in ctx handler assigns onto the clone; only inheritance is broken).
Spec reference
JSON-LD 1.1 API, Context Processing: the active context "consists of: the active term definitions …, the current base IRI, an inverse context, an optional vocabulary mapping, an optional default language, an optional default base direction, and an optional previous context", and step 1 of the algorithm is "Initialize result to the result of cloning active context, with inverse context set to null." Step 5.8 then only modifies the default base direction "If context has a @direction entry" — an absent entry leaves the inherited value in place.
A two-line fix in _clone_active_context appears sufficient:
if '@direction' in active_ctx:
child['@direction'] = active_ctx['@direction']
Notes
- Same bug filed against jsonld.js: digitalbazaar/jsonld.js#586 (with the equivalent one-line fix in
_cloneActiveContext). Fixing one without the other would make the two implementations diverge from each other. - Implementations already diverge on this: Titanium JSON-LD (Java) copies both
defaultLanguageanddefaultBaseDirectionin itsActiveContextcopy constructor, so it inherits@directionper spec and produces different expansion/N-Quads output for the documents above. - The W3C test suite doesn't cover this — there is no json-ld-api fixture combining a default
@directionwith a subsequent context layer or scoped context, which is presumably why it has gone unnoticed in both implementations. - Compatibility note: fixing this changes canonical N-Quads (and therefore RDFC hashes / proof values) for any signed document that combines a default
@directionwith multiple context layers or scoped contexts underrdfDirection: 'i18n-datatype'. Related standards discussion on@direction/rdfDirectiondivergence in Data Integrity: w3c/vc-data-integrity#366.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in lib/pyld/jsonld.py at _clone_active_context and compare the fields it copies with the active-context fields described in the issue. Reproduce the multi-layer and scoped-context examples with jsonld.expand and jsonld.to_rdf, then add regression coverage so inherited @direction remains present and the expected i18n-datatype output is preserved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- data
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100