Source code for extended_choices.helpers

"""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 ), ) )