"""
Defines base constant and base container for constants.
"""
import types
from collections import OrderedDict as odict
from .exceptions import CandvConstantAlreadyBoundError
from .exceptions import CandvContainerMisusedError
from .exceptions import CandvInvalidConstantClass
from .exceptions import CandvInvalidGroupMemberError
from .exceptions import CandvMissingConstantError
from ._utils import export
UNBOUND_CONSTANT_CONTAINER_NAME = "__UNBOUND__"
[docs]@export
class SimpleConstant:
"""
Base class for all constants.
:ivar str name:
constant's name: set up automatically and is equal to the name of the
container's attribute
"""
def __init__(self):
self.name = None
self.container = None
def _post_init(self, name, container=None):
"""
Called automatically by the 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 the container.
Method :meth:`merge_into_group` will be called during evaluation of the
group
**Example**:
.. code-block:: python
from candv import Constants
from candv import SimpleConstant
class FOO(Constants):
A = SimpleConstant()
B = SimpleConstant().to_group(Constants,
B2 = SimpleConstant(),
B0 = SimpleConstant(),
B1 = SimpleConstant(),
)
"""
return _LazyConstantsGroup(self, group_class, **group_members)
[docs] def merge_into_group(self, group):
"""
Called automatically by the 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:`Constants` or of its subclass into which
this constant will be merged
:returns: ``None``
"""
@property
def full_name(self):
prefix = (
self.container.full_name
if self.container
else UNBOUND_CONSTANT_CONTAINER_NAME
)
return f"{prefix}.{self.name}"
[docs] def to_primitive(self, *args, **kwargs):
"""
Represent the constant via Python's primitive data structures.
.. versionchanged:: 1.5.0
The ``context`` param is replaced by ``*args`` and ``**kwargs``.
.. versionadded:: 1.3.0
"""
return {
'name': self.name,
}
def __repr__(self):
"""
Produce a text identifying the constant.
"""
return f"<constant '{self.full_name}'>"
def __hash__(self):
"""
.. versionadded:: 1.3.1
"""
return hash(self.full_name)
def __eq__(self, other):
"""
.. versionadded:: 1.3.1
"""
return (
isinstance(other, SimpleConstant)
and (self.full_name == other.full_name)
)
def __ne__(self, other):
"""
.. versionadded:: 1.3.1
"""
return not (self == other)
class _LazyConstantsGroup:
def __init__(self, constant, group_class, **group_members):
self._validate_group_members(group_members)
self.constant = constant
self.group_class = group_class
self.group_members = group_members
@staticmethod
def _validate_group_members(group_members):
for name, obj in group_members.items():
if not isinstance(obj, (SimpleConstant, _LazyConstantsGroup)):
raise CandvInvalidGroupMemberError(
f'invalid group member "{obj}": only instances of "{SimpleConstant}" '
f'or other groups are allowed'
)
def _evaluate(self, parent, name):
full_name = f"{parent.full_name}.{name}"
group_bases = (self.group_class, )
self.group_members.update({
'name': name,
'full_name': full_name,
'container': parent,
'__repr': f"<constants group '{full_name}'>",
})
group = type(full_name, group_bases, self.group_members)
group.to_primitive = self._make_to_primitive(group, self.constant)
self.constant.merge_into_group(group)
del self.constant
del self.group_class
del self.group_members
return group
@staticmethod
def _make_to_primitive(group, constant):
# define aliases to avoid shadowing and infinite recursion
constant_primitive = constant.to_primitive
group_primitive = group.to_primitive
def to_primitive(self, *args, **kwargs):
primitive = constant_primitive(*args, **kwargs)
primitive.update(group_primitive(*args, **kwargs))
return primitive
return types.MethodType(to_primitive, group)
class _ConstantsContainerMeta(type):
"""
Metaclass for creating container classes for constants.
"""
def __new__(self, class_name, bases, attributes):
self._ensure_attribute(attributes, "name", class_name)
self._ensure_attribute(attributes, "full_name", class_name)
cls = super().__new__(self, class_name, bases, attributes)
# set before validations to get correct repr
cls.__repr = self._get_or_make_repr_value(attributes)
self._validate_constant_class(cls)
cls._members = self._make_members_from_attributes(cls, attributes)
return cls
@staticmethod
def _ensure_attribute(attributes, attribute_name, default_value):
if attribute_name not in attributes:
attributes[attribute_name] = default_value
@staticmethod
def _validate_constant_class(target_cls):
constant_class = getattr(target_cls, "constant_class", None)
if not issubclass(constant_class, SimpleConstant):
raise CandvInvalidConstantClass(
f'invalid "constant_class" for "{target_cls}": must be derived from '
f'"{SimpleConstant}", but got "{constant_class}"'
)
@staticmethod
def _get_or_make_repr_value(attributes):
value = attributes.pop("__repr", None)
if not value:
name = attributes["name"]
value = f"<constants container '{name}'>"
return value
@classmethod
def _make_members_from_attributes(cls, target_cls, attributes):
members = []
for name, the_object in attributes.items():
if isinstance(the_object, _LazyConstantsGroup):
group = the_object._evaluate(target_cls, name)
setattr(target_cls, name, group)
members.append((name, group))
elif isinstance(the_object, target_cls.constant_class):
cls._validate_constant_is_not_bound(target_cls, name, the_object)
the_object._post_init(name=name, container=target_cls)
members.append((name, the_object))
elif isinstance(the_object, SimpleConstant):
# init but do not append constants which are more generic
# than ``constant_class``
the_object._post_init(name=name)
return odict(members)
@staticmethod
def _validate_constant_is_not_bound(target_cls, attribute_name, the_object):
if the_object.container is not None:
raise CandvConstantAlreadyBoundError(
f'cannot use "{the_object}" as value for "{attribute_name}" attribute '
f'of "{target_cls}" container: already bound to "{the_object.container}"'
)
def __repr__(self):
return self.__repr
def __getitem__(self, name):
"""
Try to get constant by its name.
:param str name: name of constant to search for
:returns: a constant
:rtype: an instance of :class:`SimpleConstant` or its subclass
:raises CandvMissingConstantError:
if constant with name ``name`` is not present in container
**Example**:
.. code-block:: python
from candv import Constants
from candv import SimpleConstant
class FOO(Constants):
foo = SimpleConstant()
bar = SimpleConstant()
.. code-block:: python
>>> FOO['foo']
<constant 'FOO.foo'>
"""
try:
return self._members[name]
except KeyError:
raise CandvMissingConstantError(
f'constant "{name}" is not present in "{self}"'
)
def __contains__(self, name):
return name in self._members
def __len__(self):
return len(self._members)
def __iter__(self):
return self.iternames()
def get(self, name, default=None):
"""
Try to get a constant by its name or fallback to a default.
:param str name:
name of constant to search
: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:`SimpleConstant` or its subclass, or `default` value
**Example**:
.. code-block:: python
from candv import Constants
from candv import SimpleConstant
class FOO(Constants):
foo = SimpleConstant()
bar = SimpleConstant()
.. code-block:: python
>>> FOO.get('foo')
<constant 'FOO.foo'>
>>> FOO.get('xxx')
None
>>> FOO.get('xxx', default=123)
123
"""
return self._members.get(name, default)
def has_name(self, name):
"""
Check if the 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 the container.
:returns: a list of constant names in order constants were defined
:rtype: :class:`list` of strings
**Example**:
.. code-block:: python
from candv import Constants
from candv import SimpleConstant
class FOO(Constants):
foo = SimpleConstant()
bar = SimpleConstant()
.. code-block:: python
>>> FOO.names()
['foo', 'bar']
"""
return list(self._members.keys())
def iternames(self):
"""
Get an iterator over constants names.
Same as :meth:`names`, but returns an interator.
"""
return iter(self._members.keys())
def constants(self):
"""
List all constants in the container.
:returns: list of constants in order they were defined
:rtype: :class:`list`
**Example**:
.. code-block:: python
from candv import Constants
from candv import SimpleConstant
class FOO(Constants):
foo = SimpleConstant()
bar = SimpleConstant()
.. code-block:: python
>>> [x.name for x in FOO.constants()]
['foo', 'bar']
"""
return list(self._members.values())
def iterconstants(self):
"""
Get an iterator over constants.
Same as :meth:`constants`, but returns an interator
"""
return iter(self._members.values())
def items(self):
"""
Get list of constants names along with constants themselves.
:returns:
list of constants with their names in order they were defined.
Each element in the list is a :class:`tuple` in format ``(name, constant)``.
:rtype: :class:`list`
**Example**:
.. code-block:: python
from candv import Constants
from candv import SimpleConstant
class FOO(Constants):
foo = SimpleConstant()
bar = SimpleConstant()
.. code-block:: python
>>> FOO.items()
[('foo', <constant 'FOO.foo'>), ('bar', <constant 'FOO.bar'>)]
"""
return list(self._members.items())
def iteritems(self):
"""
Get an iterator over constants names along with constants themselves.
Same as :meth:`items`, but returns an interator
"""
return iter(self._members.items())
#: .. versionadded:: 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
#: .. versionadded:: 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
def to_primitive(self, *args, **kwargs):
"""
.. versionchanged:: 1.5.0
The ``context`` param is replaced by ``*args`` and ``**kwargs``.
.. versionadded:: 1.3.0
"""
items = [
x.to_primitive(*args, **kwargs)
for x in self.iterconstants()
]
return {
'name': self.name,
'items': items,
}
[docs]@export
class Constants(metaclass=_ConstantsContainerMeta):
"""
Base class for creating constants containers.
Each constant defined within the container will remember its creation order.
See an example in :meth:`constants`.
:cvar constant_class:
defines a class of constants which a container will store.
This attribute **MUST** be set up manually when you define a new container
type. Otherwise container will not be initialized. Default: ``None``
:raises CandvContainerMisusedError:
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 = SimpleConstant
def __new__(cls):
raise CandvContainerMisusedError(
f'"{cls}" cannot be instantiated: constant containers are not designed '
f'for that'
)
[docs]@export
def with_constant_class(the_class):
"""
Create a mixin class with ``constant_class`` attribute.
Allows to set a constant class for constants container outside container itself.
This may help to create more readable container definition, e.g.:
.. code-block:: python
from candv import Constants
from candv import SimpleConstant
from candv import with_constant_class
class CustomConstant(SimpleConstant):
...
class FOO(with_constant_class(CustomConstant), Constants):
A = CustomConstant()
B = CustomConstant()
.. code-block:: python
>>> FOO.constant_class
<class '__main__.CustomConstant'>
"""
class ConstantsMixin:
constant_class = the_class
return ConstantsMixin