from __future__ import print_function
import six
from rdflib.term import URIRef
from rdflib.namespace import Namespace
from collections import OrderedDict, defaultdict
from yarom.mapper import FCN
from .context import Context
from .dataObject import BaseDataObject
class Informational(object):
def __init__(self, name=None, display_name=None, description=None,
value=None, default_value=None, identifier=None,
property_type='DatatypeProperty', multiple=True,
property_name=None, also=()):
self.name = name
self._property_name = property_name
self.display_name = name if display_name is None else display_name
self.default_value = default_value
self.description = description
self._value = value
self.identifier = identifier
self.property_type = property_type
self.multiple = multiple
if also and not isinstance(also, (list, tuple)):
also = (also,)
self.also = also
self.default_override = None
"""
An override for the default value, typically set by setting the value
in a DataSource class dictionary
"""
self.cls = None
@property
def property_name(self):
return self.name if self._property_name is None else self._property_name
@property_name.setter
def property_name(self, v):
self._property_name = v
def copy(self):
res = type(self)()
for x in vars(self):
setattr(res, x, getattr(self, x))
return res
def __repr__(self):
return ("Informational(name='{}',"
" display_name={},"
" default_value={},"
" description={},"
" identifier={})").format(self.name,
repr(self.display_name),
repr(self.default_value),
repr(self.description),
repr(self.identifier))
[docs]class DuplicateAlsoException(Exception):
pass
[docs]class DataSourceType(type(BaseDataObject)):
"""A type for DataSources
Sets up the graph with things needed for MappedClasses
"""
def __init__(self, name, bases, dct):
self.__info_fields = []
others = []
newdct = dict()
for z in dct:
meta = dct[z]
if isinstance(meta, Informational):
meta.cls = self
meta.name = z
self.__info_fields.append(meta)
else:
others.append((z, dct[z]))
for x in bases:
if isinstance(x, DataSourceType):
self.__info_fields += [inf.copy() for inf in x.__info_fields]
for k, v in others:
for i in range(len(self.__info_fields)):
if self.__info_fields[i].name == k:
self.__info_fields[i] = self.__info_fields[i].copy()
self.__info_fields[i].default_override = v
break
else: # no 'break'
newdct[k] = v
if not getattr(self, '__doc__', None):
self.__doc__ = self._docstr()
super(DataSourceType, self).__init__(name, bases, newdct)
def _docstr(self):
s = ''
for inf in self.__info_fields:
s += '{} : :class:`{}`'.format(inf.display_name, inf.property_type) + \
('\n Attribute: `{}`'.format(inf.name if inf.property_name is None else inf.property_name)) + \
(('\n\n ' + inf.description) if inf.description else '') + \
('\n\n Default value: {}'.format(inf.default_value) if inf.default_value is not None else '') + \
'\n\n'
return s
@property
def info_fields(self):
return self.__info_fields
[docs]class DataSource(six.with_metaclass(DataSourceType, BaseDataObject)):
'''
A source for data that can get translated into PyOpenWorm objects.
The value for any field can be passed to __init__ by name. Additionally, if
the sub-class definition of a DataSource assigns a value for that field like::
class A(DataSource):
some_field = 3
that value will be used over the default value for the field, but not over
any value provided to __init__.
'''
source = Informational(display_name='Input source',
description='The data source that was translated into this one',
identifier=URIRef('http://openworm.org/schema/DataSource/source'),
property_type='ObjectProperty')
translation = Informational(display_name='Translation',
description='Information about the translation process that created this object',
identifier=URIRef('http://openworm.org/schema/DataSource/translation'),
property_type='ObjectProperty')
description = Informational(display_name='Description',
description='Free-text describing the data source')
rdf_namespace = Namespace("http://openworm.org/entities/data_sources/DataSource#")
def __init__(self, **kwargs):
self.info_fields = OrderedDict((i.name, i) for i in self.__class__.info_fields)
parent_kwargs = dict()
new_kwargs = dict()
for k, v in kwargs.items():
if k not in self.info_fields:
parent_kwargs[k] = v
else:
new_kwargs[k] = v
super(DataSource, self).__init__(**parent_kwargs)
vals = defaultdict(dict)
for n, inf in self.info_fields.items():
v = new_kwargs.get(n, None)
if v is not None:
vals[n]['i'] = v
else:
v = inf.default_value
if inf.default_override is not None:
vals[n]['e'] = inf.default_override
vals[n]['d'] = inf.default_value
for also in inf.also:
if v is not None and vals[also.name].setdefault('a', v) != v:
raise DuplicateAlsoException('Only one also is allowed')
for n, vl in vals.items():
inf = self.info_fields[n]
v = vl.get('i', vl.get('e', vl.get('a', vl['d'])))
# Make the POW property
# To break it down, we set the name to the info name since that's
# how we access the info on this object, but the inf.property_name
# (which may or may not be the same as the name) also gets the POW
# property by the normal semantics of DataObject.
#
# Kludge? Elegant solution to a stupid problem? You decide!
setattr(self,
inf.name,
getattr(inf.cls, inf.property_type)(owner=self,
linkName=inf.property_name,
multiple=True))
ctxd_prop = getattr(self, inf.name).contextualize(self.context)
if v is not None:
ctxd_prop(v)
[docs] def defined_augment(self):
return self.translation.has_defined_value()
[docs] def identifier_augment(self):
return self.make_identifier(self.translation.defined_values[0].identifier.n3())
def __str__(self):
try:
sio = six.StringIO()
print(self.__class__.__name__, file=sio)
for info in self.info_fields.values():
print(' ' + info.display_name, end=': ', file=sio)
for val in getattr(self, info.name).defined_values:
val_line_sep = '\n ' + ' ' * len(info.display_name)
print(val_line_sep.join(str(val).split('\n')), end=' ', file=sio)
print(file=sio)
return sio.getvalue()
except AttributeError:
return super(DataSource, self).__str__()
[docs]class Translation(BaseDataObject):
"""
Representation of the method by which a DataSource was translated and
the sources of that translation. Unlike the 'source' field attached to
DataSources, the Translation may distinguish different kinds of input
source to a translation.
"""
def __init__(self, translator=None, **kwargs):
super(Translation, self).__init__(**kwargs)
Translation.ObjectProperty('translator', owner=self)
if translator is not None:
self.translator(translator)
[docs] def defined_augment(self):
return self.translator.has_defined_value() and self.translator.onedef().defined
[docs] def identifier_augment(self):
return self.make_identifier(self.translator.onedef().identifier.n3())
[docs]class GenericTranslation(Translation):
"""
A generic translation that just has sources in order
"""
def __init__(self, source=None, **kwargs):
super(GenericTranslation, self).__init__(**kwargs)
self.source = GenericTranslation.ObjectProperty(multiple=True)
if source is not None:
self.source(source)
[docs] def defined_augment(self):
return super(GenericTranslation, self).defined_augment() and \
self.source.has_defined_value()
[docs] def identifier_augment(self):
data = super(GenericTranslation, self).identifier_augment().n3() + \
"".join(x.identifier.n3() for x in self.source.defined_values)
return self.make_identifier(data)
[docs]class DataObjectContextDataSource(DataSource):
def __init__(self, context, **kwargs):
super(DataObjectContextDataSource, self).__init__(**kwargs)
if context is not None:
self.context = context
else:
self.context = Context()
def format_types(typ):
if isinstance(typ, type):
return ':class:`{}`'.format(FCN(typ))
else:
return ', '.join(':class:`{}`'.format(FCN(x)) for x in typ)
[docs]class DataTransatorType(type(BaseDataObject)):
def __init__(self, name, bases, dct):
super(DataTransatorType, self).__init__(name, bases, dct)
if not getattr(self, '__doc__', None):
self.__doc__ = '''Input type(s): {}\n
Output type(s): {}\n
URI: {}'''.format(format_types(self.input_type),
format_types(self.output_type),
self.translator_identifier)
[docs]class BaseDataTranslator(six.with_metaclass(DataTransatorType, BaseDataObject)):
""" Translates from a data source to PyOpenWorm objects """
input_type = DataSource
output_type = DataSource
translator_identifier = None
translation_type = Translation
def __init__(self):
if self.translator_identifier is not None:
super(BaseDataTranslator, self).__init__(ident=self.translator_identifier)
else:
super(BaseDataTranslator, self).__init__()
[docs] def get_data_objects(self, data_source):
""" Override this to change how data objects are generated """
if not isinstance(data_source, self.input_type):
return set([])
else:
return self.translate(data_source)
def __call__(self, *args, **kwargs):
self.output_key = kwargs.pop('output_key', None)
try:
return self.translate(*args, **kwargs)
finally:
self.output_key = None
[docs] def translate(self, *args, **kwargs):
'''
Notionally, this method takes a data source, which is translated into
some other data source. There doesn't necessarily need to be an input
data source.
'''
raise NotImplementedError
[docs] def make_translation(self, sources=()):
'''
It's intended that implementations of DataTranslator will override this
method to make custom Translations according with how different
arguments to Translate are (or are not) distinguished.
The actual properties of a Translation subclass must be defined within
the 'translate' method
'''
return self.translation_type.contextualize(self.context)(translator=self)
def make_new_output(self, sources, *args, **kwargs):
trans = self.make_translation(sources)
res = self.output_type.contextualize(self.context)(*args, translation=trans,
key=self.output_key, **kwargs)
for s in sources:
res.contextualize(self.context).source(s)
return res
[docs]class DataTranslator(BaseDataTranslator):
"""
A specialization with the GenericTranslation translation type that adds
sources for the translation automatically when a new output is made
"""
translation_type = GenericTranslation
[docs] def make_translation(self, sources=()):
res = super(DataTranslator, self).make_translation(sources)
for s in sources:
res.source(s)
return res
[docs]class PersonDataTranslator(BaseDataTranslator):
""" A person who was responsible for carrying out the translation of a data source """
def __init__(self, person):
"""
Parameters
----------
person : PyOpenWorm.dataObject.DataObject
The person responsible for carrying out the translation.
"""
self.person = person
# No translate impl is provided here since this is intended purely as a descriptive object
__yarom_mapped_classes__ = (Translation, DataSource, DataTranslator,
BaseDataTranslator, GenericTranslation, PersonDataTranslator)