digitalbazaar / digitalbazaar/pyld

Default @direction is not inherited across context layers — _clone_active_context omits @direction

Open Beginner friendly
#337 2 comments 0 reactions 0 assignees View on GitHub

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 defaultLanguage and defaultBaseDirection in its ActiveContext copy constructor, so it inherits @direction per 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 @direction with 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 @direction with multiple context layers or scoped contexts under rdfDirection: 'i18n-datatype'. Related standards discussion on @direction/rdfDirection divergence in Data Integrity: w3c/vc-data-integrity#366.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.