# -*- coding: utf-8 -*-
"""
This module defines base constant and base container for constants. All other
stuff must be derived from them.
Each container has :attr:`~ConstantsContainer.constant_class` attribute. It
specifies class of constants which will be defined within contaner.
"""
import six
from collections import OrderedDict
[docs]class Constant(object):
"""
Base class for all constants. Can be merged into a container instance.
:ivar str name: constant's name. Is set up automatically and is equal to
the name of container's attribute
"""
# Tracks each time a Constant instance is created. Used to retain order.
_creation_counter = 0
def __init__(self):
self.name = None
self.container = None
# Increase the creation counter, and save our local copy.
self._creation_counter = Constant._creation_counter
Constant._creation_counter += 1
def _post_init(self, name, container=None):
"""
Called automatically by container after container's class construction.
"""
self.name = name
self.container = container
[docs] def to_group(self, group_class, **group_members):
"""
Convert a constant into a constants group.
:param class group_class: a class of group container which will be
created
:param group_members: unpacked dict which defines group members.
:returns: a lazy constants group which will be evaluated by container.
During group evaluation :meth:`merge_into_group` will be
called.
**Example**::
from candv import Constants, SimpleConstant
class FOO(Constants):
A = SimpleConstant()
B = SimpleConstant().to_group(
group_class=Constants,
B2=SimpleConstant(),
B0=SimpleConstant(),
B1=SimpleConstant(),
)
"""
return _LazyConstantsGroup(self, group_class, **group_members)
[docs] def merge_into_group(self, group):
"""
Called automatically by container after group construction.
.. note::
Redefine this method in all derived classes. Attach all custom
attributes and methods to the group here.
:param group: an instance of :class:`ConstantsContainer` or it's
subclass this constant will be merged into
:returns: ``None``
"""
group._creation_counter = self._creation_counter
@property
[docs] def full_name(self):
prefix = self.container.full_name if self.container else "__UNBOUND__"
return "{0}.{1}".format(prefix, self.name)
def __repr__(self):
"""
Return text identifying both which constant this is and which
collection it belongs to.
"""
return "<constant '{0}'>".format(self.full_name)
class _LazyConstantsGroup(object):
def __init__(self, constant, group_class, **group_members):
for name, obj in group_members.items():
if not isinstance(obj, (Constant, _LazyConstantsGroup)):
raise TypeError(
"\"{0}\" cannot be a member of a group. Only instances of "
"\"{1}\" or other groups can be."
.format(obj, Constant)
)
self.constant = constant
self.group_class = group_class
self.group_members = group_members
def _evaluate(self, parent, name):
full_name = "{0}.{1}".format(parent.full_name, name)
self.group_members.update({
'name': name,
'full_name': full_name,
'container': parent,
'__repr': "<constants group '{0}'>".format(full_name),
})
group = type(full_name, (self.group_class, ), self.group_members)
self.constant.merge_into_group(group)
del self.constant
del self.group_class
del self.group_members
return group
class _ConstantsContainerMeta(type):
"""
Metaclass for creating constants container classes.
"""
def __new__(self, class_name, bases, attributes):
if not 'name' in attributes:
attributes['name'] = class_name
if not 'full_name' in attributes:
attributes['full_name'] = class_name
__repr = attributes.pop(
'__repr',
"<constants container '{0}'>".format(attributes['name'])
)
cls = (
super(_ConstantsContainerMeta, self)
.__new__(self, class_name, bases, attributes)
)
cls.__repr = __repr
constant_class = getattr(cls, 'constant_class', None)
if not issubclass(constant_class, Constant):
raise TypeError(
"\"{0}\" which is used as \"constant_class\" for \"{1}\" must "
"be derived from \"{2}\"."
.format(constant_class, cls, Constant)
)
constants = []
for name, the_object in six.iteritems(attributes):
if isinstance(the_object, _LazyConstantsGroup):
group = the_object._evaluate(cls, name)
setattr(cls, name, group)
constants.append((name, group))
elif isinstance(the_object, cls.constant_class):
if the_object.container is not None:
raise ValueError(
'Cannot use "{0}" as value for the attribute '
'"{1}" for "{2}", because "{0}" already belongs '
'to "{3}".'
.format(the_object, name, cls, the_object.container)
)
the_object._post_init(name, cls)
constants.append((name, the_object))
elif isinstance(the_object, Constant):
the_object._post_init(name)
constants.sort(key=lambda x: x[1]._creation_counter)
cls._constants = OrderedDict(constants)
return cls
def __repr__(self):
return self.__repr
def __getitem__(self, name):
"""
Try to get constant by it's name.
:param str name: name of constant to search for
:returns: a constant
:rtype: an instance of :class:`Constant` or it's subclass
:raises KeyError: if constant name ``name`` is not present in
container
**Example**::
>>> from candv import Constants, SimpleConstant
>>> class FOO(Constants):
... foo = SimpleConstant()
... bar = SimpleConstant()
...
>>> FOO['foo']
<constant 'FOO.foo'>
"""
try:
return self._constants[name]
except KeyError:
raise KeyError(
"Constant \"{0}\" is not present in \"{1}\""
.format(name, self)
)
def __contains__(self, name):
return name in self._constants
def __len__(self):
return len(self._constants)
def __iter__(self):
return self.iternames()
def get(self, name, default=None):
"""
Try to get constant by it's name or fallback to default.
:param str name: name of constant to search for
:param default: an object returned by default if constant with a given
name is not present in the container
:returns: a constant or a default value
:rtype: an instance of :class:`Constant` or it's subclass, or `default`
value
**Example**::
>>> from candv import Constants, SimpleConstant
>>> class FOO(Constants):
... foo = SimpleConstant()
... bar = SimpleConstant()
...
>>> FOO.get('foo')
<constant 'FOO.foo'>
>>> FOO.get('xxx')
>>>
>>> FOO.get('xxx', default=123)
123
"""
return self[name] if name in self else default
def has_name(self, name):
"""
Check if container has a constant with a given name.
:param str name: a constant's name to check
:returns: ``True`` if given name belongs to container,
``False`` otherwise
:rtype: :class:`bool`
"""
return name in self
def names(self):
"""
List all names of constants within container.
:returns: a list of constant names in order constants were defined
:rtype: :class:`list` of strings
**Example**::
>>> from candv import Constants, SimpleConstant
>>> class FOO(Constants):
... foo = SimpleConstant()
... bar = SimpleConstant()
...
>>> FOO.names()
['foo', 'bar']
"""
return list(self.iternames())
def iternames(self):
"""
Same as :meth:`names` but returns an interator.
"""
return six.iterkeys(self._constants)
def constants(self):
"""
List all constants in container.
:returns: list of constants in order they were defined
:rtype: :class:`list`
**Example**::
>>> from candv import Constants, SimpleConstant
>>> class FOO(Constants):
... foo = SimpleConstant()
... bar = SimpleConstant()
...
>>> [x.name for x in FOO.constants()]
['foo', 'bar']
"""
return list(self.iterconstants())
def iterconstants(self):
"""
Same as :meth:`constants` but returns an interator.
"""
return six.itervalues(self._constants)
def items(self):
"""
Get list of constants with their names.
:returns: list of constants with their names in order they were
defined. Each element in list is a :class:`tuple` in format
``(name, constant)``.
:rtype: :class:`list`
**Example**::
>>> from candv import Constants, SimpleConstant
>>> class FOO(Constants):
... foo = SimpleConstant()
... bar = SimpleConstant()
...
>>> FOO.items()
[('foo', <constant 'FOO.foo'>), ('bar', <constant 'FOO.bar'>)]
"""
return list(self.iteritems())
def iteritems(self):
"""
Same as :meth:`items` but returns an interator.
"""
return six.iteritems(self._constants)
#: *New since 1.1.2.*
#:
#: Alias for :meth:`constants`.
#: Added for consistency with dictionaries. Use :class:`~candv.Values` and
#: :meth:`~candv.Values.values` if you need to have constants with real
#: values.
values = constants
#: *New since 1.1.2.*
#:
#: Alias for :meth:`iterconstants`.
#: Added for consistency with dictionaries. Use :class:`~candv.Values` and
#: :meth:`~candv.Values.itervalues` if you need to have constants with real
#: values.
itervalues = iterconstants
@six.add_metaclass(_ConstantsContainerMeta)
[docs]class ConstantsContainer(object):
"""
Base class for creating constants containers. Each constant defined within
container will remember it's creation order. See an example in
:meth:`constants`.
:cvar constant_class: stores a class of constants which can be stored by
container. This attribute **MUST** be set up manually
when you define a new container type. Otherwise
container will not be initialized. Default: ``None``
:raises TypeError: if you try to create an instance of container.
Containers are singletons and they cannot be
instantiated. Their attributes must be used directly.
"""
#: Defines a top-level class of constants which can be stored by container
constant_class = Constant
def __new__(cls):
"""
Classes representing constants containers are not intended to be
instantiated.
The class object itself is used directly.
"""
raise TypeError(
"\"{0}\" cannot be instantiated, because constant containers are "
"not designed for that."
.format(cls)
)
[docs]def with_constant_class(the_class):
"""
A mixin factory which allows to set constant class for constants container
outside container itself. This may help to create more readable container
definition, e.g.:
>>> from candv import Constants, SimpleConstant, with_constant_class
>>>
>>> class SomeConstant(SimpleConstant):
... pass
...
>>> class FOO(with_constant_class(SomeConstant), Constants):
... A = SomeConstant()
... B = SomeConstant()
...
>>> FOO.constant_class
<class '__main__.SomeConstant'>
"""
class ConstantContainerMixin(object):
constant_class = the_class
return ConstantContainerMixin