# -*- coding: utf-8 -*-
''' A schema defines attrinbutes used within a completion framework
Classes
=======
:class:`AttributesSchema`
-------------------------
:class:`EditableAttributes`
---------------------------
:class:`ProcessAttributes`
--------------------------
Functions
=========
:func:`set_attribute`
---------------------
'''
from __future__ import print_function
from __future__ import absolute_import
import six
from importlib import import_module
from pkgutil import iter_modules
from soma.sorted_dictionary import OrderedDict
from soma.controller import Controller
from soma.functiontools import partial, SomaPartial
import traits.api as traits
from capsul.pipeline.pipeline_nodes import ProcessNode
[docs]class AttributesSchema(object):
'''
An AttributesSchema has a name, which is used as an identifier to specify completion.
'''
# Name of the schema. Must be defined in subclasses.
schema_name = None
def __init__(self):
# Instantiate EditableAttributes classes that are defined in schema
sets = dict((k, getattr(self.__class__, k)()) for k in
dir(self.__class__)
if isinstance(getattr(self.__class__, k), type) and
issubclass(getattr(self.__class__, k), EditableAttributes))
self.attribute_sets = sets
[docs]class EditableAttributes(Controller):
''' A set of attributes (group) used to define process parameters
attributes. Attributes are traits in the EditableAttributes Controller.
'''
def __str__(self):
return '<{0}({1})>'.format(self.__class__.__name__, None)
[docs]def set_attribute(object, name, value):
'''
'''
setattr(object, name, value)
[docs]class ProcessAttributes(Controller):
'''
This is the base class for managing attributes for a process.
It stores attributes associated with a Process, for each of its parameters.
Attribute values can be accessed "globally" (for the whole process) as
ProcessAttributes instance traits.
Or each parameter attributes set may be accessed individually, using
:meth:`get_parameters_attributes()`.
To define attributes for a process, the programmer may subclass
ProcessAttributes and define some EditableAttributes in it. Each
EditableAttributes is a group of attributes (traits in the EditableAttributes instance or subclass).
A ProcessAttributes subclass should be registered to a factory to be linked
to a process name, using the `factory_id` class variable.
See Capsul :ref:`advanced usage doc <completion>` for details.
'''
def __init__(self, process, schema_dict):
super(ProcessAttributes, self).__init__()
self._process = process
self._schema_dict = schema_dict
self.editable_attributes = OrderedDict()
self.parameter_attributes = {}
def __getinitargs__(self):
# needed for self.copy()
return (self._process, self._schema_dict)
[docs] def set_parameter_attributes(self, parameter, schema, editable_attributes,
fixed_attibute_values, allow_list=True):
'''
Set attributes associated with a single process parameter.
Parameters
----------
parameter: str
process parameter name
schema: str
schema used for it (input, output, shared)
editable_attributes: str, EditableAttributes instance, or list of them
EditableAttributes or id containing attributes traits
fixed_attibute_values: dict (str/str)
values of non-editable attributes
allow_list: bool
if True (the default), it the process parameter is a list, then
attributes are transformed into lists.
'''
if parameter in self.parameter_attributes:
if schema == 'link':
return # this is just a lower priority
raise KeyError('Attributes already set for parameter %s'
% parameter)
process = self._process
if isinstance(process, ProcessNode):
process = process.process
if parameter not in process._instance_traits():
print('WARNING: parameter', parameter,
'not in process', process.name)
return
if isinstance(editable_attributes, six.string_types) \
or isinstance(editable_attributes, EditableAttributes):
editable_attributes = [editable_attributes]
parameter_editable_attributes = []
for ea in editable_attributes:
add_editable_attributes = False
if isinstance(ea, six.string_types):
key = ea
ea = self.editable_attributes.get(key)
if ea is None:
ea = getattr(self._schema_dict[schema], key)()
self.editable_attributes[key] = ea
add_editable_attributes = True
elif isinstance(ea, EditableAttributes):
key = ea
if key not in self.editable_attributes:
self.editable_attributes[key] = ea
add_editable_attributes = True
else:
raise TypeError(
'Invalid value for editable attributes: {0}'.format(ea))
parameter_editable_attributes.append(ea)
if add_editable_attributes:
is_list = allow_list \
and isinstance(process.trait(parameter).trait_type,
traits.List)
for name in list(ea.user_traits().keys()):
# don't use items() since traits may change during iter.
trait = ea.trait(name)
if is_list:
trait = traits.List(trait)
ea.remove_trait(name)
ea.add_trait(name, trait)
if name in self.user_traits():
if not is_list \
and isinstance(self.trait(name).trait_type,
traits.List):
# a process attribute trait may have been changed
# into a list. Here we assume attributes are only
# strings (at this point - it can be changed at
# higher lever afterwards), so change it back into
# a single value trait.
self.remove_trait(name)
self.add_trait(name, trait)
# else don't add it again: this way non-list versions
# of attributes get priority. If both exist, lists
# should get one single value (otherwise there is an
# ambiguity or inconsistency), so the attribute should
# not be a list.
else:
self.add_trait(name, trait)
f = SomaPartial(set_attribute, ea)
self.on_trait_change(f, name)
self.parameter_attributes[parameter] = (parameter_editable_attributes,
fixed_attibute_values)
[docs] def get_parameters_attributes(self):
''' Get attributes for each process parameter
'''
pa = {}
process = self._process
if isinstance(process, ProcessNode):
process = process.process
for parameter, trait in six.iteritems(process.user_traits()):
if trait.output:
if hasattr(process, 'id'):
process_name = process.id
else:
process_name = process.name
attributes = {
'generated_by_process': process_name,
'generated_by_parameter': parameter}
else:
attributes = {}
editable_fixed = self.parameter_attributes.get(parameter, ([], {}))
editable_attributes, fixed_attibute_values = editable_fixed
for ea in editable_attributes:
for attribute in ea.user_traits():
value = getattr(ea, attribute)
attributes[attribute] = value
attributes.update(fixed_attibute_values)
if attributes:
pa[parameter] = attributes
return pa
[docs] def copy(self, with_values=True):
''' overloads :meth:`soma.Controller.copy`
'''
other = self.__class__(self._process, self._schema_dict)
ea_map = {}
for parameter, pa in six.iteritems(self.parameter_attributes):
if parameter not in other.parameter_attributes:
# otherwise assume this has been done in a subclass constructor
eas, fa = pa
oeas = []
for ea in eas:
oea = ea_map.get(ea)
if oea is None:
oea = ea.copy()
oeas.append(oea)
other.set_parameter_attributes(parameter, '', oeas, fa,
allow_list=False)
# copy the values
if with_values:
for name in self.user_traits():
#print('copy attribs:', self, name, getattr(self, name))
#print(' to:', other.trait(name).trait_type)
#if isinstance(other.trait(name).trait_type, traits.List):
#print(' ', other.trait(name).inner_traits[0].trait_type)
#if isinstance(self.trait(name).trait_type, traits.List):
#print(' self list:', self.trait(name).inner_traits[0].trait_type)
setattr(other, name, getattr(self, name))
return other
[docs] def copy_to_single(self, with_values=True):
''' Similar to :meth:`copy`, excepts that it converts list attributes
into single values. This is useful within the completion system
infrastructure, to get from an attributes set containing lists
(process parameters which are lists), a single value allowing to
determine a single path.
This method is merely useful to the end user.
'''
other = ProcessAttributes(self._process, self._schema_dict)
ea_map = {}
for parameter, pa in six.iteritems(self.parameter_attributes):
if parameter not in other.parameter_attributes:
# otherwise assume this has been done in a subclass constructor
eas, fa = pa
oeas = []
for ea in eas:
oea = ea_map.get(ea)
if oea is None:
oea = EditableAttributes()
for name, trait in six.iteritems(ea.user_traits()):
if isinstance(trait.trait_type, traits.List):
trait = trait.inner_traits[0]
oea.add_trait(name, trait)
ea_map[ea] = oea
oeas.append(oea)
other.set_parameter_attributes(parameter, '', oeas, fa,
allow_list=False)
# copy the values
if with_values:
for name in self.user_traits():
value = getattr(self, name)
if isinstance(value, list):
if len(value) != 0:
value = value[0]
else:
value = self.trait(name).inner_traits[0].default
if value is not None:
setattr(other, name, value)
return other