From 7c6d531cbadb35743e85b9edc7acdc8661fc056f Mon Sep 17 00:00:00 2001 From: Eirik Alme Nordstrand Date: Tue, 30 Sep 2025 11:19:07 +0200 Subject: [PATCH] Added full path to context resolution to differentiate identical contexts under different active_property --- lib/pyld/context_resolver.py | 10 ++--- lib/pyld/jsonld.py | 79 +++++++++++++++++++----------------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/lib/pyld/context_resolver.py b/lib/pyld/context_resolver.py index 6714cbb..4fd8d57 100644 --- a/lib/pyld/context_resolver.py +++ b/lib/pyld/context_resolver.py @@ -28,7 +28,7 @@ def __init__(self, shared_cache, document_loader): self.shared_cache = shared_cache self.document_loader = document_loader - def resolve(self, active_ctx, context, base, cycles=None): + def resolve(self, active_ctx, context, path, base, cycles=None): """ Resolve a context. @@ -56,7 +56,7 @@ def resolve(self, active_ctx, context, base, cycles=None): resolved = self._get(ctx) if not resolved: resolved = self._resolve_remote_context( - active_ctx, ctx, base, cycles) + active_ctx, path, ctx, base, cycles) # add to output and continue if isinstance(resolved, list): @@ -72,7 +72,7 @@ def resolve(self, active_ctx, context, base, cycles=None): code='invalid local context') else: # context is an object, get/create `ResolvedContext` for it - key = canonicalize(dict(ctx)).decode('UTF-8') + key = canonicalize([*path, dict(ctx)]).decode('UTF-8') resolved = self._get(key) if not resolved: # create a new static `ResolvedContext` and cache it @@ -102,7 +102,7 @@ def _cache_resolved_context(self, key, resolved, tag): tag_map[tag] = resolved return resolved - def _resolve_remote_context(self, active_ctx, url, base, cycles): + def _resolve_remote_context(self, active_ctx, path, url, base, cycles): # resolve relative URL and fetch context url = jsonld.prepend_base(base, url) context, remote_doc = self._fetch_context(active_ctx, url, cycles) @@ -112,7 +112,7 @@ def _resolve_remote_context(self, active_ctx, url, base, cycles): self._resolve_context_urls(context, base) # resolve, cache, and return context - resolved = self.resolve(active_ctx, context, base, cycles) + resolved = self.resolve(active_ctx, context, path, base, cycles) self._cache_resolved_context(url, resolved, remote_doc.get('tag')) return resolved diff --git a/lib/pyld/jsonld.py b/lib/pyld/jsonld.py index 5266a89..2f44c94 100644 --- a/lib/pyld/jsonld.py +++ b/lib/pyld/jsonld.py @@ -730,7 +730,7 @@ def compact(self, input_, ctx, options): 'jsonld.CompactError', cause=cause) # do compaction - compacted = self._compact(active_ctx, None, expanded, options) + compacted = self._compact(active_ctx, [], expanded, options) if (options['compactArrays'] and not options['graph'] and _is_array(compacted)): @@ -867,7 +867,7 @@ def expand(self, input_, options): active_ctx, remote_context, options) # do expansion - expanded = self._expand(active_ctx, None, document, options, + expanded = self._expand(active_ctx, [], document, options, inside_list=False) # optimize away @graph with no other properties @@ -1270,7 +1270,7 @@ def process_context(self, active_ctx, local_ctx, options): options.setdefault('contextResolver', ContextResolver(_resolved_context_cache, options['documentLoader'])) - return self._process_context(active_ctx, local_ctx, options) + return self._process_context(active_ctx, local_ctx, [], options) def register_rdf_parser(self, content_type, parser): """ @@ -1757,7 +1757,7 @@ def _compare_rdf_triples(t1, t2): return True - def _compact(self, active_ctx, active_property, element, options): + def _compact(self, active_ctx, path, element, options): """ Recursively compacts an element using the given active context. All values must be in expanded form before this method is called. @@ -1770,12 +1770,14 @@ def _compact(self, active_ctx, active_property, element, options): :return: the compacted value. """ + active_property = path[-1] if len(path) > 0 else None + # recursively compact array if _is_array(element): rval = [] for e in element: # compact, dropping any None values - e = self._compact(active_ctx, active_property, e, options) + e = self._compact(active_ctx, path, e, options) if e is not None: rval.append(e) if options['compactArrays'] and len(rval) == 1: @@ -1792,7 +1794,7 @@ def _compact(self, active_ctx, active_property, element, options): active_ctx, active_property, '@context') if ctx is not None: active_ctx = self._process_context( - active_ctx, ctx, options, + active_ctx, ctx, path, options, propagate=True, override_protected=True) @@ -1823,7 +1825,7 @@ def _compact(self, active_ctx, active_property, element, options): JsonLdProcessor.get_context_value( active_ctx, active_property, '@container')) if '@list' in container: - return self._compact(active_ctx, active_property, element['@list'], options) + return self._compact(active_ctx, path, element['@list'], options) # FIXME: avoid misuse of active property as an expanded property? inside_reverse = (active_property == '@reverse') @@ -1842,7 +1844,7 @@ def _compact(self, active_ctx, active_property, element, options): input_ctx, active_property, '@context') if property_scoped_ctx is not None: active_ctx = self._process_context( - active_ctx, property_scoped_ctx, options, + active_ctx, property_scoped_ctx, path, options, propagate=True, override_protected=True) @@ -1866,7 +1868,7 @@ def _compact(self, active_ctx, active_property, element, options): input_ctx, compacted_type, '@context') if ctx is not None: active_ctx = self._process_context( - active_ctx, ctx, options, + active_ctx, ctx, [*path, compacted_type], options, propagate=False) # recursively process element keys in order @@ -1915,7 +1917,7 @@ def _compact(self, active_ctx, active_property, element, options): if expanded_property == '@reverse': # recursively compact expanded value compacted_value = self._compact( - active_ctx, '@reverse', expanded_value, options) + active_ctx, [*path, '@reverse'], expanded_value, options) # handle double-reversed properties for compacted_property, value in \ @@ -1945,7 +1947,7 @@ def _compact(self, active_ctx, active_property, element, options): if expanded_property == '@preserve': # compact using active_property compacted_value = self._compact( - active_ctx, active_property, expanded_value, options) + active_ctx, path, expanded_value, options) if not (_is_array(compacted_value) and len(compacted_value) == 0): JsonLdProcessor.add_value(rval, expanded_property, compacted_value) continue @@ -2029,7 +2031,7 @@ def _compact(self, active_ctx, active_property, element, options): # recursively compact expanded item compacted_item = self._compact( - active_ctx, item_active_property, + active_ctx, [*path, item_active_property], inner_ if (is_list or is_graph) else expanded_item, options) # handle @list @@ -2157,7 +2159,7 @@ def _compact(self, active_ctx, active_property, element, options): # whose key maps to @id, recompact without @type if len(compacted_item.keys()) == 1 and '@id' in expanded_item: compacted_item = self._compact( - active_ctx, item_active_property, + active_ctx, [*path, item_active_property], {'@id': expanded_item['@id']}, options) key = key or self._compact_iri(active_ctx, '@none') @@ -2191,7 +2193,7 @@ def _compact(self, active_ctx, active_property, element, options): return element def _expand( - self, active_ctx, active_property, element, options, + self, active_ctx, path, element, options, inside_list=False, inside_index=False, type_scoped_ctx=None): @@ -2213,6 +2215,7 @@ def _expand( :return: the expanded value. """ + active_property = path[-1] if len(path) > 0 else None # nothing to expand if element is None: return element @@ -2231,7 +2234,7 @@ def _expand( for e in element: # expand element e = self._expand( - active_ctx, active_property, e, options, + active_ctx, path, e, options, inside_list=inside_list, inside_index=inside_index, type_scoped_ctx=type_scoped_ctx) @@ -2293,14 +2296,14 @@ def _expand( # apply property-scoped context after reverting term-scoped context if property_scoped_ctx is not None: active_ctx = self._process_context( - active_ctx, property_scoped_ctx, options, + active_ctx, property_scoped_ctx, path, options, override_protected=True) # recursively expand object # if element has a context, process it if '@context' in element: active_ctx = self._process_context( - active_ctx, element['@context'], options) + active_ctx, element['@context'], path, options) # set the type-scoped context to the context on input, for use later type_scoped_ctx = active_ctx @@ -2322,12 +2325,12 @@ def _expand( type_scoped_ctx, type_, '@context') if ctx is not None and ctx is not False: active_ctx = self._process_context( - active_ctx, ctx, options, propagate=False) + active_ctx, ctx, [*path, key], options, propagate=False) # process each key and value in element, ignoring @nest content rval = {} self._expand_object( - active_ctx, active_property, expanded_active_property, + active_ctx, path, expanded_active_property, element, rval, options, inside_list, type_key, @@ -2420,7 +2423,7 @@ def _expand( return rval def _expand_object( - self, active_ctx, active_property, expanded_active_property, + self, active_ctx, path, expanded_active_property, element, expanded_parent, options, inside_list=False, type_key=None, @@ -2438,6 +2441,7 @@ def _expand_object( :return: the expanded value. """ + active_property = path[-1] if len(path) > 0 else None nests = [] unexpanded_value = None @@ -2547,7 +2551,7 @@ def _expand_object( if (expanded_property == '@included' and self._processing_mode(active_ctx, 1.1)): included_result = JsonLdProcessor.arrayify( - self._expand(active_ctx, active_property, value, options)) + self._expand(active_ctx, path, value, options)) if not all(_is_subject(v) for v in included_result): raise JsonLdError( 'Invalid JSON-LD syntax; "values of @included ' @@ -2636,7 +2640,7 @@ def _expand_object( code='invalid @reverse value') expanded_value = self._expand( - active_ctx, '@reverse', value, options, + active_ctx, [*path, '@reverse'], value, options, inside_list=inside_list) # properties double-reversed @@ -2680,7 +2684,7 @@ def _expand_object( term_ctx = active_ctx ctx = JsonLdProcessor.get_context_value(active_ctx, key, '@context') if ctx is not None: - term_ctx = self._process_context(active_ctx, ctx, options, + term_ctx = self._process_context(active_ctx, ctx, [*path, key], options, propagate=True, override_protected=True) container = JsonLdProcessor.arrayify( @@ -2700,14 +2704,14 @@ def _expand_object( property_index = None if index_key != '@index': property_index = self._expand_iri(active_ctx, index_key, vocab=options.get('base', '')) - expanded_value = self._expand_index_map(term_ctx, key, value, index_key, as_graph, property_index, options) + expanded_value = self._expand_index_map(term_ctx, [*path, key], value, index_key, as_graph, property_index, options) elif '@id' in container and _is_object(value): as_graph = '@graph' in container - expanded_value = self._expand_index_map(term_ctx, key, value, '@id', as_graph, None, options) + expanded_value = self._expand_index_map(term_ctx, [*path, key], value, '@id', as_graph, None, options) elif '@type' in container and _is_object(value): expanded_value = self._expand_index_map( self._revert_to_previous_context(term_ctx), - key, value, '@type', False, None, options) + [*path, key], value, '@type', False, None, options) else: # recurse into @list or @set is_list = (expanded_property == '@list') @@ -2716,7 +2720,7 @@ def _expand_object( if is_list and expanded_active_property == '@graph': next_active_property = None expanded_value = self._expand( - term_ctx, next_active_property, value, options, + term_ctx, [*path, next_active_property], value, options, inside_list=is_list) elif JsonLdProcessor.get_context_value(active_ctx, key, '@type') == '@json': expanded_value = { @@ -2726,7 +2730,7 @@ def _expand_object( else: # recursively expand value w/key as new active property expanded_value = self._expand( - term_ctx, key, value, options, + term_ctx, [*path, key], value, options, inside_list=False) # drop None values if property is not @value (dropped below) @@ -2798,7 +2802,7 @@ def _expand_object( 'jsonld.SyntaxError', {'value': nv}, code='invalid @nest value') self._expand_object( - active_ctx, active_property, expanded_active_property, + active_ctx, path, expanded_active_property, nv, expanded_parent, options, inside_list=inside_list, type_key=type_key, @@ -3013,7 +3017,7 @@ def _from_rdf(self, dataset, options): return result - def _process_context(self, active_ctx, local_ctx, options, + def _process_context(self, active_ctx, local_ctx, path, options, override_protected=False, propagate=True, validate_scoped=True, @@ -3046,7 +3050,7 @@ def _process_context(self, active_ctx, local_ctx, options, return self._clone_active_context(active_ctx) # resolve contexts - resolved = options['contextResolver'].resolve(active_ctx, local_ctx, options.get('base', '')) + resolved = options['contextResolver'].resolve(active_ctx, local_ctx, path, options.get('base', '')) # override propagate if first resolved context has `@propagate` if _is_object(resolved[0].document) and isinstance(resolved[0].document.get('@propagate'), bool): @@ -3144,7 +3148,7 @@ def _process_context(self, active_ctx, local_ctx, options, if '_uuid' not in active_ctx: active_ctx['_uuid'] = str(uuid.uuid1()) resolved_import = options['contextResolver'].resolve( - active_ctx, value, options.get('base', '')) + active_ctx, value, path, options.get('base', '')) if len(resolved_import) != 1: raise JsonLdError( 'Invalid JSON-LD syntax; @import must reference a single context.', @@ -3304,7 +3308,7 @@ def _process_context(self, active_ctx, local_ctx, options, if process: try: self._process_context( - rval, key_ctx, options, + rval, key_ctx,[*path, k] ,options, override_protected=True, cycles=cycles) except Exception as cause: @@ -3385,7 +3389,7 @@ def _expand_language_map(self, active_ctx, language_map, direction): rval.append(val) return rval - def _expand_index_map(self, active_ctx, active_property, value, index_key, as_graph, property_index, options): + def _expand_index_map(self, active_ctx, path, value, index_key, as_graph, property_index, options): """ Expands in index, id or type map. @@ -3396,6 +3400,7 @@ def _expand_index_map(self, active_ctx, active_property, value, index_key, as_gr :param as_graph: contents should form a named graph :param property_index: index is a property """ + active_property = path[-1] if len(path) > 0 else None rval = [] is_type_index = index_key == '@type' for k, v in sorted(value.items()): @@ -3403,11 +3408,11 @@ def _expand_index_map(self, active_ctx, active_property, value, index_key, as_gr ctx = JsonLdProcessor.get_context_value( active_ctx, k, '@context') if ctx is not None: - active_ctx = self._process_context(active_ctx, ctx, options, + active_ctx = self._process_context(active_ctx, ctx, path, options, propagate=False) v = self._expand( - active_ctx, active_property, + active_ctx, path, JsonLdProcessor.arrayify(v), options, inside_list=False, @@ -5507,7 +5512,7 @@ def _expand_iri( # resolve against base rval = value - if base and '@base' in active_ctx: + if base != None and '@base' in active_ctx: # The None case preserves rval as potentially relative if active_ctx['@base'] is not None: rval = prepend_base(prepend_base(base, active_ctx['@base']), rval)