[Python-modules-commits] [python-pyld] 04/05: Implement new experimental embed API.
Wolfgang Borgert
debacle at moszumanska.debian.org
Tue Oct 13 22:56:25 UTC 2015
This is an automated email from the git hooks/post-receive script.
debacle pushed a commit to tag 0.6.6
in repository python-pyld.
commit c47503827d4f3446dbd9d789de4c7176931779ef
Author: Dave Longley <dlongley at digitalbazaar.com>
Date: Thu Dec 4 16:15:29 2014 -0500
Implement new experimental embed API.
- See: https://github.com/json-ld/json-ld.org/issues/377
---
lib/pyld/jsonld.py | 396 +++++++++++++++++++++++++++++++----------------------
1 file changed, 233 insertions(+), 163 deletions(-)
diff --git a/lib/pyld/jsonld.py b/lib/pyld/jsonld.py
index bae5081..a1db010 100644
--- a/lib/pyld/jsonld.py
+++ b/lib/pyld/jsonld.py
@@ -17,7 +17,7 @@ __license__ = 'New BSD license'
__version__ = '0.6.6-dev'
__all__ = [
- 'compact', 'expand', 'flatten', 'frame', 'from_rdf', 'to_rdf',
+ 'compact', 'expand', 'flatten', 'frame', 'link', 'from_rdf', 'to_rdf',
'normalize', 'set_document_loader', 'get_document_loader',
'parse_link_header', 'load_document',
'register_rdf_parser', 'unregister_rdf_parser',
@@ -209,6 +209,31 @@ def frame(input_, frame, options=None):
return JsonLdProcessor().frame(input_, frame, options)
+def link(input_, ctx, options=None):
+ """
+ **Experimental**
+
+ Links a JSON-LD document's nodes in memory.
+
+ :param input_: the JSON-LD document to link.
+ :param ctx: the JSON-LD context to apply or None.
+ :param [options]: the options to use.
+ [base] the base IRI to use.
+ [expandContext] a context to expand with.
+ [documentLoader(url)] the document loader
+ (default: _default_document_loader).
+
+ :return: the linked JSON-LD output.
+ """
+ # API matches running frame with a wildcard frame and embed: '@link'
+ # get arguments
+ frame = {'@embed': '@link'}
+ if ctx:
+ frame['@context'] = ctx
+ frame['@embed'] = '@link'
+ return frame(input, frame, options)
+
+
def normalize(input_, options=None):
"""
Performs JSON-LD normalization.
@@ -627,6 +652,11 @@ class JsonLdProcessor(object):
options.setdefault('skipExpansion', False)
options.setdefault('activeCtx', False)
options.setdefault('documentLoader', _default_document_loader)
+ options.setdefault('link', False);
+ if options['link']:
+ # force skip expansion when linking, "link" is not part of the
+ # public API, it should only be called from framing
+ options['skipExpansion'] = True
if options['skipExpansion']:
expanded = input_
@@ -853,7 +883,8 @@ class JsonLdProcessor(object):
:param options: the options to use.
[base] the base IRI to use.
[expandContext] a context to expand with.
- [embed] default @embed flag (default: True).
+ [embed] default @embed flag: '@last', '@always', '@never', '@link'
+ (default: '@last').
[explicit] default @explicit flag (default: False).
[requireAll] default @requireAll flag (default: True).
[omitDefault] default @omitDefault flag (default: False).
@@ -866,7 +897,7 @@ class JsonLdProcessor(object):
options = options or {}
options.setdefault('base', input_ if _is_string(input_) else '')
options.setdefault('compactArrays', True)
- options.setdefault('embed', True)
+ options.setdefault('embed', '@last')
options.setdefault('explicit', False)
options.setdefault('requireAll', True)
options.setdefault('omitDefault', False)
@@ -930,9 +961,11 @@ class JsonLdProcessor(object):
framed = self._frame(expanded, expanded_frame, options)
try:
- # compact result (force @graph option to True)
+ # compact result (force @graph option to True, skip expansion,
+ # check for linked embeds)
options['graph'] = True
options['skipExpansion'] = True
+ options['link'] = {}
options['activeCtx'] = True
result = self.compact(framed, ctx, options)
except JsonLdError as cause:
@@ -1637,16 +1670,35 @@ class JsonLdProcessor(object):
# recursively compact object
if _is_object(element):
+ if(options['link'] and '@id' in element and
+ element['@id'] in options['link']):
+ # check for a linked element to reuse
+ linked = options['link'][element['@id']]
+ for link in linked:
+ if link['expanded'] == element:
+ return link['compacted']
+
# do value compaction on @values and subject references
if _is_value(element) or _is_subject_reference(element):
- return self._compact_value(
+ rval = self._compact_value(
active_ctx, active_property, element)
+ if options['link'] and _is_subject_reference(element):
+ # store linked element
+ options['link'].setdefault(element['@id'], []).append(
+ {'expanded': element, 'compacted': rval})
+ return rval
# FIXME: avoid misuse of active property as an expanded property?
inside_reverse = (active_property == '@reverse')
- # recursively process element keys in order
rval = {}
+
+ if options['link'] and '@id' in element:
+ # store linked element
+ options['link'].setdefault(element['@id'], []).append(
+ {'expanded': element, 'compacted': rval})
+
+ # recursively process element keys in order
for expanded_property, expanded_value in sorted(element.items()):
# compact @id and @type(s)
if expanded_property == '@id' or expanded_property == '@type':
@@ -2195,7 +2247,9 @@ class JsonLdProcessor(object):
# create framing state
state = {
'options': options,
- 'graphs': {'@default': {}, '@merged': {}}
+ 'graphs': {'@default': {}, '@merged': {}},
+ 'subjectStack': [],
+ 'link': {}
}
# produce a map of all graphs and name each bnode
@@ -3021,18 +3075,15 @@ class JsonLdProcessor(object):
:param property: the parent property, initialized to None.
"""
# validate the frame
- self._validate_frame(state, frame)
+ self._validate_frame(frame)
frame = frame[0]
# get flags for current frame
options = state['options']
- embed_on = self._get_frame_flag(frame, options, 'embed')
- explicit_on = self._get_frame_flag(frame, options, 'explicit')
- require_all_on = self._get_frame_flag(frame, options, 'requireAll')
flags = {
- 'embed': embed_on,
- 'explicit': explicit_on,
- 'requireAll': require_all_on
+ 'embed': self._get_frame_flag(frame, options, 'embed'),
+ 'explicit': self._get_frame_flag(frame, options, 'explicit'),
+ 'requireAll': self._get_frame_flag(frame, options, 'requireAll')
}
# filter out subjects that match the frame
@@ -3040,118 +3091,154 @@ class JsonLdProcessor(object):
# add matches to output
for id_, subject in sorted(matches.items()):
+ if flags['embed'] == '@link' and id_ in state['link']:
+ # TODO: may want to also match an existing linked subject
+ # against the current frame ... so different frames could
+ # produce different subjects that are only shared in-memory
+ # when the frames are the same
+
+ # add existing linked subject
+ self._add_frame_output(parent, property, state['link'][id_])
+ continue
+
# Note: In order to treat each top-level match as a
- # compartmentalized result, create an independent copy of the
- # embedded subjects map when the property is None, which only
- # occurs at the top-level.
+ # compartmentalized result, clear the unique embedded subjects map
+ # when the property is None, which only occurs at the top-level.
if property is None:
- state['embeds'] = {}
+ state['uniqueEmbeds'] = {}
- # start output
+ # start output for subject
output = {'@id': id_}
+ state['link'][id_] = output
+
+ # if embed is @never or if a circular reference would be created
+ # by an embed, the subject cannot be embedded, just add the
+ # reference; note that a circular reference won't occur when the
+ # embed flag is `@link` as the above check will short-circuit
+ # before reaching this point
+ if(flags['embed'] == '@never' or self._creates_circular_reference(
+ subject, state['subjectStack'])):
+ self._add_frame_output(parent, property, output)
+ continue
- # prepare embed meta info
- embed = {'parent': parent, 'property': property}
-
- # if embed is on and there is an existing embed
- if embed_on and id_ in state['embeds']:
- # only overwrite an existing embed if it has already been
- # added to its parent -- otherwise its parent is somewhere up
- # the tree from this embed and the embed would occur twice
- # once the tree is added
- embed_on = False
-
- # existing embed's parent is an array
- existing = state['embeds'][id_]
- if _is_array(existing['parent']):
- for p in existing['parent']:
- if JsonLdProcessor.compare_values(output, p):
- embed_on = True
- break
- # existing embed's parent is an object
- elif JsonLdProcessor.has_value(
- existing['parent'], existing['property'], output):
- embed_on = True
-
- # existing embed has already been added, so allow an overwrite
- if embed_on:
+ # if only the last match should be embedded
+ if flags['embed'] == '@last':
+ # remove any existing embed
+ if id_ in state['uniqueEmbeds']:
self._remove_embed(state, id_)
+ state['uniqueEmbeds'][id_] = {
+ 'parent': parent,
+ 'property': property
+ }
- # not embedding, add output without any other properties
- if not embed_on:
- self._add_frame_output(state, parent, property, output)
- else:
- # add embed meta info
- state['embeds'][id_] = embed
-
- # iterate over subject properties in order
- for prop, objects in sorted(subject.items()):
- # copy keywords to output
- if _is_keyword(prop):
- output[prop] = copy.deepcopy(subject[prop])
- continue
+ # push matching subject onto stack to enable circular embed checks
+ state['subjectStack'].append(subject)
- # if property isn't in the frame
- if prop not in frame:
- # if explicit is off, embed values
- if not explicit_on:
- self._embed_values(state, subject, prop, output)
- continue
+ # iterate over subject properties in order
+ for prop, objects in sorted(subject.items()):
+ # copy keywords to output
+ if _is_keyword(prop):
+ output[prop] = copy.deepcopy(subject[prop])
+ continue
+
+ # explicit is on and property isn't in frame, skip processing
+ if flags['explicit'] and prop not in frame:
+ continue
- # add objects
- objects = subject[prop]
- for o in objects:
- # recurse into list
- if _is_list(o):
- # add empty list
- list_ = {'@list': []}
- self._add_frame_output(state, output, prop, list_)
-
- # add list objects
- src = o['@list']
- for o in src:
+ # add objects
+ objects = subject[prop]
+ for o in objects:
+ # recurse into list
+ if _is_list(o):
+ # add empty list
+ list_ = {'@list': []}
+ self._add_frame_output(output, prop, list_)
+
+ # add list objects
+ src = o['@list']
+ for o in src:
+ if _is_subject_reference(o):
# recurse into subject reference
- if _is_subject_reference(o):
- self._match_frame(
- state, [o['@id']],
- frame[prop][0]['@list'],
- list_, '@list')
- # include other values automatically
+ if prop in frame:
+ subframe = frame[prop][0]['@list']
else:
- self._add_frame_output(
- state, list_, '@list',
- copy.deepcopy(o))
- continue
+ subframe = self._create_implicit_frame(
+ flags)
+ self._match_frame(
+ state, [o['@id']],
+ subframe, list_, '@list')
+ else:
+ # include other values automatically
+ self._add_frame_output(
+ list_, '@list', copy.deepcopy(o))
+ continue
+ if _is_subject_reference(o):
# recurse into subject reference
- if _is_subject_reference(o):
- self._match_frame(
- state, [o['@id']], frame[prop], output, prop)
- # include other values automatically
+ if prop in frame:
+ subframe = frame[prop]
else:
- self._add_frame_output(
- state, output, prop, copy.deepcopy(o))
+ subframe = self._create_implicit_frame(flags)
+ self._match_frame(
+ state, [o['@id']], subframe, output, prop)
+ else:
+ # include other values automatically
+ self._add_frame_output(output, prop, copy.deepcopy(o))
- # handle defaults in order
- for prop in sorted(frame.keys()):
- # skip keywords
- if _is_keyword(prop):
- continue
- # if omit default is off, then include default values for
- # properties that appear in the next frame but are not in
- # the matching subject
- next = frame[prop][0]
- omit_default_on = self._get_frame_flag(
- next, options, 'omitDefault')
- if not omit_default_on and prop not in output:
- preserve = '@null'
- if '@default' in next:
- preserve = copy.deepcopy(next['@default'])
- preserve = JsonLdProcessor.arrayify(preserve)
- output[prop] = [{'@preserve': preserve}]
-
- # add output to parent
- self._add_frame_output(state, parent, property, output)
+ # handle defaults in order
+ for prop in sorted(frame.keys()):
+ # skip keywords
+ if _is_keyword(prop):
+ continue
+ # if omit default is off, then include default values for
+ # properties that appear in the next frame but are not in
+ # the matching subject
+ next = frame[prop][0]
+ omit_default_on = self._get_frame_flag(
+ next, options, 'omitDefault')
+ if not omit_default_on and prop not in output:
+ preserve = '@null'
+ if '@default' in next:
+ preserve = copy.deepcopy(next['@default'])
+ preserve = JsonLdProcessor.arrayify(preserve)
+ output[prop] = [{'@preserve': preserve}]
+
+ # add output to parent
+ self._add_frame_output(parent, property, output)
+
+ # pop matching subject from circular ref-checking stack
+ state['subjectStack'].pop()
+
+ def _create_implicit_frame(self, flags):
+ """
+ Creates an implicit frame when recursing through subject matches. If
+ a frame doesn't have an explicit frame for a particular property, then
+ a wildcard child frame will be created that uses the same flags that
+ the parent frame used.
+
+ :param flags: the current framing flags.
+
+ :return: the implicit frame.
+ """
+ frame = {}
+ for key in flags:
+ frame['@' + key] = [flags[key]]
+ return [frame]
+
+ def _creates_circular_reference(self, subject_to_embed, subject_stack):
+ """
+ Checks the current subject stack to see if embedding the given subject
+ would cause a circular reference.
+
+ :param subject_to_embed: the subject to embed.
+ :param subject_stack: the current stack of subjects.
+
+ :return: true if a circular reference would be created, false if not.
+ """
+ for subject in reversed(subject_stack[:-1]):
+ if subject['@id'] == subject_to_embed['@id']:
+ return True
+ return False
def _get_frame_flag(self, frame, options, name):
"""
@@ -3163,14 +3250,25 @@ class JsonLdProcessor(object):
:return: the flag value.
"""
- return frame.get('@' + name, [options[name]])[0]
+ rval = frame.get('@' + name, [options[name]])[0]
+ if name == 'embed':
+ # default is "@last"
+ # backwards-compatibility support for "embed" maps:
+ # true => "@last"
+ # false => "@never"
+ if rval is True:
+ rval = '@last'
+ elif rval is False:
+ rval = '@never'
+ elif rval != '@always' and rval != '@never' and rval != '@link':
+ rval = '@last'
+ return rval
- def _validate_frame(self, state, frame):
+ def _validate_frame(self, frame):
"""
Validates a JSON-LD frame, throwing an exception if the frame is
invalid.
- :param state: the current frame state.
:param frame: the frame to validate.
"""
if (not _is_array(frame) or len(frame) != 1 or
@@ -3240,7 +3338,7 @@ class JsonLdProcessor(object):
wildcard = False
if k in subject:
- # v === [] means do not match if property is present
+ # v == [] means do not match if property is present
if _is_array(v) and len(v) == 0:
return False
@@ -3257,50 +3355,6 @@ class JsonLdProcessor(object):
# return true if wildcard or subject matches some properties
return wildcard or matches_some
- def _embed_values(self, state, subject, property, output):
- """
- Embeds values for the given subject and property into the given output
- during the framing algorithm.
-
- :param state: the current framing state.
- :param subject: the subject.
- :param property: the property.
- :param output: the output.
- """
- # embed subject properties in output
- objects = subject[property]
- for o in objects:
- # recurse into @list
- if _is_list(o):
- list_ = {'@list': []}
- self._add_frame_output(state, output, property, list_)
- self._embed_values(state, o, '@list', list_['@list'])
- return
-
- # handle subject reference
- if _is_subject_reference(o):
- id_ = o['@id']
-
- # embed full subject if isn't already embedded
- if id_ not in state['embeds']:
- # add embed
- embed = {'parent': output, 'property': property}
- state['embeds'][id_] = embed
- # recurse into subject
- o = {}
- s = state['subjects'][id_]
- for prop, v in s.items():
- # copy keywords
- if _is_keyword(prop):
- o[prop] = copy.deepcopy(v)
- continue
- self._embed_values(state, s, prop, o)
- self._add_frame_output(state, output, property, o)
- # copy non-subject value
- else:
- self._add_frame_output(
- state, output, property, copy.deepcopy(o))
-
def _remove_embed(self, state, id_):
"""
Removes an existing embed.
@@ -3309,7 +3363,7 @@ class JsonLdProcessor(object):
:param id_: the @id of the embed to remove.
"""
# get existing embed
- embeds = state['embeds']
+ embeds = state['uniqueEmbeds']
embed = embeds[id_]
property = embed['property']
@@ -3319,9 +3373,10 @@ class JsonLdProcessor(object):
# remove existing embed
if _is_array(embed['parent']):
# replace subject with reference
- for i, parent in embed['parent']:
+ for i, parent in enumerate(embed['parent']):
if JsonLdProcessor.compare_values(parent, subject):
embed['parent'][i] = subject
+ foo = True
break
else:
# replace subject with reference
@@ -3346,11 +3401,10 @@ class JsonLdProcessor(object):
remove_dependents(next)
remove_dependents(id_)
- def _add_frame_output(self, state, parent, property, output):
+ def _add_frame_output(self, parent, property, output):
"""
Adds framing output to the given parent.
- :param state: the current framing state.
:param parent: the parent to add to.
:param property: the parent property.
:param output: the output to add.
@@ -3398,6 +3452,22 @@ class JsonLdProcessor(object):
ctx, input_['@list'], options)
return input_
+ # handle in-memory linked nodes
+ id_alias = self._compact_iri(ctx, '@id')
+ if id_alias in input_:
+ id_ = input_[id_alias]
+ if id_ in options['link']:
+ try:
+ idx = options['link'][id_].index(input_)
+ # already visited
+ return options['link'][id_][idx]
+ except:
+ # prevent circular visitation
+ options['link'][id_].append(input_)
+ else:
+ # prevent circular visitation
+ options['link'][id_] = [input_]
+
# recurse through properties
for prop, v in input_.items():
result = self._remove_preserve(ctx, v, options)
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-pyld.git
More information about the Python-modules-commits
mailing list