from __future__ import print_function
from types import ModuleType
import logging
import rdflib
from rdflib.term import Variable, URIRef
from rdflib.graph import ConjunctiveGraph
import wrapt
from .data import DataUser
from .context_common import CONTEXT_IMPORTS
from .context_store import ContextStore, RDFContextStore
from .contextualize import (AbstractBaseContextualizable,
Contextualizable,
ContextualizableClass,
ContextualizingProxy,
contextualize_metaclass)
from .utils import FCN
from six.moves.urllib.parse import quote
from six import text_type
import six
L = logging.getLogger(__name__)
DEFAULT_CONTEXT_KEY = 'default_context_id'
'''
Configuration file key for the URI of a default RDF graph context.
This is the URI of the default graph in a project or bundle.
'''
IMPORTS_CONTEXT_KEY = 'imports_context_id'
'''
Configuration file key for the URI of an imports RDF graph context.
The imports context holds the relationships between contexts, especially the imports
relationship
'''
class ModuleProxy(wrapt.ObjectProxy):
def __init__(self, ctx, *args, **kwargs):
super(ModuleProxy, self).__init__(*args, **kwargs)
self._self_overrides = dict()
self._self_ctx = ctx
def add_attr_override(self, name, override):
self._self_overrides[name] = override
def __getattr__(self, name):
o = self._self_overrides.get(name, None)
if o is not None:
return o
else:
o = super(ModuleProxy, self).__getattr__(name)
if isinstance(o, AbstractBaseContextualizable):
o = o.contextualize(self._self_ctx)
self._self_overrides[name] = o
return o
class ContextMeta(ContextualizableClass):
@property
def context(self):
return None
@context.setter
def context(self, v):
pass
def contextualize_class_augment(self, context):
if context is None:
return self
ctxd_meta = contextualize_metaclass(context, self)
res = ctxd_meta(self.__name__, (self,), dict(class_context=context.identifier))
res.__module__ = self.__module__
return res
class ContextualizableDataUserMixin(Contextualizable, DataUser):
@property
def conf(self):
if self.context is None:
return super(ContextualizableDataUserMixin, self).conf
else:
return self.context.conf
@conf.setter
def conf(self, conf):
super(ContextualizableDataUserMixin, self).conf = conf
[docs]class Context(six.with_metaclass(ContextMeta,
ContextualizableDataUserMixin)):
"""
A context. Analogous to an RDF context, with some special sauce
.. automethod:: __call__
.. automethod:: __bool__
"""
def __init__(self, ident=None,
imported=(),
mapper=None,
key=None,
base_namespace=None,
**kwargs):
super(Context, self).__init__(**kwargs)
if key is not None and ident is not None:
raise Exception("Only one of 'key' or 'ident' can be given to Context")
if key is not None and base_namespace is None:
raise Exception("If 'key' is given, then 'base_namespace' must also be given to Context")
if not isinstance(ident, URIRef) \
and isinstance(ident, (str, text_type)):
ident = URIRef(ident)
if not isinstance(base_namespace, rdflib.namespace.Namespace) \
and isinstance(base_namespace, (str, text_type)):
base_namespace = rdflib.namespace.Namespace(base_namespace)
if ident is None and key is not None:
ident = URIRef(base_namespace[quote(key)])
if not hasattr(self, 'identifier'):
self.identifier = ident
else:
raise Exception(self)
self._statements = []
self._imported_contexts = list(imported)
self._rdf_object = None
self._graph = None
if mapper is None:
if self.context:
mapper = self.context.mapper
self.__mapper = mapper
self.base_namespace = base_namespace
self._change_counter = 0
self._triples_saved = 0
self._stored_context = None
self._own_stored_context = None
self._stopper = None #: flag value to prevent recursion
@property
def mapper(self):
if self.__mapper is None:
from .mapper import Mapper
self.__mapper = Mapper(conf=self.conf)
return self.__mapper
[docs] def contents(self):
'''
Returns statements added to this context
Returns
-------
generator
'''
return (x for x in self._statements)
[docs] def clear(self):
'''
Clear declared statements
'''
del self._statements[:]
[docs] def add_import(self, context):
'''
Add an imported context
'''
self._imported_contexts.append(context)
[docs] def add_statement(self, stmt):
'''
Add a statement to the context. Typically, statements will be added by
`contextualizing <Contextualizable.contextualize>` a
`~owmeta_core.dataobject.DataObject` and making a statement thereon. For instance,
if a class ``A`` has a property ``p``, then for the context ``ctx``::
ctx(A)(ident='http://example.org').p('val')
would add a statement to ``ctx`` like::
(A(ident='http://example.org'), A.p.link, rdflib.term.Literal('val'))
Parameters
----------
stmt : owmeta_core.statement.Statement
Statement to add
'''
if self.identifier != stmt.context.identifier:
raise ValueError("Cannot add statements from a different context")
self._graph = None
self._statements.append(stmt)
self._change_counter += 1
[docs] def remove_statement(self, stmt):
'''
Remove a statement from the context
Parameters
----------
stmt : tuple
Statement to remove
'''
self._graph = None
self._statements.remove(stmt)
self._change_counter += 1
@property
def imports(self):
'''
Return imports on this context
Yields
------
Context
'''
for x in self._imported_contexts:
yield x
[docs] def transitive_imports(self):
'''
Return imports on this context and on imported contexts
Yields
------
Context
'''
for x in self._imported_contexts:
yield x
for y in x.transitive_imports():
yield y
[docs] def save_imports(self, context=None, *args, transitive=True, **kwargs):
'''
Add the `imports` on this context to a graph
Parameters
----------
context : .Context, optional
The context to add statements to. This context's configured graph will
ultimately receive the triples. By default, a context will be created with
``self.conf[IMPORTS_CONTEXT_KEY]`` as the identifier
transitive : bool, optional
If `True`, call imported imported contexts to save their imports as well
'''
if context is None:
ctx_key = self.conf[IMPORTS_CONTEXT_KEY]
imports_context = Context(ident=ctx_key, conf=self.conf)
else:
imports_context = context
self.declare_imports(imports_context, transitive)
imports_context.save_context(*args, **kwargs)
[docs] def declare_imports(self, context=None, transitive=False):
'''
Declare `imports <~context_dataobject.ContextDataObject.imports>` statements in
the given context
Parameters
----------
context : .Context, optional
The context in which to declare statements. If not provided, one will be
created with ``self.conf[IMPORTS_CONTEXT_KEY]`` as the identifier
Returns
-------
Context
The context in which the statements were declared
'''
if not context:
ctx_key = self.conf[IMPORTS_CONTEXT_KEY]
context = Context(ident=ctx_key, conf=self.conf)
self._declare_imports(context, transitive)
return context
def _declare_imports(self, context, transitive):
for ctx in self._imported_contexts:
if self.identifier is not None \
and ctx.identifier is not None \
and not isinstance(ctx.identifier, rdflib.term.BNode):
context(self.rdf_object).imports(ctx.rdf_object)
if transitive:
ctx._declare_imports(context, transitive)
[docs] def save_context(self, graph=None, inline_imports=False, autocommit=True, saved_contexts=None):
'''
Adds the staged statements in the context to a graph
Parameters
----------
graph : rdflib.graph.Graph or set, optional
the destination graph. Defaults to ``self.rdf``
inline_imports : bool, optional
if `True`, imported contexts will also be written added to the graph
autocommit : boolean, optional
if `True`, `graph.commit <rdflib.graph.Graph.commit>` is invoked after adding statements to the
graph (including any imported contexts if `inline_imports` is `True`)
saved_contexts : set, optional
a collection of identifiers for previously saved contexts. Note that `id` is
used to get an identifier: the return value of `id` can be repeated after an
object is deleted.
'''
if saved_contexts is None:
saved_contexts = set()
if (self._change_counter, id(self)) in saved_contexts:
return
saved_contexts.add((self._change_counter, id(self)))
if graph is None:
graph = self._retrieve_configured_graph()
if autocommit and hasattr(graph, 'commit'):
graph.commit()
if inline_imports:
for ctx in self._imported_contexts:
ctx.save_context(graph, inline_imports, autocommit=False,
saved_contexts=saved_contexts)
if isinstance(graph, set):
graph.update(self._save_context_triples())
else:
ctx_graph = self.get_target_graph(graph)
ctx_graph.addN((s, p, o, ctx_graph) for s, p, o in self._save_context_triples())
if autocommit and hasattr(graph, 'commit'):
graph.commit()
save = save_context
''' Alias to save_context '''
@property
def triples_saved(self):
'''
The number of triples saved in the most recent call to `save_context`
'''
return self._triples_saved_helper()
def _triples_saved_helper(self, seen=None):
if seen is None:
seen = set()
if id(self) in seen:
return 0
seen.add(id(self))
res = self._triples_saved
for ctx in self._imported_contexts:
res += ctx._triples_saved_helper(seen)
return res
def _save_context_triples(self):
self._triples_saved = 0
for x in self._statements:
t = x.to_triple()
if not (isinstance(t[0], Variable) or
isinstance(t[2], Variable) or
isinstance(t[1], Variable)):
self._triples_saved += 1
yield t
def get_target_graph(self, graph):
res = graph
if self.identifier is not None:
if hasattr(graph, 'graph_aware') and graph.graph_aware:
res = graph.graph(self.identifier)
elif hasattr(graph, 'context_aware') and graph.context_aware:
res = graph.get_context(self.identifier)
return res
[docs] def contents_triples(self):
'''
Returns, as triples, the statements staged in this context
Yields
------
tuple
A triple of `RDFLib Identifiers <rdflib.term.Identifier>`
'''
for x in self._statements:
yield x.to_triple()
def contextualize_augment(self, context):
'''
Returns a contextualized proxy of this context
Parameters
----------
context : .Context
The context to contextualize this context with
'''
res = ContextualizingProxy(context, self)
res.add_attr_override('_stored_context', None)
res.add_attr_override('_own_stored_context', None)
return res
@property
def rdf_object(self):
'''
Returns a dataobject for this context
Returns
-------
owmeta_core.dataobject.DataObject
'''
if self._rdf_object is None:
from owmeta_core.context_dataobject import ContextDataObject
self._rdf_object = ContextDataObject.contextualize(self.context)(ident=self.identifier)
return self._rdf_object.contextualize(self.context)
[docs] def __bool__(self):
'''
Always returns `True`. Prevents a context with zero statements from testing false
since that's not typically a useful branching condition.
'''
return True
__nonzero__ = __bool__
def __len__(self):
return len(self._statements)
[docs] def __call__(self, o=None, *args, **kwargs):
"""
Contextualize an object
Parameters
----------
o : object
The object to contexualize
"""
if o is None:
if kwargs:
o = kwargs
elif args:
o = {x.__name__: x for x in [o] + list(args)}
if isinstance(o, ModuleType):
return ModuleProxy(self, o)
elif isinstance(o, dict):
return ContextContextManager(self, o)
elif isinstance(o, AbstractBaseContextualizable):
return o.contextualize(self)
else:
return o
def __str__(self):
return repr(self)
def __repr__(self):
ident = getattr(self, 'identifier', '???')
if ident is None:
identpart = ''
else:
identpart = 'ident="{}"'.format(ident)
return '{}({})'.format(FCN(type(self)), identpart)
[docs] def load_own_graph_from_configured_store(self):
'''
Create a RDFLib graph for accessing statements in this context, *excluding* imported
contexts. The "configured" graph is the one at ``self.conf['rdf.graph']``.
Returns
-------
rdflib.graph.ConjunctiveGraph
'''
res = ConjunctiveGraph(identifier=self.identifier,
store=RDFContextStore(self, include_imports=False))
res.namespace_manager = self.rdf.namespace_manager
return res
[docs] def load_graph_from_configured_store(self):
'''
Create an RDFLib graph for accessing statements in this context, *including* imported
contexts. The "configured" graph is the one at ``self.rdf``.
Returns
-------
rdflib.graph.ConjunctiveGraph
'''
res = ConjunctiveGraph(identifier=self.identifier,
store=RDFContextStore(self, imports_graph=self.imports_graph()))
res.namespace_manager = self.rdf.namespace_manager
return res
def imports_graph(self):
context_imports_graph = None
init = False
try:
if self.context is not None and self._stopper is not self:
if self._stopper is None:
init = True
self._stopper = self
context_imports_graph = self.context.imports_graph()
if context_imports_graph is not None:
return context_imports_graph
ctxid = self.conf.get(IMPORTS_CONTEXT_KEY, None)
if ctxid:
ctx_uriref = URIRef(ctxid)
graph = self.rdf.get_context(ctx_uriref)
imports_ctx = Context.contextualize(self.context)(ctx_uriref, conf=self.conf)
return ConjunctiveGraph(identifier=ctx_uriref,
store=RDFContextStore(imports_ctx, imports_graph=graph))
else:
return None
finally:
if init:
self._stopper = None
[docs] def rdf_graph(self):
'''
Return the principal graph for this context. For a regular `Context` this will be
the "staged" graph.
Returns
-------
rdflib.graph.ConjunctiveGraph
See Also
--------
staged : Has the "staged" principal graph.
mixed : Has the "mixed" principal graph.
stored : Has the "stored" graph, including imports.
own_stored : Has the "stored" graph, excluding imports.
'''
if self._graph is None:
self._graph = self.load_staged_graph()
return self._graph
[docs] def load_mixed_graph(self):
'''
Create a graph for accessing statements both staged (see `load_staged_graph`) and
stored (see `load_graph_from_configured_store`). No effort is made to either
deduplicate, smush blank nodes, or logically reconcile statements between staged
and stored graphs.
Returns
-------
rdflib.graph.ConjunctiveGraph
'''
return ConjunctiveGraph(identifier=self.identifier,
store=ContextStore(context=self, include_stored=True,
imports_graph=self.imports_graph()))
[docs] def load_staged_graph(self):
'''
Create a graph for accessing statements declared in this specific instance of
this context. This statements may not have been written to disk; therefore, they
are "staged".
Returns
-------
rdflib.graph.ConjunctiveGraph
'''
return ConjunctiveGraph(identifier=self.identifier, store=ContextStore(context=self))
@property
def mixed(self):
'''
A read-only context whose principal graph is the "mixed" graph.
Returns
-------
QueryContext
See also
--------
rdf_graph
load_mixed_graph : Defines the principal graph for this context
'''
return QueryContext.contextualize(self.context)(
mapper=self.mapper,
graph=self.load_mixed_graph(),
ident=self.identifier,
conf=self.conf)
@property
def staged(self):
'''
A read-only context whose principal graph is the "staged" graph.
Returns
-------
QueryContext
See also
--------
rdf_graph
load_staged_graph : Defines the principal graph for this context
'''
return QueryContext.contextualize(self.context)(
mapper=self.mapper,
graph=self.load_staged_graph(),
ident=self.identifier,
conf=self.conf)
@property
def stored(self):
'''
A read-only context whose principal graph is the "stored" graph, including
imported contexts.
Returns
-------
QueryContext
See also
--------
rdf_graph
load_graph_from_configured_store : Defines the principal graph for this context
'''
if self._stored_context is None:
self._stored_context = QueryContext.contextualize(self.context)(
mapper=self.mapper,
graph=self.load_graph_from_configured_store(),
ident=self.identifier,
conf=self.conf)
return self._stored_context
@property
def own_stored(self):
'''
A read-only context whose principal graph is the "stored" graph, excluding
imported contexts.
Returns
-------
QueryContext
See also
--------
rdf_graph
load_own_graph_from_configured_store : Defines the principal graph for this context
'''
if self._own_stored_context is None:
self._own_stored_context = QueryContext.contextualize(self.context)(
mapper=self.mapper,
graph=self.load_own_graph_from_configured_store(),
ident=self.identifier,
conf=self.conf)
return self._own_stored_context
def _retrieve_configured_graph(self):
return self.rdf
def resolve_class(self, uri):
if self.mapper is None:
return None
return self.mapper.resolve_class(uri, self)
[docs]class QueryContext(Context):
'''
A read-only context.
'''
def __init__(self, graph, *args, **kwargs):
super(QueryContext, self).__init__(*args, **kwargs)
self.__graph = graph
def rdf_graph(self, *args, **kwargs):
return self.__graph
@property
def imports(self):
imports_graph = self.imports_graph()
if imports_graph is None:
return
for t in imports_graph.triples((self.identifier, CONTEXT_IMPORTS, None)):
yield QueryContext(mapper=self.mapper,
graph=self.__graph,
ident=t[2],
conf=self.conf)
def add_import(self, *args, **kwargs): raise ContextIsReadOnly
def save_imports(self, *args, **kwargs): raise ContextIsReadOnly
def save_context(self, *args, **kwargs): raise ContextIsReadOnly
class ContextIsReadOnly(Exception):
def __init__(self):
super(ContextIsReadOnly, self).__init__('This context is read-only')
ClassContexts = dict()
class ClassContextMeta(ContextMeta):
def __call__(self, ident, base_namespace=None, imported=()):
res = ClassContexts.get(URIRef(ident))
if not res:
res = super(ClassContextMeta, self).__call__(ident=ident,
base_namespace=base_namespace, imported=imported)
ClassContexts[URIRef(ident)] = res
else:
if base_namespace or imported:
raise Exception('Arguments can only be provided to a ClassContext on'
' first creation')
return res
class ClassContext(six.with_metaclass(ClassContextMeta, Context)):
pass
[docs]class ContextContextManager(object):
""" The context manager created when Context::__call__ is passed a dict """
def __init__(self, ctx, to_import):
self._overrides = dict()
self._ctx = ctx
self._backing_dict = to_import
self.save = self._ctx.save_context
@property
def context(self):
return self._ctx
def __call__(self, o):
return self._ctx(o)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
pass
def __getattr__(self, name):
return self.lookup(name)
def __getitem__(self, key):
return self.lookup(key)
def lookup(self, key):
o = self._overrides.get(key, None)
if o is not None:
return o
o = self._backing_dict[key]
if isinstance(o, AbstractBaseContextualizable):
o = o.contextualize(self._ctx)
self._overrides[key] = o
return o