'''
composer.py - feature oriented composition of python code
'''
from __future__ import absolute_import
import inspect
import importlib
import sys
from functools import wraps
from .importhooks import LazyComposerHook
from .helpers import (_delegate, _is_class_instance, _get_role_name,
_get_base_name, _get_method, _extract_classmethod, _extract_staticmethod)
[docs]def get_features_from_equation_file(filename):
'''
returns list of feature names read from equation file given
by ``filename``.
format: one feature per line; comments start with ``#``
Example::
#this is a comment
basefeature
#empty lines are ignored
myfeature
anotherfeature
'''
features = []
for line in open(filename):
line = line.split('#')[0].strip()
if line:
features.append(line)
return features
class CompositionError(Exception): pass
class Composer(object):
def _introduce(self, role, target_attrname, transformation, base):
if hasattr(base, target_attrname):
raise CompositionError(
'Cannot introduce "%s" from "%s" into "%s"!'
' Attribute exists already!' % (
target_attrname,
_get_role_name(role),
_get_base_name(base),
)
)
if callable(transformation):
evaluated_trans = transformation()
if not callable(evaluated_trans):
raise CompositionError(
'Cannot introduce "%s" from "%s" into "%s"!'
' Method Introduction is not callable!' % (
target_attrname,
_get_role_name(role),
_get_base_name(base),
)
)
setattr(base, target_attrname, _get_method(evaluated_trans, base))
else:
setattr(base, target_attrname, transformation)
def _refine(self, role, target_attrname, transformation, base):
if not hasattr(base, target_attrname):
raise CompositionError(
'Cannot refine "%s" of "%s" by "%s"!'
' Attribute does not exist in original!' % (
target_attrname,
_get_base_name(base),
_get_role_name(role),
)
)
if callable(transformation):
baseattr = getattr(base, target_attrname)
if callable(baseattr):
wrapper = self._create_refinement_wrapper(
transformation, baseattr, base, target_attrname
)
setattr(base, target_attrname, wrapper)
else:
setattr(base, target_attrname, transformation(baseattr))
else:
setattr(base, target_attrname, transformation)
def _create_refinement_wrapper(self, transformation, baseattr, base, target_attrname):
"""
applies refinement ``transformation`` to ``baseattr`` attribute of ``base``.
``baseattr`` can be any type of callable (function, method, functor)
this method handles the differences.
docstrings are also rescued from the original if the refinement
has no docstring set.
"""
#first step: extract the original
special_refinement_type=None
instance_refinement = _is_class_instance(base)
if instance_refinement:
dictelem = base.__class__.__dict__.get(target_attrname, None)
else:
dictelem = base.__dict__.get(target_attrname, None)
if isinstance(dictelem, staticmethod):
special_refinement_type = 'staticmethod'
original = _extract_staticmethod(dictelem)
elif isinstance(dictelem, classmethod):
special_refinement_type = 'classmethod'
original = _extract_classmethod(dictelem)
else:
if instance_refinement:
#methods need a delegator
original = _delegate(baseattr)
##TODO: evaluate this:
#original = base.__class__.__dict__[target_attrname]
else:
#default handling
original = baseattr
#step two: call the refinement passing it the original
#the result is the wrapper
wrapper = transformation(original)
#rescue docstring
if not wrapper.__doc__:
wrapper.__doc__ = baseattr.__doc__
#step three: make wrapper ready for injection
if special_refinement_type == 'staticmethod':
wrapper = staticmethod(wrapper)
elif special_refinement_type == 'classmethod':
wrapper = classmethod(wrapper)
if instance_refinement:
wrapper = wrapper.__get__(base, base.__class__)
return wrapper
def _apply_transformation(self, role, base, transformation, attrname):
if attrname.startswith('introduce_'):
target_attrname = attrname[len('introduce_'):]
self._introduce(role, target_attrname, transformation, base)
elif attrname.startswith('refine_'):
target_attrname = attrname[len('refine_'):]
self._refine(role, target_attrname, transformation, base)
elif attrname.startswith('child_'):
target_attrname = attrname[len('child_'):]
refinement = transformation()
self.compose(refinement, getattr(base, target_attrname))
def _compose_pair(self, role, base):
'''
composes onto base by applying the role
'''
#apply transformations in role to base
for attrname in dir(role):
transformation = getattr(role, attrname)
self._apply_transformation(role, base, transformation, attrname)
return base
def compose(self, *things):
'''
compose applies multiple fsts onto a base implementation.
Pass the base implementation as last parameter.
fsts are merged from RIGHT TO LEFT (like function application)
e.g.:
class MyFST(object):
#place introductions and refinements here
introduce_foo = 'bar'
compose(MyFST(), MyClass)
'''
if not len(things):
raise CompositionError('nothing to compose')
if len(things) == 1:
#composing one element is simple
return things[0]
else:
#recurse after applying last role to object
return self.compose(*(
list(things[:-2]) #all but the last two
#plus the composition of the last two
+ [self._compose_pair(things[-2], things[-1])]
))
def compose_later(self, *things):
"""
register list of things for composition using compose()
compose_later takes a list of fsts.
The last element specifies the base module as string
things are composed directly after the base module
is imported by application code
"""
if len(things) == 1:
return things[0]
module_name = things[-1]
if module_name in sys.modules:
raise CompositionError(
'compose_later call after module has been imported: '
+ module_name
)
LazyComposerHook.add(module_name, things[:-1], self)
def select(self, *features):
"""
selects the features given as string
e.g
passing 'hello' and 'world' will result in imports of
'hello' and 'world'. Then, if possible 'hello.feature'
and 'world.feature' are imported and select is called
in each feature module.
"""
for feature_name in features:
feature_module = importlib.import_module(feature_name)
#if available, import feature.py and select the feature
try:
feature_spec_module = importlib.import_module(
feature_name + '.feature'
)
if not hasattr(feature_spec_module, 'select'):
raise CompositionError(
'Function %s.feature.select not found!\n '
'Feature modules need to specify a function'
' select(composer).' % (
feature_name
)
)
args, varargs, keywords, defaults = inspect.getargspec(
feature_spec_module.select
)
if varargs or keywords or defaults or len(args) != 1:
raise CompositionError(
'invalid signature: %s.feature.select must '
'have the signature select(composer)' % (
feature_name
)
)
#call the feature`s select function
feature_spec_module.select(self)
except ImportError:
#Unfortunately, python makes it really hard
#to distinguish missing modules from modules
#that contain errors.
#Hacks like parsing the exception message will
#not work reliably due to import hooks and such.
#Conclusion: features must contain a feature.py for now
#reraise
raise
def select_equation(self, filename):
"""
select features from equation file
format: one feature per line; comments start with ``#``
Example::
#this is a comment
basefeature
#empty lines are ignored
myfeature
anotherfeature
"""
features = get_features_from_equation_file(filename)
self.select(*features)