Source code for bronx.patterns.getbytag

"""
GetByTag does not provide any "official" design pattern but we consider it to be
somehow an extension of the Singleton pattern.

The main objective is to manage and keep alive a collection of objects that are
identified by a unique **tag**. For a given **tag**, only one object can be created.
If one attempts to create an object with an already existing **tag**, the
constructor will return the previously created object that matches the **tag**.

Basic example (see :class:`GetByTag` class documentation for more advanced features)::

    >>> class A(GetByTag):
    ...     def __init__(self, data=None):
    ...         self.data = data

    >>> class A1(A):
    ...     # A1 and A will share the same list of tags...
    ...     _tag_topcls = False

    >>> class B(GetByTag):
    ...     def __init__(self, data=None):
    ...         self.data = data


    >>> a = A(data=1)
    >>> print(a.tag)
    default
    >>> b = B(data=1)
    >>> print(b.tag)
    default
    >>> b is not a
    True
    >>> a1 = A1(data=10)
    >>> print(a1.tag)
    default
    >>> a1 is a  # Chocking? No... since a and a1 have been created using the same default tag.
    True

    # A new object of tag 'toto' can be created as follows:
    >>> t = A1('toto')
    >>> t is not a
    True
    >>> tbis = A1(tag='toto')
    >>> t is tbis
    True

"""

from weakref import WeakSet

from bronx.fancies import loggers

logger = loggers.getLogger(__name__)


[docs]class GetByTagMeta(type): """Meta class constructor for :class:`GetByTag`. The purpose is quite simple : to set a dedicated shared table in the new class in construction. """ def __new__(cls, n, b, d): """Create a new class object.""" logger.debug('Base class for getbytag usage "%s / %s", bc = ( %s ), internal = %s', cls, n, b, d) if d.setdefault('_tag_topcls', True): d['_tag_table'] = dict() d['_tag_focus'] = dict(default=None) d['_tag_class'] = WeakSet() realnew = super().__new__(cls, n, b, d) realnew._tag_class.add(realnew) return realnew def __call__(self, *args, **kw): """Create a new class object.""" return self.__new__(self, *args, **kw)
[docs]class GetByTag(metaclass=GetByTagMeta): """ Utility to retrieve a new/existing object by a special argument named ``tag``. If an object had already been created with that tag, return this object. Optionally, :class:`GetByTag` can manage the active/passive state of the created objects. It's done using the notion of **focus**. **Focus** can be set, caught or checked using the :meth:`set_focus` class method, the :meth:`catch_focus` method and the :meth:`has_focus` method. Some actions can be launched when **focus** is gained or lost simply by subclassing the :meth:`focus_gain_allow`, :meth:`focus_gain_hook` and :meth:`focus_loose_hook` methods. """ _tag_default = 'default' _tag_implicit_new = True def __new__(cls, *args, **kw): """ Some class variables may have an impact on GetByTag behaviour: * ``_tag_default``: Sets the default ``tag`` (if the ``tag`` attribute is omitted when calling the constructor, the ``_tag_default`` string will be used. * ``_tag_implicit_new``: If set to ``False``, unless ``new=True`` is specified when calling the constructor, it won't be allowed to create new objects (a RuntimeError exception will be thrown). * ``_tag_topcls``: if set to ``False``, the tags list will be shared with the parent class (and possibly other siblings). The :meth:`tag_classes` class method allows to retrieve the list of Classes sharing the same list of tags """ tag = kw.pop('tag', None) if tag is None: if args: args = list(args) tag = args.pop(0) else: tag = cls._tag_default tag = cls.tag_clean(tag) new = kw.pop('new', False) if not new and tag in cls._tag_table: newobj = cls._tag_table[tag] else: if not cls._tag_implicit_new and not new: cls._tag_implicit_new_error(tag) newobj = super().__new__(cls) newobj._tag = tag cls._tag_table[tag] = newobj newobj.__init__(*args, **kw) return newobj @classmethod def _tag_implicit_new_error(cls, tag): """Called whenever a tag does not exist and _tag_implicit_new = False.""" raise RuntimeError(("It's not allowed to create a new {:s} object (new tag={:s}) " "without an explicit new=True argument.").format(cls.__name__, tag)) @property def tag(self): """The current object's tag.""" return self._tag
[docs] @classmethod def tag_clean(cls, tag): """By default, return the actual tag.""" return tag
[docs] @classmethod def tag_keys(cls): """Return an alphabetically ordered list of actual keys of the objects instantiated.""" return sorted(cls._tag_table.keys())
[docs] @classmethod def tag_values(cls): """Return a non-ordered list of actual values of the objects instantiated.""" return list(cls._tag_table.values())
[docs] @classmethod def tag_items(cls): """Proxy to the ``items`` method of the internal dictionary table of objects.""" return list(cls._tag_table.items())
[docs] @classmethod def tag_check(cls, tag): """Check if the tag is in list of actual keys of the objects instanciated.""" return (tag in cls._tag_table.keys())
[docs] @classmethod def tag_focus(cls, select='default'): """Return the tag value of the actual object with focus according to the ``select`` value.""" return cls._tag_focus[select]
[docs] @classmethod def set_focus(cls, obj, select='default'): """Define a new tag value for the focus in the scope of the ``select`` value.""" # Do the sanity checks obj.focus_gain_allow() # Call the hook on the previous default object prev_focus = cls._tag_focus[select] if prev_focus is not None: prev_obj = cls(prev_focus) prev_obj.focus_loose_hook() # Actually change the default cls._tag_focus[select] = obj.tag # Call the hook on the new default object obj.focus_gain_hook()
[docs] def has_focus(self, select='default'): """Return a boolean value on equality of current tag and focus tag.""" return self.tag == self.__class__._tag_focus[select]
[docs] def catch_focus(self, select='default'): """The current object decides to be on focus !""" self.set_focus(self, select)
[docs] @classmethod def tag_clear(cls): """Clear all internal information about objects and focus for that class.""" cls._tag_table = dict() cls._tag_focus = dict(default=None)
[docs] @classmethod def tag_classes(cls): """Return a list of current classes that have been registered with the same GetByTag root.""" return list(cls._tag_class)
def __copy__(self): """I don't know how to deep copy a GetByTag...""" logger.debug("There is no trivial way to copy a GetByTag instance: returning self") return self def __deepcopy__(self, memo): """I don't know how to deep copy a GetByTag...""" logger.debug("There is no trivial way to deepcopy a GetByTag instance: returning self") memo[id(self)] = self return self
[docs] def focus_loose_hook(self): """This method is called when an object looses the focus.""" pass
[docs] def focus_gain_allow(self): """This method is called on the target object prior to any focus change. It might be useful if one wants to perform checks and raise an exception. """ pass
[docs] def focus_gain_hook(self): """This method is called when an object gains the focus.""" pass
if __name__ == '__main__': import doctest doctest.testmod()