"""Provides classes used to construct a full ``Choices`` instance.
Notes
-----
The documentation format in this file is numpydoc_.
.. _numpydoc: https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt
"""
from __future__ import unicode_literals
from builtins import object # pylint: disable=redefined-builtin
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping
from django.utils.functional import Promise
[docs]class ChoiceAttributeMixin(object):
"""Base class to represent an attribute of a ``ChoiceEntry``.
Used for ``constant``, ``name``, and ``display``.
It must be used as a mixin with another type, and the final class will be a type with
added attributes to access the ``ChoiceEntry`` instance and its attributes.
Attributes
----------
choice_entry : instance of ``ChoiceEntry``
The ``ChoiceEntry`` instance that hold the current value, used to access its constant,
value and display name.
constant : property
Returns the choice field holding the constant of the attached ``ChoiceEntry``.
value : property
Returns the choice field holding the value of the attached ``ChoiceEntry``.
display : property
Returns the choice field holding the display name of the attached ``ChoiceEntry``.
original_value : ?
The value used to create the current instance.
creator_type : type
The class that created a new class. Will be ``ChoiceAttributeMixin`` except if it was
overridden by the author.
Example
-------
Classes can be created manually:
>>> class IntChoiceAttribute(ChoiceAttributeMixin, int): pass
>>> field = IntChoiceAttribute(1, ChoiceEntry(('FOO', 1, 'foo')))
>>> field
1
>>> field.constant, field.value, field.display
('FOO', 1, 'foo')
>>> field.choice_entry
('FOO', 1, 'foo')
Or via the ``get_class_for_value`` class method:
>>> klass = ChoiceAttributeMixin.get_class_for_value(1.5)
>>> klass.__name__
'FloatChoiceAttribute'
>>> float in klass.mro()
True
"""
def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument
"""Construct the object (the other class used with this mixin).
Notes
-----
Only passes the very first argument to the ``super`` constructor.
All others are not needed for the other class, only for this mixin.
"""
if issubclass(cls, Promise):
# Special case to manage lazy django stuff like ugettext_lazy
return super(ChoiceAttributeMixin, cls).__new__(cls)
return super(ChoiceAttributeMixin, cls).__new__(cls, *args[:1])
def __init__(self, value, choice_entry):
"""Initiate the object to save the value and the choice entry.
Parameters
----------
value : ?
Value to pass to the ``super`` constructor (for the other class using this mixin)
choice_entry: ChoiceEntry
The ``ChoiceEntry`` instance that hold the current value, used to access its constant,
value and display name.
Notes
-----
Call the ``super`` constructor with only the first value, as the other class doesn't
expect the ``choice_entry`` parameter.
"""
if isinstance(self, Promise):
# Special case to manage lazy django stuff like ugettext_lazy
# pylint: disable=protected-access
super(ChoiceAttributeMixin, self).__init__(value._proxy____args, value._proxy____kw)
else:
super(ChoiceAttributeMixin, self).__init__()
self.original_value = value
self.choice_entry = choice_entry
if self.choice_entry.attributes:
for key, value in self.choice_entry.attributes.items():
setattr(self, key, value)
@property
def constant(self):
"""Property that returns the ``constant`` attribute of the attached ``ChoiceEntry``."""
return self.choice_entry.constant
@property
def value(self):
"""Property that returns the ``value`` attribute of the attached ``ChoiceEntry``."""
return self.choice_entry.value
@property
def display(self):
"""Property that returns the ``display`` attribute of the attached ``ChoiceEntry``."""
return self.choice_entry.display
[docs] @classmethod
def get_class_for_value(cls, value):
"""Class method to construct a class based on this mixin and the type of the given value.
Parameters
----------
value: ?
The value from which to extract the type to create the new class.
Notes
-----
The create classes are cached (in ``cls.__classes_by_type``) to avoid recreating already
created classes.
"""
type_ = value.__class__
# Check if the type is already a ``ChoiceAttribute``
if issubclass(type_, ChoiceAttributeMixin):
# In this case we can return this type
return type_
# Create a new class only if it wasn't already created for this type.
if type_ not in cls._classes_by_type:
# Compute the name of the class with the name of the type.
class_name = str('%sChoiceAttribute' % type_.__name__.capitalize())
# Create a new class and save it in the cache.
cls._classes_by_type[type_] = type(class_name, (cls, type_), {
'creator_type': cls,
})
# Return the class from the cache based on the type.
return cls._classes_by_type[type_]
def __reduce__(self):
"""Reducer to make the auto-created classes picklable.
Returns
-------
tuple
A tuple as expected by pickle, to recreate the object when calling ``pickle.loads``:
1. a callable to recreate the object
2. a tuple with all positioned arguments expected by this callable
"""
return (
# Function to create a choice attribute
create_choice_attribute,
(
# The class that created the class of the current value
self.creator_type,
# The original type of the current value
self.original_value,
# The tied `choice_entry`
self.choice_entry
)
)
def __bool__(self):
"""Use the original value to know if the value is truthy of falsy"""
return bool(self.original_value)
_classes_by_type = {}
[docs]def create_choice_attribute(creator_type, value, choice_entry):
"""Create an instance of a subclass of ChoiceAttributeMixin for the given value.
Parameters
----------
creator_type : type
``ChoiceAttributeMixin`` or a subclass, from which we'll call the ``get_class_for_value``
class-method.
value : ?
The value for which we want to create an instance of a new subclass of ``creator_type``.
choice_entry: ChoiceEntry
The ``ChoiceEntry`` instance that hold the current value, used to access its constant,
value and display name.
Returns
-------
ChoiceAttributeMixin
An instance of a subclass of ``creator_type`` for the given value
"""
klass = creator_type.get_class_for_value(value)
return klass(value, choice_entry)
[docs]class ChoiceEntry(tuple):
"""Represents a choice in a ``Choices`` object, with easy access to its attribute.
Expecting a tuple with three entries. (constant, value, display name), it will add three
attributes to access then: ``constant``, ``value`` and ``display``.
By passing a dict after these three first entries, in the tuple, it's also possible to
add some other attributes to the ``ChoiceEntry` instance``.
Parameters
----------
tuple_ : tuple
A tuple with three entries, the name of the constant, the value, and the display name.
A dict could be added as a fourth entry to add additional attributes.
Example
-------
>>> entry = ChoiceEntry(('FOO', 1, 'foo'))
>>> entry
('FOO', 1, 'foo')
>>> (entry.constant, entry.value, entry.display)
('FOO', 1, 'foo')
>>> entry.choice
(1, 'foo')
You can also pass attributes to add to the instance to create:
>>> entry = ChoiceEntry(('FOO', 1, 'foo', {'bar': 1, 'baz': 2}))
>>> entry
('FOO', 1, 'foo')
>>> entry.bar
1
>>> entry.baz
2
Raises
------
AssertionError
If the number of entries in the tuple is not expected. Must be 3 or 4.
"""
# Allow to easily change the mixin to use in subclasses.
ChoiceAttributeMixin = ChoiceAttributeMixin
def __new__(cls, tuple_):
"""Construct the tuple with 3 entries, and save optional attributes from the 4th one."""
# Ensure we have exactly 3 entries in the tuple and an optional dict.
assert 3 <= len(tuple_) <= 4, 'Invalid number of entries in %s' % (tuple_,)
attributes = None
if len(tuple_) == 4:
attributes = tuple_[3]
assert attributes is None or isinstance(attributes, Mapping), 'Last argument must be a dict-like object in %s' % (tuple_,)
if attributes:
for invalid_key in {'constant', 'value', 'display'}:
assert invalid_key not in attributes, 'Additional attributes cannot contain one named "%s" in %s' % (invalid_key, tuple_,)
# Call the ``tuple`` constructor with only the real tuple entries.
obj = super(ChoiceEntry, cls).__new__(cls, tuple_[:3])
# Save all special attributes.
# pylint: disable=protected-access
obj.attributes = attributes
obj.constant = obj._get_choice_attribute(tuple_[0])
obj.value = obj._get_choice_attribute(tuple_[1])
obj.display = obj._get_choice_attribute(tuple_[2])
# Add an attribute holding values as expected by django.
obj.choice = (obj.value, obj.display)
# Add additional attributes.
if attributes:
for key, value in attributes.items():
setattr(obj, key, value)
return obj
def _get_choice_attribute(self, value):
"""Get a choice attribute for the given value.
Parameters
----------
value: ?
The value for which we want a choice attribute.
Returns
-------
An instance of a class based on ``ChoiceAttributeMixin`` for the given value.
Raises
------
ValueError
If the value is None, as we cannot really subclass NoneType.
"""
if value is None:
raise ValueError('Using `None` in a `Choices` object is not supported. You may '
'use an empty string.')
return create_choice_attribute(self.ChoiceAttributeMixin, value, self)
def __reduce__(self):
"""Reducer to pass attributes when pickling.
Returns
-------
tuple
A tuple as expected by pickle, to recreate the object when calling ``pickle.loads``:
1. a callable to recreate the object
2. a tuple with all positioned arguments expected by this callable
"""
return (
# The ``ChoiceEntry`` class, or a subclass, used to create the current instance
self.__class__,
# The original values of the tuple, and attributes (we pass a tuple as single argument)
(
(
self.constant.original_value,
self.value.original_value,
self.display.original_value,
self.attributes
),
)
)