decoratory


Namedecoratory JSON
Version 0.9.9.3 PyPI version JSON
download
home_pagehttps://decoratory.app/index.html?ref=PyPI
SummaryPython Decorators: Singleton, SemiSingleton, Multiton, Observer, Observable, generic Wrapper.
upload_time2024-01-07 13:51:10
maintainerMartin Abel
docs_urlNone
authorMartin Abel
requires_python>=3.7
licenseMIT
keywords python decorators semi-singleton singleton multiton observer observable wrapper
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            
.. _top:

..  --------------------------------------------------------------------------
    Documentation for the decoratory package
    --------------------------------------------------------------------------
    __title__ = "Decoratory"
    __module__ = "Readme.rst"
    __author__ = "Martin Abel"
    __maintainer__ = "Martin Abel"
    __credits__ = ["Martin Abel"]
    __company__ = "eVation"
    __email__ = "python@evation.eu"
    __url__ = "https://decoratory.app"
    __copyright__ = f"(c) Copyright 2020-2024, {__author__}, {__company__}."
    __created__ = "2020-01-01"
    __version__ = "0.9.9.1"
    __date__ = "2024-01-07"
    __time__ = "13:25:54"
    __state__ = "Beta"
    __license__ = "MIT"
    --------------------------------------------------------------------------


==============================================================================
Decoratory
==============================================================================


**Introduction**

The *decoratory package* is based on the `Decorator Arguments Template`_, an
integrated concept for Python decorators with and without parameters. In
addition, all decorators created with it support complex arguments, e.g.
lists of values and functions, without unnecessarily complicating the
decoration of simple cases by these extensions. All implementation details
are described on the `Project Homepage`_.


**Installation** ::

    pip install --upgrade decoratory

After installation, basic information about the package, its individual
modules and their methods is available from the command line. ::

    python -m decoratory --help

In particular, there is a *comprehensive unit test* for each module, which
can be executed from the command line using the ``--test`` option. ::

    python -m decoratory --test

.. _toc:


**Package Contents**

The *decoratory package* includes some classic decorators
implemented and functionally extended with this concept, e.g.

* `Singleton`_
* `Multiton`_
* `Wrapper`_
* `Observer`_

This is an open list of modules that possibly will grow over time.


**Description**

To illustrate the functionality of each module, simple as well as
more complex examples are presented. Even if only one particular module
is needed, it is recommended to view the preceding examples as well. For
more examples of the full range of possibilities, please refer to
`Decorator Implementations`_ on the `Project Homepage`_.


******************************************************************************
Singleton
******************************************************************************

A `singleton pattern`_ is a design pattern that limits the instantiation of
a class to a single (unique) instance. This is useful when exactly one unique
object is needed i.e. to manage an expensive resource or coordinate actions
across module boundaries.

As a simple example serves the decoration of the class  ``Animal`` as a
singleton. In the context of the `Decorator Arguments Template`_, this can be
done both without brackets (decorator class) and with brackets (decorator
instance), meaning both notations describe the same functional situation.

.. code-block:: python

    # *** example_singleton.py - class Animal with Singleton decoration

    from decoratory.singleton import Singleton

    @Singleton                      # or @Singleton()
    class Animal:
        def __init__(self, name):
            self.name = name

        def __repr__(self):
            return f"{self.__class__.__name__}('{self.name}')"

    # Create Instances
    a = Animal(name='Teddy')        # Creates Teddy, the primary instance
    b = Animal(name='Roxie')        # Returns Teddy, no Roxi is created

If instances of the class ``Animal`` are now created, this is only done for the
very first instantiation, and for all further instantiations always this
*primary instance* is given back.

.. code-block:: python

    # *** example_singleton.py - verfication of the unique instance

    # Case 1: Static decoration using @Singleton or @Singleton()
    print(f"a = {a}")               # a = Animal('Teddy')
    print(f"b = {b}")               # b = Animal('Teddy')
    print(f"a is b: {a is b}")      # a is b: True
    print(f"a == b: {a == b}")      # a == b: True

.. _dynamic-decoration:

If instead of the above *static decoration* using pie-notation, i.e. with
@-notation at the class declaration, the *dynamic decoration* within Python
code is used, additional parameters can be passed to the decorator for
passing to or through the class initializer.

.. code-block:: python

    # *** example_singleton.py - dynamic decoration with extra parameters

    # Case 2: Dynamic decoration providing extra initial default values
    Animal = Singleton(Animal, 'Teddy')
    Animal()                        # Using the decorator's default 'Teddy'
    a = Animal(name='Roxie')        # Returns Teddy
    print(a)                        # Animal('Teddy')

Quite generally, for all the following decorators based on this
`Decorator Arguments Template`_, these two properties are always fulfilled:

#. Decoration as a class (without parentheses) and Decoration as an instance
   (with empty parentheses) are equivalent
#. For dynamic decoration, extra parameters can be passed, e.g. for the
   class initializer

So far, this singleton implementation follows the concept of *once
forever*, i.e. whenever a new instance of a class is created, one always
gets the *primary instance* back - without any possibility of ever changing
it again.

Although this behavior is consistent with the fundamental concept of a
singleton, there are situations where it might be useful to reset a
*singleton*. Such a *resettable singleton*, also called *semi-singleton*,
could be useful to express in code that an instance is often retrieved but
rarely changed.

.. code-block:: python

    # *** example_singleton.py - decoration as 'resettable singleton'

    @Singleton(resettable=True)     # Exposes an additional reset method
    class Animal:
        def __init__(self, name):
            self.name = name

        def __repr__(self):
            return f"{self.__class__.__name__}('{self.name}')"

    # Case 3: Decoration using @Singleton(resettable=True)
    print(Animal(name='Teddy'))     # Animal('Teddy')
    print(Animal(name='Roxie'))     # Animal('Teddy')   (=primary instance)
    Animal.reset()                  # Reset the singleton
    print(Animal(name='Roxie'))     # Animal('Roxie')
    print(Animal(name='Teddy'))     # Animal('Roxie')   (=primary instance)

Without this striking ``resettable=True`` decoration ``Animal`` has no
``reset`` method and the call ``Animal.reset()`` will fail raising an
``AttributeError``. For situations where this concept needs
to be used more often, a subclass shortcut ``SemiSingleton`` is provided.

.. code-block:: python

    # *** example_singleton.py - decoration as a 'semi singleton'

    from decoratory.singleton import SemiSingleton

    @SemiSingleton                  # or @SemiSingleton()
    class Animal:
        pass                        # Some code ...

Both ``Singleton`` and ``SemiSingleton`` of course provide a ``get_instance()``
method to directly retrieve the primary instance, e.g. using
``Animal.get_instance()``.

    **Hint** --- Using ``reset()`` and ``get_instance()`` in combination

    *It should be noted that the combination of* ``reset()`` *and immediately
    following* ``get_instance()`` *does not return a valid object, but*
    ``None``. *So a* ``reset()`` *should always be followed by an
    instantiation to ensure that a valid singleton instance exists.*

Within the main process of Python's Global Interpreter Lock (GIL), both
**Singleton and SemiSingleton are thread-safe**. In example, using a
``ThreadPoolExecutor`` threadsafety can be easily demonstrated with sample
code like this:

.. code-block:: python

    # *** example_singleton.py - threadsafety

    from decoratory.singleton import Singleton
    from concurrent.futures import ThreadPoolExecutor, as_completed

    @Singleton                      # or @Singleton()
    class Animal:
        def __init__(self, name):
            self.name = name

        def __repr__(self):
            return f"{self.__class__.__name__}('{self.name}')"

    # Create Instances
    names = ["Teddy", "Roxie", "Molly", "Benny"]
    with ThreadPoolExecutor(max_workers=2) as tpe:
        futures = [tpe.submit(Animal, name) for name in names]

    # Case 4: Decoration using @Singleton
    for future in futures:
        instance = future.result()  # All the same instances, i.e.
        print(instance)             # Animal('Teddy') -- four times!

The same instance is always presented, most likely ``Animal('Teddy')`` of
the first submitted thread, but it could also be any of the others.


******************************************************************************
Multiton
******************************************************************************

A `multiton pattern`_ is a design pattern that extends the singleton pattern.
Whereas the singleton allows for exactly one instance per class, the multiton
ensures one single (unique) *instance per key*.

In this implementation, the key parameter can be anything that is possible as
a key for a Python ``dict()`` dictionary, such as an immutable type or a
callable eventually returning such an immutable type etc.

In case of an invalid key, key is set ``None`` and with only
one key value the multiton simply collapses to a singleton, therefore the
decoration ``@Multiton`` resp. ``@Multiton()`` or even ``@Multiton(key=17)``
or  ``@Multiton(key='some constant value')`` and so on always creates a
singleton.

Normally, the key is part of or is composed from the initial values of the
classified object, as in the following example, where the key function matches
the signature of the initializer and uses the initial value of the ``name``
parameter to construct a key value for the instances of ``Animal``.

.. code-block:: python

    # *** example_multitonton.py - class Animal with Multiton decoration

    from decoratory.multiton import Multiton

    @Multiton(key=lambda spec, name: name)
    class Animal:
        def __init__(self, spec, name):
            self.spec = spec
            self.name = name

        def __repr__(self):
            return f"{self.__class__.__name__}('{self.spec}', '{self.name}')"

    # Create Instances
    a = Animal('dog', name='Teddy')
    b = Animal('cat', name='Molly')
    c = Animal('dog', name='Roxie')

When instances of the class ``Animal`` are now created, this only happens for
the *first instantiation per key value*, the initial name of the animal. For
all subsequent instantiations, this *primary instance per key value* is
returned. But for each new key value, a new ``Animal`` instance is created
and stored in the internal directory.

.. code-block:: python

    # *** example_multitonton.py - One unique instance per name

    # Case 1: decoration @Multiton(key=lambda spec, name: name)
    print(a)                        # Animal('dog', 'Teddy')
    print(b)                        # Animal('cat', 'Molly')
    print(c)                        # Animal('dog', 'Roxie')

With three different names, a separate instance is created in each case.
In contrast, the following variant distinguishes only two types (equivalence
classes): animals with a character 'y' in their name and those without and
thus the key values can only be ``True`` or ``False``.

.. code-block:: python

    # *** example_multitonton.py - One unique instance per equivalence class

    # Case 2: decoration @Multiton(key=lambda spec, name: 'y' in name)
    print(a)                        # Animal('dog', 'Teddy')
    print(b)                        # Animal('dog', 'Teddy')
    print(c)                        # Animal('dog', 'Roxie')

The initial parameter values of the initializer can also be accessed by their
``args``-index or ``kwargs``-name. So the following decorations are also
possible:

.. code-block:: python

    # *** example_multitonton.py - Alternative decoration examples

    # Case 3: One unique instance per specie
    @Multiton(key="{0}".format)     # spec is args[0]
    class Animal:
        pass                        # Some code ...

    # Case 4: One unique instance per name
    @Multiton(key="{name}".format)  # name is kwargs['name']
    class Animal:
        pass                        # Some code ...

    # Case 5: One unique instance for all init values, i.e. no duplicates
    @Multiton(key=lambda spec, name: (spec, name))
    class Animal:
        pass                        # Some code ...

    # Case 6: One unique instance from a @staticmethod or @classmethod
    @Multiton(key=F("my_key"))      # Late binding with F(classmethod_string)
    class Animal:
        pass                        # Some code ...

        @classmethod
        def my_key(cls, spec, name):
            return 'y' in name

To actively control access to new equivalence classes, ``Multiton`` provides
the ``seal()``, ``unseal()``, and ``issealed()`` methods for sealing, unsealing,
and checking the sealing state of the ``Multiton``. By default, the sealing
state is set ``False``, so for every new key a new (unique) object is
instantiated. When sealed (e.g. later in the process) is set ``True`` the
dictionary has completed, i.e. restricted to the current object set and
any new key raises a ``KeyError``.

In situations where it might be useful to reset the multiton to express in
code that instances are often retrieved but rarely modified, setting the
decorator parameter ``resettable=True`` will expose the ``reset()`` method,
by means of which the internal directory of instances can be completely cleared.

Last but not least, ``Multiton`` provides a ``instances`` property and
associated getter and setter methods to directly retrieve the internal
dictionary of primary instances. It is obvious that manipulations on this
directory can corrupt the functionality of the multiton, but sometimes it
is useful to have the freedom of access.

    **Hint** --- Changes affecting key values of classified objects

    *Classifications into the multiton directory are done only once on
    initial key data. Subsequent changes affecting a key value are not
    reflected in the multiton directory key, i.e. the directory may then be
    corrupted by such modifications.*

    *Therefore,* **never change key related values of classified objects!**

All these things taken together could give the following exemplary picture:

.. code-block:: python

    # *** example_multitonton.py - seal, unseal, reset, get_instance

    # Case 7: with decoration @Multiton(key=lambda spec, name: name,
    #                                   resettable=True)
    Animal.reset()                  # Because of resettable=True
    print(Animal.get_instances())   # {}
    print(Animal.issealed())        # False     (=default)
    Animal('dog', name='Teddy')     # Animal('dog', 'Teddy')
    print(Animal.get_instances())   # {'Teddy': Animal('dog', 'Teddy')}
    Animal.seal()                   # Seal the multiton!
    print(Animal.issealed())        # True
    try:                            # Try to..
        Animal('cat', name='Molly') # .. add a new animal
    except  KeyError as ex:         # .. will fail
        print(f"Sorry {ex.args[1]}, {ex.args[0]}")
    print(Animal.get_instances())   # {'Teddy': Animal('dog', 'Teddy')}
    Animal.unseal()                 # Unseal the multiton!
    print(Animal.issealed())        # False
    Animal('cat', name='Molly')     # Now, Molly is added
    print(Animal.get_instances())   # {'Teddy': Animal('dog', 'Teddy'),
                                    #  'Molly': Animal('cat', 'Molly')}
    Animal.get_instances().pop('Teddy')
    print(Animal.get_instances())   # {'Molly': Animal('cat', 'Molly')}
    Animal.get_instances().clear()  # Same as Animal.reset()
    print(Animal.get_instances())   # {}

The last two lines show the functional equivalence of
``Animal.get_instances().clear()`` with ``Animal.reset()``, but the ``reset``
option is more transparent because it is not necessary to look
"behind the scenes".

Within the main process of Python's Global Interpreter Lock (GIL),
**Multiton is thread-safe**. In example, using a ``ThreadPoolExecutor``
threadsafety can be easily demonstrated with sample code like this:

.. code-block:: python

    # *** example_multiton.py - threadsafety

    from decoratory.multiton import Multiton
    from concurrent.futures import ThreadPoolExecutor, as_completed

    @Multiton(key=lambda spec, name: spec)
    class Animal:
        def __init__(self, spec, name):
            self.spec = spec
            self.name = name

        def __repr__(self):
            return f"{self.__class__.__name__}('{self.spec}', '{self.name}')"

    # Create Instances
    pets = [('dog', 'Teddy'), ('dog', 'Roxie'),    # dogs
            ('cat', 'Molly'), ('cat', 'Felix')]    # cats
    with ThreadPoolExecutor(max_workers=2) as tpe:
        futures = [tpe.submit(Animal, *pet) for pet in pets]

    # Case 8: Decoration using spec: @Multiton(key=lambda spec, name: spec)
    for future in futures:          # Same instance per spec (=key), i.e.
        instance = future.result()  # Animal('dog', 'Teddy') - for all dogs
        print(instance)             # Animal('cat', 'Molly') - for all cats

Per type of animal (``key = spec``) always the same instance is presented,
most likely ``Animal('Teddy')`` for all dogs and ``Animal('cat', 'Molly')``
for all cats, resulting from the first submitted thread per species, but it
could also be any of the others.


******************************************************************************
Wrapper
******************************************************************************

As the name implies, a wrapper encloses the original function with an

* (optional) ``before`` call functionality

and/or an

* (optional) ``after`` call functionality.

This implementation additionally supports an

* (optional) ``replace`` call functionality.

This generic Wrapper is all the more broadly applicable, the more flexibly
these three activities can be formulated. All three decorator parameters,
``before``, ``after`` and ``replace``, can be combined with each other and
support both single callables and (nested) lists of ``F``-types
(imported from module decoratory.basic, see `F signature`_ below for details).
In addition, ``replace`` supports passing a result object from successive
replacement calls through an optional keyword argument named ``result`` with
a defaut value, e.g. ``result=None``.

Even without any of these arguments, such an *empty wrapper* can be used
to *overwrite* default values, for example.

.. code-block:: python

    # *** example_wrapper.py - overwrite default parameter values

    from decoratory.wrapper import Wrapper

    # Case 1: Dynamic decoration with decorator arguments, only
    def some_function(value: str = "original"):
        print(f"value = '{value}'")

    # Function call with default parameters
    some_function()                 # value = 'original'
    some_function = Wrapper(some_function, value="changed")
    some_function()                 # value = 'changed'

The functionality of ``some_function()`` itself remains unchanged.
For the sake of clarity, the principle of *all or nothing* is applied, i.e.
defaults must be defined for all parameters and they are only used if no
current parameters at all are transmitted. There is no mixing of current and
default parameters. Thus, even a call of the decorated function with an
incomplete parameter set is explicitly not supported and will throw a
``TypeError``.

A typical scenario for a wrapper is, of course, the execution of additional
functionality before and/or after a given functionality, which itself remains
unchanged, such as ``enter/leave`` markers, call data caches, runtime
measurements, etc. Here is a typical example:

.. code-block:: python

    # *** example_wrapper.py - enclose original function

    from decoratory.wrapper import Wrapper
    from decoratory.basic import F

    # Case 2: Decoration with before and after functionalities
    def print_message(message: str = "ENTER"):
        print(message)

    @Wrapper(before=print_message, after=F(print_message, "LEAVE"))
    def some_function(value: str = "original"):
        print(f"value = '{value}'")

    some_function()                 # ENTER
                                    # value = 'original'
                                    # LEAVE

.. _F signature:

While ``before`` calls ``print_message`` with its default parameters the
``after`` parameter uses the ``F``-function from ``decoratory.basic``.
It has a signature ``F(callable, *args, **kwargs)`` and encapsulates the
passing of any function with optional positional and keyword parameters.
Accordingly, the keyword parameter ``after=F(print_message, message="LEAVE")``
would also be possible.

The idea behind the ``replace`` option is not so much to replace the complete
original functionality, because you could simply create your own functionality
for that but to wrap the original functionality, e.g. according to the principle:

#. Edit and/or prepare the call parameters for the original functionality
#. Execute the original functionality with these modified call parameters
#. Edit and/or revise the result and return this modified result

All this together could then look like this:

.. code-block:: python

    # *** example_wrapper.py - enclose and replacing original function

    # Case 3: Decoration with replace functionality
    def replace_wrapper(value: str="replace"):
        # 1. Edit the call parameters for the original functionality
        value = value.upper()
        # 2. Execute original functionality with modified call parameters
        result = some_function.substitute.callee(value)             # (1)
        # 3. Edit the result and return this modified result
        return f"result: '{result}'"

    @Wrapper(replace=replace_wrapper)
    def some_function(value: str = "original"):
        print(f"value = '{value}'")
        return value

    result = some_function()        # value = 'REPLACE'
    print(result)                   # result: 'REPLACE'

The first output ``value = 'REPLACE'`` comes from the original function
``some_function()`` but using parameters  modified to uppercase letters
by the``replace_wrapper()``. The second line ``result: 'REPLACE'`` is the
result of the ``return`` modified by the ``replace_wrapper()``. Please note
the line marked with ``(1)`` in the ``replace_wrapper()``: It is very
important to avoid self-recursions:

    **Hint** --- Avoidance of self-recursion in the replace wrapper

    *In the replace wrapper, the undecorated version of the original
    functionality must always be called. It is accessible via the*
    ``substitute.callee`` *method of the wrapper!*

For the sake of completeness, a rather more complex example illustrates
the replacement of the original functionality with a sequence of replacement
functionalities, passing a ``result`` object of type ``int`` between
successive calls.

.. code-block:: python

    # *** example_wrapper.py - enclose and replacing original function

    # Case 4: Decoration with before, after and multiple replacements
    def print_message(message: str = "UNDEFINED"):
        print(message)

    def replacement_printer(add: int = 1, *, result=None):
        result += add if isinstance(result, int) else 0
        print(f"result = {result}")
        return result

    @Wrapper(before=F(print, "ENTER"), # Python's print()
             replace=[F(replacement_printer, 1, result=0),
                      F(replacement_printer, 3),
                      F(replacement_printer, 5)],
             after=F(print_message, "LEAVE"))
    def result_printer(message: str = "UNKNOWN"):
        print(message)

    result_printer()                # ENTER         (before)
                                    # result = 1    (replacement_printer, 1)
                                    # result = 4    (replacement_printer, 3)
                                    # result = 9    (replacement_printer, 5)
                                    # LEAVE         (after)
                                    # 9             (output default_printer)

The absence of the outputs of ``UNDEFINED`` and ``UNKNOWN`` reflects the
correct replacements by the decoration, and the order of execution is exactly
as expected: ``before`` then ``replace`` then ``after`` and in each of these
variables the lists are processed in ascending order.

The *decoration of a class* always refers to the initializer of the class, e.g.

.. code-block:: python

    # *** example_wrapper.py - class decoration

    @Wrapper(before=F(print, "BEFORE init"), after=F(print, "AFTER init"))
    class Animal:
        def __init__(self, name):
            self.name = name
            print("RUNNING init")

    # Case 5: Decoration of a class always refers to __init__
    a = Animal(name='Teddy')        # BEFORE init
                                    # RUNNING init
                                    # AFTER init


For all other methods applies:

    **Hint** --- Dynamic versus static decoration

    *Decorations to* ``@staticmethod`` *or* ``@classmethod`` *can be done
    analogously to the function decorations above, since they already exist
    at compile time. Instance methods, on the other hand, do not exist until
    an object instance is created and must be decorated dynamically as an
    instance (e.g. see*  `Instance Decoration`_ *below).*

With ``Wrapper`` and custom service functions, a *private wrapper library*
can be built and reused.

.. code-block:: python

    # *** example_wrapper.py - private wrapper library

    # Case 6: Define a private wrapper library
    before_wrapper = Wrapper(before=F(print, "BEFORE"))
    after_wrapper = Wrapper(after=F(print, "AFTER"))

    # Multiple decorations for specialized functionality encapsulation
    @before_wrapper
    @after_wrapper
    def some_function(value: str = "original"):
        print(f"value = '{value}'")

    some_function()                 # BEFORE
                                    # value = 'original'
                                    # AFTER


******************************************************************************
Observer
******************************************************************************

The `observer pattern`_ is generally used to inform one or more registered
objects (observers, subscribers, objects) about selected actions of an
observed object (observable, publisher, subject).

The time of activation is set to ``AFTER`` by default, i.e. the observable
performs its own activity and then activates all registered observers in the
specified order. This setting can be adjusted to before, after, both or even
no activation at all via the parameter ``activate`` of ``Observable``.

This implementation provides several ways to decorate a function or class
as an observable or observer.

* `Observable Decoration`_
* `Observer Decoration`_
* `Class Decoration`_
* `Instance Decoration`_


Observable Decoration
---------------------

The simplest and at the same time the most pythonic variant of decoration
is to *decorate only the observed entities as an* ``Observable``.

This is possible because all observer pattern functionalities are concentrated
in the ``Observable.BaseClass = BaseObservable`` of the observable decorator,
while the ``Observer.BaseClass = BaseObserver`` of the observer decorator
remains empty here. If necessary, it is possible to inherit from both
BaseClasses to modify their functionalities.

.. code-block:: python

    # *** example_observer.py - observable decoration

    from decoratory.observer import Observable
    from decoratory.basic import F

    def person(say: str = "Hello?"):
        print(f"{person.__name__} says '{say}'")

    @Observable(observers=F(person, 'Hey, dog!'))
    def dog(act: str = "Woof!"):
        print(f"{dog.__name__} acts '{act}'")

    # Case 1: Observable decoration
    #    ---> Person as an observer to observable dog
    person()                        # person says 'Hello?'    (person acting)
    dog()                           # dog acts 'Woof!'        (dog acting)
                                    # person says 'Hey, dog!' (observer to dog)

Obviously, the addressed observer, the person, must be declared before
the observed dog. With the simpler decoration
``@Observable(observers=person)`` the person would always respond with their
default action and say ``'Hello?'``. The usage of ``F`` enables the transfer
of individual parameters to the observer.

Due to hierarchies in stacked observer patterns, a more detailed management
of observed vs. observing objects may be necessary.

.. code-block:: python

    # *** example_observer.py - observable decoration

    def person(say: str = "Hello?"):
        print(f"{person.__name__} says '{say}'")

    @Observable(observers=F(person, 'Hey, cat!'))
    def cat(act: str = "Meow!"):
        print(f"{cat.__name__} acts '{act}'")

    @Observable(observers=[F(cat, 'Roar!'), F(person, 'Hey, dog!')])
    def dog(act: str = "Woof!"):
        print(f"{dog.__name__} acts '{act}'")

    # Case 2: Stacked observable decoration
    #    ---> Cat observes dog, person observes cat and dog
    person()                        # person says 'Hello?'    (person acting)

    cat()                           # cat acts 'Meow!'        (cat acting)
                                    # person says 'Hey, cat!' (observer to cat)

    dog()                           # dog acts 'Woof!'        (dog acting)
                                    # cat acts 'Roar!'        (observer to dog)
                                    # person says 'Hey, cat!' (observer to cat)
                                    # person says 'Hey, dog!' (observer to dog)

Person is an observer, but not an observable, so the call to ``person()``
reflects only person’s own activity.
Cat is an observable that is observed by person and therefore the activity
``cat()`` triggers a follow-up activity by person.
Calling ``dog()`` results in three activities at the observers, because
``dog()`` is observed by the *observed cat*, which informs the person about
its own activity.

The order of reactions is determined by the order in the list in which
the cat observes the dog prior to the person. If this order is reversed:

.. code-block:: python

    # *** example_observer.py - observable decoration

    @Observable(observers=[F(person, 'Hey, dog!'), F(cat, 'Roar!')])
    def dog(act: str = "Woof!"):
        print(f"{dog.__name__} acts '{act}'")

    # Case 3: Stacked observable decoration
    #    ---> Cat observes dog, person observes dog and cat
    dog()                           # dog acts 'Woof!'        (dog acting)
                                    # person says 'Hey, dog!' (observer to dog)
                                    # cat acts 'Roar!'        (observer to dog)
                                    # person says 'Hey, cat!' (observer to cat)

Again, calling ``dog()`` results in three activities at the observers,
but here person reacts first as an observer to dog and later again as an
observer to cat.

If this behavior is not desired, ``dog()`` can instead address the
*original cat* using the ``cat.substitute.callee``, i.e.

.. code-block:: python

    # *** example_observer.py - observable decoration

    @Observable(observers=[F(cat.substitute.callee, 'Roar!'),
                           F(person, 'Hey, dog!')])
    def dog(act: str = "Woof!"):
        print(f"{dog.__name__} acts '{act}'")

    # Case 4: Stacked observable decoration
    #    ---> Original cat observes dog, person observes dog and cat
    dog()                           # dog acts 'Woof!'        (dog acting)
                                    # cat acts 'Roar!'        (observer to dog)
                                    # person says 'Hey, dog!' (observer to dog)

In this case, cat acts before person because of the order of the observer
list and because the *original cat* observes dog the ``Hey, cat!`` statement
of person is missing.


Observer Decoration
-------------------

In this reversed decoration scheme, the observer decorator collects its
observables. This seems more elaborate at first glance, but some prefer to
explicitly designate the ``Observer`` and ``Observable`` roles in their code.

Because an observer decoration uses observable methods, all
observable(s) must always be *declared and decorated* before their
observer(s).

    **1. Rule:** Declare *Observables before Observers*

    **2. Rule:** Decorating as *@Observable* before using in an *@Observer*

Thus, the initial example ``Case 1`` from `Observable Decoration`_ translates to:

.. code-block:: python

    # *** example_observer.py - observer decoration

    from decoratory.observer import Observer, Observable
    from decoratory.basic import X

    @Observable
    def dog(act: str = "Woof!"):    # 1. Rule: declare dog before person!
        print(f"{dog.__name__} acts '{act}'")

    @Observer(observables=X(dog, 'Hey, dog!'))
    def person(say: str = "Hello?"):
        print(f"{person.__name__} says '{say}'")

    # Case 1: Observer decoration
    #    ---> Person as an observer to observable dog
    person()                        # person says 'Hello?'
    dog()                           # dog acts 'Woof!'        (dog acting)
                                    # person says 'Hey, dog!' (observer to dog)

The use of the *semantic cross-function* ``X`` from ``decoratory.basic``
instead of ``F`` indicates that ``dog`` is the observable, but the ``X``
arguments apply for the observer ``person``.

For multiple decorations, the *order of decoration* is also relevant here.
The situation ``Case 2`` from `Observable Decoration`_ with person,
dog and cat would then look like:

.. code-block:: python

    # *** example_observer.py - observer decoration

    @Observable                     # 2. Rule: dog before cat & person
    def dog(act: str = "Woof!"):    # 1. Rule: dog before cat & person
        print(f"{dog.__name__} acts '{act}'")

    @Observer(observables=X(dog, 'Roar!'))
    @Observable                     # 2. Rule: observable cat before person
    def cat(act: str = "Meow!"):    # 1. Rule: cat before person
        print(f"{cat.__name__} acts '{act}'")

    @Observer(observables=[X(dog, 'Hey, dog!'),
                           X(cat.substitute.callee, say='Hey, cat!')])
    def person(say: str = "Hello?"):
        print(f"{person.__name__} says '{say}'")

    # Case 2: Stacked observer decoration
    #    ---> Cat observes dog, person observes cat and dog
    person()                        # person says 'Hello?'    (person acting)

    cat()                           # cat acts 'Meow!'        (cat acting)
                                    # person says 'Hey, cat!' (observer to cat)

    dog()                           # dog acts 'Woof!'        (dog acting)
                                    # cat acts 'Roar!'        (observer to dog)
                                    # person says 'Hey, cat!' (observer to cat)
                                    # person says 'Hey, dog!' (observer to dog)

Here, cat becomes an observer but its callee ``cat.substitute.callee`` is an
observable which can be observed by person! This *observed cat* observes
the dog, reacts and triggers the person.

To reproduce ``Case 4`` from above, simply swap the order of the decorations
at the cat and the dog then is observed by the *original cat*.

.. code-block:: python

    # *** example_observer.py - observer decoration

    @Observable                     # 2. Rule: dog before cat & person
    def dog(act: str = "Woof!"):    # 1. Rule: dog before cat & person
        print(f"{dog.__name__} acts '{act}'")

    @Observable                     # 2. Rule: cat before person
    @Observer(observables=X(dog, 'Roar!'))
    def cat(act: str = "Meow!"):    # 1. Rule: cat before person
        print(f"{cat.__name__} acts '{act}'")

    @Observer(observables=[X(dog, 'Hey, dog!'), X(cat, say='Hey, cat!')])
    def person(say: str = "Hello?"):
        print(f"{person.__name__} says '{say}'")

    # Case 3: Stacked observer decoration
    #    ---> Cat observes dog, person observes cat and dog
    person()                        # person says 'Hello?'    (person acting)

    cat()                           # cat acts 'Meow!'        (cat acting)
                                    # person says 'Hey, cat!' (observer to cat)

    dog()                           # dog acts 'Woof!'        (dog acting)
                                    # cat acts 'Roar!'        (observer to dog)
                                    # person says 'Hey, dog!' (observer to dog)

Now, both dog and cat end up being observables, observed by the person. But the
cat observing the dog is the *original cat*, which does not inform the person
about its activities, and so person’s statement ``Hey, cat!`` is missing.


Class Decoration
----------------

Both techniques, `Observable Decoration`_ and `Observer Decoration`_,
are static, in the sense, decorations are done e.g. in @-notation evaluated
at compile time. They are applied to *static functions*.

*Decoration of a class* by default addresses decoration of the
*class initializer*, this means

.. code-block:: python

    @Observable
    class Dog:
        def __init__(self):
            pass                    # Some code ...

should be understood as

.. code-block:: python

    class Dog:
        @Observable
        def __init__(self):
            pass                    # Some code ...

But this behavior is insidious, e.g.

.. code-block:: python

    # *** example_observer.py - class decoration

    from decoratory.observer import Observable

    class Person:
        def __init__(self, name: str = "Jane Doe"):
            print(f"{name} arrived.")

    @Observable(observers=Person)
    class Dog:
        def __init__(self, name: str = "Teddy"):
            print(f"Dog {name} arrived.")

    # Case 1: Dog is an observable to Person
    prs = Person()                  # Jane Doe arrived.
    dog = Dog()                     # Dog Teddy arrived.
                                    # Jane Doe arrived.

The instantiation of ``Dog`` induced an instantiation of ``Person``.

    **Hint** --- Take care when decorating a class initializer

    *Notifying the* ``__init__`` *method of an observer results in a new
    instance! This means calling the observable induces instantiation of
    a new observer object, surely in not any case this is the desired
    behavior ...*

So the decoration should not address a class but one (or more) target
methods of the class. As already mentioned, this is easy if this callback
function is a ``@staticmethod`` or ``@classmethod``.

.. code-block:: python

    # *** example_observer.py - class decoration

    class Person:
        def __init__(self, name: str = "Jane Doe"):
            print(f"{name} arrived.")

        @staticmethod
        def action1(act: str = "Hello?"):
            print(f"Person says {act}")

        @classmethod
        def action2(cls, act: str = "What's up?"):
            print(f"Person says {act}")

    @Observable(observers=[Person.action1, Person.action2])
    class Dog:
        def __init__(self, name: str = "Teddy"):
            print(f"Dog {name} arrived.")

    # Case 2: Dog is an observable to Person.action
    prs = Person()                  # Jane Doe arrived.
    dog = Dog()                     # Dog Teddy arrived.
                                    # Person says Hello?
                                    # Person says What's up?

This is how it usually works: *one action of the observable*, here it's
the instantiation of ``Dog``, triggers *one to many actions at each observer*,
here ``Person``.

.. _Class Decoration, Case 3:

But often an instance method is also interesting as a callback function:

- If a *particular instance* ``prs = Person(name="John Doe")`` of a
  person is meant, a decoration like ``@Observable(observers=prs.action)``
  with the *instance method* can be applied to ``Dog``.
- For *any instance* of a person ``@Observable(observers=Person().action)``
  works.

Even a list of ``F`` structures would be possible to optionally submit
different parameters.

.. code-block:: python

    # *** example_observer.py - class decoration

    from decoratory.observer import Observable
    from decoratory.basic import F

    class Person:
        def __init__(self, name: str = "Jane Doe"):
            self.name = name
            print(f"{name} arrived.")

        def action(self, act: str = "Hello?"):
            print(f"{self.name} says {act}")

    prs1 = Person()                 # Jane Doe arrived.
    prs2 = Person("John Doe")       # John Doe arrived.

    @Observable(observers=[prs1.action, F(prs2.action, "What's up?")])
    class Dog:
        def __init__(self, name: str = "Teddy"):
            print(f"Dog {name} arrived.")

    # Case 3: Dog is an observable to actions of various person instances.
    dog = Dog()                     # Dog Teddy arrived.
                                    # Jane Doe says Hello?
                                    # John Doe says What's up?

But here, *one action of the observable*, the instantiation of ``Dog``, triggers
*one to many actions at each selected resp. instantiated observer*, ``Person``.
In such situations, a late `dynamic decoration <#dynamic-decoration>`_
could be a good idea.

So far, instantiating ``Dog`` resulted in an information and induced
action at ``Person``. If ``Dog`` has its own actions that need to be
selectively monitored, each of the selected actions can of course be decorated
individually as an ``Observable``. For the sake of a better overview, this
can also be done on the class itself.

.. code-block:: python

    # *** example_observer.py - class decoration

    class Person:
        def __init__(self, name: str = "Jane Doe"):
            self.name = name
            print(f"{name} arrived.")

        @classmethod
        def actionA(cls, act: str = "Hello?"):
            print(f"Person says {act}")

        def actionB(self, act: str = "Hello?"):
            print(f"{self.name} says {act}")

    @Observable(methods=["action1", "action2"],
                observers=[Person.actionA, Person("Any Doe").actionB])
    class Dog:
        def __init__(self, name: str = "Teddy"):
            self.name = name
            print(f"Dog {name} arrived.")

        @staticmethod
        def action1(act: str = "Woof!"):
            print(f"Dog acts {act}")

        def action2(self, act: str = "Brrr!"):
            print(f"{self.name} acts {act}")

    # Case 4: Dog is an observable with selected actions.
                                    # Any Doe arrived.
    prs = Person()                  # Jane Doe arrived.
    dog = Dog()                     # Dog Teddy arrived.

    dog.action1()                   # Dog acts Woof!        (@staticmethod)
                                    # Person says Hello?    (@classmethod)
                                    # Any Doe says Hello?   (Instance 'Any')

    Dog.action2(dog)                # Teddy acts Brrr!      (Instance 'Teddy')
                                    # Person says Hello?    (@classmethod)
                                    # Any Doe says Hello?   (Instance 'Any')

The last line ``Dog.action2(dog)`` provides the instance of ``Teddy`` as *the
first argument* ``self``. This works because internally the *class method*
``Dog.action2`` was registered instead of an instance method that didn't
exist at compile time. On the other hand, the call ``dog.action2()`` would
fail because this *instance method* was not registered. But, if this is what
is to be achieved, an instance method can first be created and registered,
just as seen above in `Class Decoration, Case 3`_.


Instance Decoration
-------------------

The classic way to exchange information between objects with the observer
pattern is through the active use of the ``register``, ``dispatch``, and
``unregister`` *interface methods that an observable exposes*. Information can
be given to the right recipients at relevant places in the code. For this,
the classes are not decorated and `dynamic decoration <#dynamic-decoration>`_
comes into play. Dynamic decoration is used often also in connection with
getter/setter/property constructions since data changes take place
meaningfully over these methods.

Consider the following two example classes:

.. code-block:: python

    # *** example_observer.py - instance decoration

    class Note:                             # Observer without decoration!
        def info(self, thing):
            print(f"Note.info: val = {thing.a}")

    class Thing:                            # Observable without decoration!
        def __init__(self, a=0):            # Initializer, defining variabe 'a'
            self._a = a
        def inc(self):                      # Instance method, modifying 'a'
            self._a += 1
        def get_a(self):                    # Getter, setter, property,
            return self._a                  # modifying variable 'a'
        def set_a(self, value):
            self._a = value
        a = property(get_a, set_a)

Initially, all these classes are undecorated and typical actions might be:

.. code-block:: python

    # *** example_observer.py - instance decoration

    from decoratory.observer import Observable
    from decoratory.basic import F

    # (1) Setup instances
    nti = Note()                    # Note instance
    tgi = Thing()                   # Thing instance

    # (2) Dynamic decoration of some methods: Late binding
    tgi.inc = Observable(tgi.inc)           # Late method decoration
    Thing.set_a = Observable(Thing.set_a)   # Late property decoration
    Thing.a = property(Thing.get_a, Thing.set_a)

    # (3) Register the observer (Note) with the observable (Thing)
    tgi.inc.observable.register(F(nti.info, tgi))
    tgi.set_a.observable.register(F(nti.info, thing=tgi))

    # Case 1: Change self.a = 0 using inc()
    tgi.inc()                       # Note.info: val = 1

    # Case 2: Change self.a = 1 using setter via property
    tgi.a = 2                       # Note.info: val = 2

    # Case 3: Notification from inc() to nti.info() about Thing(3)
    tgi.inc.observable.dispatch(nti.info, Thing(3))
                                    # Note.info: val = 3

    # Case 4: Notification from set_a() to nti.info() about Thing(4)
    tgi.set_a.observable.dispatch(nti.info, Thing(4))
                                    # Note.info: val = 4

    # Case 5: Print the current value of tgi.a
    print(f"a = {tgi.a}")           # a = 2     (no changes by notification)

    # Case 6: Print list of all observers
    print(tgi.inc.observable.observers(classbased=True))
    # ---> {'Note': ['F(info, <__main__.Thing object at ..)']}
    print(tgi.set_a.observable.observers(classbased=True))
    # ---> {'Note': ['F(info, thing=<__main__.Thing object at ..)']}

    # Case 7: Unregister nti.info from tgi
    tgi.inc.observable.unregister(nti.info)
    print(tgi.inc.observable.observers(classbased=True))    # {}

In contrast to `Class Decoration`_, this `Instance Decoration`_

(1) instantiates the native classes (1), then
(2) decorates the relevant instance methods (2), and then
(3) registers the observers with the associated observables (3).

This method of instance decoration is certainly the most flexible.
However, it bears the risk of losing track of all dependencies.


~~~ `Home <#top>`_ ~~~ `Contents <#toc>`_ ~~~ `Singleton <#singleton>`_ ~~~ `Multiton <#multiton>`_ ~~~ `Wrapper <#wrapper>`_ ~~~ `Observer <#observer>`_ ~~~


.. ===========================================================================
.. _Project Homepage: https://decoratory.app/index.html?ref=PyPI
.. _singleton pattern: https://en.wikipedia.org/wiki/Singleton_pattern
.. _multiton pattern: https://en.wikipedia.org/wiki/Multiton_pattern
.. _observer pattern: https://en.wikipedia.org/wiki/Observer_pattern
.. _Decorator Arguments Template: https://decoratory.app/Section/ArgumentsTemplate.html?ref=PyPI
.. _Decorator Implementations: https://decoratory.app/Section/Decorators.html?ref=PyPI


            

Raw data

            {
    "_id": null,
    "home_page": "https://decoratory.app/index.html?ref=PyPI",
    "name": "decoratory",
    "maintainer": "Martin Abel",
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": "Martin Abel <python@evation.eu>",
    "keywords": "python decorators semi-singleton singleton multiton observer observable wrapper",
    "author": "Martin Abel",
    "author_email": "Martin Abel <python@evation.eu>",
    "download_url": "https://files.pythonhosted.org/packages/98/d0/58bafb0a651df612c02eb485ff3936d9714e8fdd1607ae4979932d399993/decoratory-0.9.9.3.tar.gz",
    "platform": "Operating System :: OS Independent",
    "description": "\r\n.. _top:\r\n\r\n..  --------------------------------------------------------------------------\r\n    Documentation for the decoratory package\r\n    --------------------------------------------------------------------------\r\n    __title__ = \"Decoratory\"\r\n    __module__ = \"Readme.rst\"\r\n    __author__ = \"Martin Abel\"\r\n    __maintainer__ = \"Martin Abel\"\r\n    __credits__ = [\"Martin Abel\"]\r\n    __company__ = \"eVation\"\r\n    __email__ = \"python@evation.eu\"\r\n    __url__ = \"https://decoratory.app\"\r\n    __copyright__ = f\"(c) Copyright 2020-2024, {__author__}, {__company__}.\"\r\n    __created__ = \"2020-01-01\"\r\n    __version__ = \"0.9.9.1\"\r\n    __date__ = \"2024-01-07\"\r\n    __time__ = \"13:25:54\"\r\n    __state__ = \"Beta\"\r\n    __license__ = \"MIT\"\r\n    --------------------------------------------------------------------------\r\n\r\n\r\n==============================================================================\r\nDecoratory\r\n==============================================================================\r\n\r\n\r\n**Introduction**\r\n\r\nThe *decoratory package* is based on the `Decorator Arguments Template`_, an\r\nintegrated concept for Python decorators with and without parameters. In\r\naddition, all decorators created with it support complex arguments, e.g.\r\nlists of values and functions, without unnecessarily complicating the\r\ndecoration of simple cases by these extensions. All implementation details\r\nare described on the `Project Homepage`_.\r\n\r\n\r\n**Installation** ::\r\n\r\n    pip install --upgrade decoratory\r\n\r\nAfter installation, basic information about the package, its individual\r\nmodules and their methods is available from the command line. ::\r\n\r\n    python -m decoratory --help\r\n\r\nIn particular, there is a *comprehensive unit test* for each module, which\r\ncan be executed from the command line using the ``--test`` option. ::\r\n\r\n    python -m decoratory --test\r\n\r\n.. _toc:\r\n\r\n\r\n**Package Contents**\r\n\r\nThe *decoratory package* includes some classic decorators\r\nimplemented and functionally extended with this concept, e.g.\r\n\r\n* `Singleton`_\r\n* `Multiton`_\r\n* `Wrapper`_\r\n* `Observer`_\r\n\r\nThis is an open list of modules that possibly will grow over time.\r\n\r\n\r\n**Description**\r\n\r\nTo illustrate the functionality of each module, simple as well as\r\nmore complex examples are presented. Even if only one particular module\r\nis needed, it is recommended to view the preceding examples as well. For\r\nmore examples of the full range of possibilities, please refer to\r\n`Decorator Implementations`_ on the `Project Homepage`_.\r\n\r\n\r\n******************************************************************************\r\nSingleton\r\n******************************************************************************\r\n\r\nA `singleton pattern`_ is a design pattern that limits the instantiation of\r\na class to a single (unique) instance. This is useful when exactly one unique\r\nobject is needed i.e. to manage an expensive resource or coordinate actions\r\nacross module boundaries.\r\n\r\nAs a simple example serves the decoration of the class  ``Animal`` as a\r\nsingleton. In the context of the `Decorator Arguments Template`_, this can be\r\ndone both without brackets (decorator class) and with brackets (decorator\r\ninstance), meaning both notations describe the same functional situation.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_singleton.py - class Animal with Singleton decoration\r\n\r\n    from decoratory.singleton import Singleton\r\n\r\n    @Singleton                      # or @Singleton()\r\n    class Animal:\r\n        def __init__(self, name):\r\n            self.name = name\r\n\r\n        def __repr__(self):\r\n            return f\"{self.__class__.__name__}('{self.name}')\"\r\n\r\n    # Create Instances\r\n    a = Animal(name='Teddy')        # Creates Teddy, the primary instance\r\n    b = Animal(name='Roxie')        # Returns Teddy, no Roxi is created\r\n\r\nIf instances of the class ``Animal`` are now created, this is only done for the\r\nvery first instantiation, and for all further instantiations always this\r\n*primary instance* is given back.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_singleton.py - verfication of the unique instance\r\n\r\n    # Case 1: Static decoration using @Singleton or @Singleton()\r\n    print(f\"a = {a}\")               # a = Animal('Teddy')\r\n    print(f\"b = {b}\")               # b = Animal('Teddy')\r\n    print(f\"a is b: {a is b}\")      # a is b: True\r\n    print(f\"a == b: {a == b}\")      # a == b: True\r\n\r\n.. _dynamic-decoration:\r\n\r\nIf instead of the above *static decoration* using pie-notation, i.e. with\r\n@-notation at the class declaration, the *dynamic decoration* within Python\r\ncode is used, additional parameters can be passed to the decorator for\r\npassing to or through the class initializer.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_singleton.py - dynamic decoration with extra parameters\r\n\r\n    # Case 2: Dynamic decoration providing extra initial default values\r\n    Animal = Singleton(Animal, 'Teddy')\r\n    Animal()                        # Using the decorator's default 'Teddy'\r\n    a = Animal(name='Roxie')        # Returns Teddy\r\n    print(a)                        # Animal('Teddy')\r\n\r\nQuite generally, for all the following decorators based on this\r\n`Decorator Arguments Template`_, these two properties are always fulfilled:\r\n\r\n#. Decoration as a class (without parentheses) and Decoration as an instance\r\n   (with empty parentheses) are equivalent\r\n#. For dynamic decoration, extra parameters can be passed, e.g. for the\r\n   class initializer\r\n\r\nSo far, this singleton implementation follows the concept of *once\r\nforever*, i.e. whenever a new instance of a class is created, one always\r\ngets the *primary instance* back - without any possibility of ever changing\r\nit again.\r\n\r\nAlthough this behavior is consistent with the fundamental concept of a\r\nsingleton, there are situations where it might be useful to reset a\r\n*singleton*. Such a *resettable singleton*, also called *semi-singleton*,\r\ncould be useful to express in code that an instance is often retrieved but\r\nrarely changed.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_singleton.py - decoration as 'resettable singleton'\r\n\r\n    @Singleton(resettable=True)     # Exposes an additional reset method\r\n    class Animal:\r\n        def __init__(self, name):\r\n            self.name = name\r\n\r\n        def __repr__(self):\r\n            return f\"{self.__class__.__name__}('{self.name}')\"\r\n\r\n    # Case 3: Decoration using @Singleton(resettable=True)\r\n    print(Animal(name='Teddy'))     # Animal('Teddy')\r\n    print(Animal(name='Roxie'))     # Animal('Teddy')   (=primary instance)\r\n    Animal.reset()                  # Reset the singleton\r\n    print(Animal(name='Roxie'))     # Animal('Roxie')\r\n    print(Animal(name='Teddy'))     # Animal('Roxie')   (=primary instance)\r\n\r\nWithout this striking ``resettable=True`` decoration ``Animal`` has no\r\n``reset`` method and the call ``Animal.reset()`` will fail raising an\r\n``AttributeError``. For situations where this concept needs\r\nto be used more often, a subclass shortcut ``SemiSingleton`` is provided.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_singleton.py - decoration as a 'semi singleton'\r\n\r\n    from decoratory.singleton import SemiSingleton\r\n\r\n    @SemiSingleton                  # or @SemiSingleton()\r\n    class Animal:\r\n        pass                        # Some code ...\r\n\r\nBoth ``Singleton`` and ``SemiSingleton`` of course provide a ``get_instance()``\r\nmethod to directly retrieve the primary instance, e.g. using\r\n``Animal.get_instance()``.\r\n\r\n    **Hint** --- Using ``reset()`` and ``get_instance()`` in combination\r\n\r\n    *It should be noted that the combination of* ``reset()`` *and immediately\r\n    following* ``get_instance()`` *does not return a valid object, but*\r\n    ``None``. *So a* ``reset()`` *should always be followed by an\r\n    instantiation to ensure that a valid singleton instance exists.*\r\n\r\nWithin the main process of Python's Global Interpreter Lock (GIL), both\r\n**Singleton and SemiSingleton are thread-safe**. In example, using a\r\n``ThreadPoolExecutor`` threadsafety can be easily demonstrated with sample\r\ncode like this:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_singleton.py - threadsafety\r\n\r\n    from decoratory.singleton import Singleton\r\n    from concurrent.futures import ThreadPoolExecutor, as_completed\r\n\r\n    @Singleton                      # or @Singleton()\r\n    class Animal:\r\n        def __init__(self, name):\r\n            self.name = name\r\n\r\n        def __repr__(self):\r\n            return f\"{self.__class__.__name__}('{self.name}')\"\r\n\r\n    # Create Instances\r\n    names = [\"Teddy\", \"Roxie\", \"Molly\", \"Benny\"]\r\n    with ThreadPoolExecutor(max_workers=2) as tpe:\r\n        futures = [tpe.submit(Animal, name) for name in names]\r\n\r\n    # Case 4: Decoration using @Singleton\r\n    for future in futures:\r\n        instance = future.result()  # All the same instances, i.e.\r\n        print(instance)             # Animal('Teddy') -- four times!\r\n\r\nThe same instance is always presented, most likely ``Animal('Teddy')`` of\r\nthe first submitted thread, but it could also be any of the others.\r\n\r\n\r\n******************************************************************************\r\nMultiton\r\n******************************************************************************\r\n\r\nA `multiton pattern`_ is a design pattern that extends the singleton pattern.\r\nWhereas the singleton allows for exactly one instance per class, the multiton\r\nensures one single (unique) *instance per key*.\r\n\r\nIn this implementation, the key parameter can be anything that is possible as\r\na key for a Python ``dict()`` dictionary, such as an immutable type or a\r\ncallable eventually returning such an immutable type etc.\r\n\r\nIn case of an invalid key, key is set ``None`` and with only\r\none key value the multiton simply collapses to a singleton, therefore the\r\ndecoration ``@Multiton`` resp. ``@Multiton()`` or even ``@Multiton(key=17)``\r\nor  ``@Multiton(key='some constant value')`` and so on always creates a\r\nsingleton.\r\n\r\nNormally, the key is part of or is composed from the initial values of the\r\nclassified object, as in the following example, where the key function matches\r\nthe signature of the initializer and uses the initial value of the ``name``\r\nparameter to construct a key value for the instances of ``Animal``.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_multitonton.py - class Animal with Multiton decoration\r\n\r\n    from decoratory.multiton import Multiton\r\n\r\n    @Multiton(key=lambda spec, name: name)\r\n    class Animal:\r\n        def __init__(self, spec, name):\r\n            self.spec = spec\r\n            self.name = name\r\n\r\n        def __repr__(self):\r\n            return f\"{self.__class__.__name__}('{self.spec}', '{self.name}')\"\r\n\r\n    # Create Instances\r\n    a = Animal('dog', name='Teddy')\r\n    b = Animal('cat', name='Molly')\r\n    c = Animal('dog', name='Roxie')\r\n\r\nWhen instances of the class ``Animal`` are now created, this only happens for\r\nthe *first instantiation per key value*, the initial name of the animal. For\r\nall subsequent instantiations, this *primary instance per key value* is\r\nreturned. But for each new key value, a new ``Animal`` instance is created\r\nand stored in the internal directory.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_multitonton.py - One unique instance per name\r\n\r\n    # Case 1: decoration @Multiton(key=lambda spec, name: name)\r\n    print(a)                        # Animal('dog', 'Teddy')\r\n    print(b)                        # Animal('cat', 'Molly')\r\n    print(c)                        # Animal('dog', 'Roxie')\r\n\r\nWith three different names, a separate instance is created in each case.\r\nIn contrast, the following variant distinguishes only two types (equivalence\r\nclasses): animals with a character 'y' in their name and those without and\r\nthus the key values can only be ``True`` or ``False``.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_multitonton.py - One unique instance per equivalence class\r\n\r\n    # Case 2: decoration @Multiton(key=lambda spec, name: 'y' in name)\r\n    print(a)                        # Animal('dog', 'Teddy')\r\n    print(b)                        # Animal('dog', 'Teddy')\r\n    print(c)                        # Animal('dog', 'Roxie')\r\n\r\nThe initial parameter values of the initializer can also be accessed by their\r\n``args``-index or ``kwargs``-name. So the following decorations are also\r\npossible:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_multitonton.py - Alternative decoration examples\r\n\r\n    # Case 3: One unique instance per specie\r\n    @Multiton(key=\"{0}\".format)     # spec is args[0]\r\n    class Animal:\r\n        pass                        # Some code ...\r\n\r\n    # Case 4: One unique instance per name\r\n    @Multiton(key=\"{name}\".format)  # name is kwargs['name']\r\n    class Animal:\r\n        pass                        # Some code ...\r\n\r\n    # Case 5: One unique instance for all init values, i.e. no duplicates\r\n    @Multiton(key=lambda spec, name: (spec, name))\r\n    class Animal:\r\n        pass                        # Some code ...\r\n\r\n    # Case 6: One unique instance from a @staticmethod or @classmethod\r\n    @Multiton(key=F(\"my_key\"))      # Late binding with F(classmethod_string)\r\n    class Animal:\r\n        pass                        # Some code ...\r\n\r\n        @classmethod\r\n        def my_key(cls, spec, name):\r\n            return 'y' in name\r\n\r\nTo actively control access to new equivalence classes, ``Multiton`` provides\r\nthe ``seal()``, ``unseal()``, and ``issealed()`` methods for sealing, unsealing,\r\nand checking the sealing state of the ``Multiton``. By default, the sealing\r\nstate is set ``False``, so for every new key a new (unique) object is\r\ninstantiated. When sealed (e.g. later in the process) is set ``True`` the\r\ndictionary has completed, i.e. restricted to the current object set and\r\nany new key raises a ``KeyError``.\r\n\r\nIn situations where it might be useful to reset the multiton to express in\r\ncode that instances are often retrieved but rarely modified, setting the\r\ndecorator parameter ``resettable=True`` will expose the ``reset()`` method,\r\nby means of which the internal directory of instances can be completely cleared.\r\n\r\nLast but not least, ``Multiton`` provides a ``instances`` property and\r\nassociated getter and setter methods to directly retrieve the internal\r\ndictionary of primary instances. It is obvious that manipulations on this\r\ndirectory can corrupt the functionality of the multiton, but sometimes it\r\nis useful to have the freedom of access.\r\n\r\n    **Hint** --- Changes affecting key values of classified objects\r\n\r\n    *Classifications into the multiton directory are done only once on\r\n    initial key data. Subsequent changes affecting a key value are not\r\n    reflected in the multiton directory key, i.e. the directory may then be\r\n    corrupted by such modifications.*\r\n\r\n    *Therefore,* **never change key related values of classified objects!**\r\n\r\nAll these things taken together could give the following exemplary picture:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_multitonton.py - seal, unseal, reset, get_instance\r\n\r\n    # Case 7: with decoration @Multiton(key=lambda spec, name: name,\r\n    #                                   resettable=True)\r\n    Animal.reset()                  # Because of resettable=True\r\n    print(Animal.get_instances())   # {}\r\n    print(Animal.issealed())        # False     (=default)\r\n    Animal('dog', name='Teddy')     # Animal('dog', 'Teddy')\r\n    print(Animal.get_instances())   # {'Teddy': Animal('dog', 'Teddy')}\r\n    Animal.seal()                   # Seal the multiton!\r\n    print(Animal.issealed())        # True\r\n    try:                            # Try to..\r\n        Animal('cat', name='Molly') # .. add a new animal\r\n    except  KeyError as ex:         # .. will fail\r\n        print(f\"Sorry {ex.args[1]}, {ex.args[0]}\")\r\n    print(Animal.get_instances())   # {'Teddy': Animal('dog', 'Teddy')}\r\n    Animal.unseal()                 # Unseal the multiton!\r\n    print(Animal.issealed())        # False\r\n    Animal('cat', name='Molly')     # Now, Molly is added\r\n    print(Animal.get_instances())   # {'Teddy': Animal('dog', 'Teddy'),\r\n                                    #  'Molly': Animal('cat', 'Molly')}\r\n    Animal.get_instances().pop('Teddy')\r\n    print(Animal.get_instances())   # {'Molly': Animal('cat', 'Molly')}\r\n    Animal.get_instances().clear()  # Same as Animal.reset()\r\n    print(Animal.get_instances())   # {}\r\n\r\nThe last two lines show the functional equivalence of\r\n``Animal.get_instances().clear()`` with ``Animal.reset()``, but the ``reset``\r\noption is more transparent because it is not necessary to look\r\n\"behind the scenes\".\r\n\r\nWithin the main process of Python's Global Interpreter Lock (GIL),\r\n**Multiton is thread-safe**. In example, using a ``ThreadPoolExecutor``\r\nthreadsafety can be easily demonstrated with sample code like this:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_multiton.py - threadsafety\r\n\r\n    from decoratory.multiton import Multiton\r\n    from concurrent.futures import ThreadPoolExecutor, as_completed\r\n\r\n    @Multiton(key=lambda spec, name: spec)\r\n    class Animal:\r\n        def __init__(self, spec, name):\r\n            self.spec = spec\r\n            self.name = name\r\n\r\n        def __repr__(self):\r\n            return f\"{self.__class__.__name__}('{self.spec}', '{self.name}')\"\r\n\r\n    # Create Instances\r\n    pets = [('dog', 'Teddy'), ('dog', 'Roxie'),    # dogs\r\n            ('cat', 'Molly'), ('cat', 'Felix')]    # cats\r\n    with ThreadPoolExecutor(max_workers=2) as tpe:\r\n        futures = [tpe.submit(Animal, *pet) for pet in pets]\r\n\r\n    # Case 8: Decoration using spec: @Multiton(key=lambda spec, name: spec)\r\n    for future in futures:          # Same instance per spec (=key), i.e.\r\n        instance = future.result()  # Animal('dog', 'Teddy') - for all dogs\r\n        print(instance)             # Animal('cat', 'Molly') - for all cats\r\n\r\nPer type of animal (``key = spec``) always the same instance is presented,\r\nmost likely ``Animal('Teddy')`` for all dogs and ``Animal('cat', 'Molly')``\r\nfor all cats, resulting from the first submitted thread per species, but it\r\ncould also be any of the others.\r\n\r\n\r\n******************************************************************************\r\nWrapper\r\n******************************************************************************\r\n\r\nAs the name implies, a wrapper encloses the original function with an\r\n\r\n* (optional) ``before`` call functionality\r\n\r\nand/or an\r\n\r\n* (optional) ``after`` call functionality.\r\n\r\nThis implementation additionally supports an\r\n\r\n* (optional) ``replace`` call functionality.\r\n\r\nThis generic Wrapper is all the more broadly applicable, the more flexibly\r\nthese three activities can be formulated. All three decorator parameters,\r\n``before``, ``after`` and ``replace``, can be combined with each other and\r\nsupport both single callables and (nested) lists of ``F``-types\r\n(imported from module decoratory.basic, see `F signature`_ below for details).\r\nIn addition, ``replace`` supports passing a result object from successive\r\nreplacement calls through an optional keyword argument named ``result`` with\r\na defaut value, e.g. ``result=None``.\r\n\r\nEven without any of these arguments, such an *empty wrapper* can be used\r\nto *overwrite* default values, for example.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_wrapper.py - overwrite default parameter values\r\n\r\n    from decoratory.wrapper import Wrapper\r\n\r\n    # Case 1: Dynamic decoration with decorator arguments, only\r\n    def some_function(value: str = \"original\"):\r\n        print(f\"value = '{value}'\")\r\n\r\n    # Function call with default parameters\r\n    some_function()                 # value = 'original'\r\n    some_function = Wrapper(some_function, value=\"changed\")\r\n    some_function()                 # value = 'changed'\r\n\r\nThe functionality of ``some_function()`` itself remains unchanged.\r\nFor the sake of clarity, the principle of *all or nothing* is applied, i.e.\r\ndefaults must be defined for all parameters and they are only used if no\r\ncurrent parameters at all are transmitted. There is no mixing of current and\r\ndefault parameters. Thus, even a call of the decorated function with an\r\nincomplete parameter set is explicitly not supported and will throw a\r\n``TypeError``.\r\n\r\nA typical scenario for a wrapper is, of course, the execution of additional\r\nfunctionality before and/or after a given functionality, which itself remains\r\nunchanged, such as ``enter/leave`` markers, call data caches, runtime\r\nmeasurements, etc. Here is a typical example:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_wrapper.py - enclose original function\r\n\r\n    from decoratory.wrapper import Wrapper\r\n    from decoratory.basic import F\r\n\r\n    # Case 2: Decoration with before and after functionalities\r\n    def print_message(message: str = \"ENTER\"):\r\n        print(message)\r\n\r\n    @Wrapper(before=print_message, after=F(print_message, \"LEAVE\"))\r\n    def some_function(value: str = \"original\"):\r\n        print(f\"value = '{value}'\")\r\n\r\n    some_function()                 # ENTER\r\n                                    # value = 'original'\r\n                                    # LEAVE\r\n\r\n.. _F signature:\r\n\r\nWhile ``before`` calls ``print_message`` with its default parameters the\r\n``after`` parameter uses the ``F``-function from ``decoratory.basic``.\r\nIt has a signature ``F(callable, *args, **kwargs)`` and encapsulates the\r\npassing of any function with optional positional and keyword parameters.\r\nAccordingly, the keyword parameter ``after=F(print_message, message=\"LEAVE\")``\r\nwould also be possible.\r\n\r\nThe idea behind the ``replace`` option is not so much to replace the complete\r\noriginal functionality, because you could simply create your own functionality\r\nfor that but to wrap the original functionality, e.g. according to the principle:\r\n\r\n#. Edit and/or prepare the call parameters for the original functionality\r\n#. Execute the original functionality with these modified call parameters\r\n#. Edit and/or revise the result and return this modified result\r\n\r\nAll this together could then look like this:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_wrapper.py - enclose and replacing original function\r\n\r\n    # Case 3: Decoration with replace functionality\r\n    def replace_wrapper(value: str=\"replace\"):\r\n        # 1. Edit the call parameters for the original functionality\r\n        value = value.upper()\r\n        # 2. Execute original functionality with modified call parameters\r\n        result = some_function.substitute.callee(value)             # (1)\r\n        # 3. Edit the result and return this modified result\r\n        return f\"result: '{result}'\"\r\n\r\n    @Wrapper(replace=replace_wrapper)\r\n    def some_function(value: str = \"original\"):\r\n        print(f\"value = '{value}'\")\r\n        return value\r\n\r\n    result = some_function()        # value = 'REPLACE'\r\n    print(result)                   # result: 'REPLACE'\r\n\r\nThe first output ``value = 'REPLACE'`` comes from the original function\r\n``some_function()`` but using parameters  modified to uppercase letters\r\nby the``replace_wrapper()``. The second line ``result: 'REPLACE'`` is the\r\nresult of the ``return`` modified by the ``replace_wrapper()``. Please note\r\nthe line marked with ``(1)`` in the ``replace_wrapper()``: It is very\r\nimportant to avoid self-recursions:\r\n\r\n    **Hint** --- Avoidance of self-recursion in the replace wrapper\r\n\r\n    *In the replace wrapper, the undecorated version of the original\r\n    functionality must always be called. It is accessible via the*\r\n    ``substitute.callee`` *method of the wrapper!*\r\n\r\nFor the sake of completeness, a rather more complex example illustrates\r\nthe replacement of the original functionality with a sequence of replacement\r\nfunctionalities, passing a ``result`` object of type ``int`` between\r\nsuccessive calls.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_wrapper.py - enclose and replacing original function\r\n\r\n    # Case 4: Decoration with before, after and multiple replacements\r\n    def print_message(message: str = \"UNDEFINED\"):\r\n        print(message)\r\n\r\n    def replacement_printer(add: int = 1, *, result=None):\r\n        result += add if isinstance(result, int) else 0\r\n        print(f\"result = {result}\")\r\n        return result\r\n\r\n    @Wrapper(before=F(print, \"ENTER\"), # Python's print()\r\n             replace=[F(replacement_printer, 1, result=0),\r\n                      F(replacement_printer, 3),\r\n                      F(replacement_printer, 5)],\r\n             after=F(print_message, \"LEAVE\"))\r\n    def result_printer(message: str = \"UNKNOWN\"):\r\n        print(message)\r\n\r\n    result_printer()                # ENTER         (before)\r\n                                    # result = 1    (replacement_printer, 1)\r\n                                    # result = 4    (replacement_printer, 3)\r\n                                    # result = 9    (replacement_printer, 5)\r\n                                    # LEAVE         (after)\r\n                                    # 9             (output default_printer)\r\n\r\nThe absence of the outputs of ``UNDEFINED`` and ``UNKNOWN`` reflects the\r\ncorrect replacements by the decoration, and the order of execution is exactly\r\nas expected: ``before`` then ``replace`` then ``after`` and in each of these\r\nvariables the lists are processed in ascending order.\r\n\r\nThe *decoration of a class* always refers to the initializer of the class, e.g.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_wrapper.py - class decoration\r\n\r\n    @Wrapper(before=F(print, \"BEFORE init\"), after=F(print, \"AFTER init\"))\r\n    class Animal:\r\n        def __init__(self, name):\r\n            self.name = name\r\n            print(\"RUNNING init\")\r\n\r\n    # Case 5: Decoration of a class always refers to __init__\r\n    a = Animal(name='Teddy')        # BEFORE init\r\n                                    # RUNNING init\r\n                                    # AFTER init\r\n\r\n\r\nFor all other methods applies:\r\n\r\n    **Hint** --- Dynamic versus static decoration\r\n\r\n    *Decorations to* ``@staticmethod`` *or* ``@classmethod`` *can be done\r\n    analogously to the function decorations above, since they already exist\r\n    at compile time. Instance methods, on the other hand, do not exist until\r\n    an object instance is created and must be decorated dynamically as an\r\n    instance (e.g. see*  `Instance Decoration`_ *below).*\r\n\r\nWith ``Wrapper`` and custom service functions, a *private wrapper library*\r\ncan be built and reused.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_wrapper.py - private wrapper library\r\n\r\n    # Case 6: Define a private wrapper library\r\n    before_wrapper = Wrapper(before=F(print, \"BEFORE\"))\r\n    after_wrapper = Wrapper(after=F(print, \"AFTER\"))\r\n\r\n    # Multiple decorations for specialized functionality encapsulation\r\n    @before_wrapper\r\n    @after_wrapper\r\n    def some_function(value: str = \"original\"):\r\n        print(f\"value = '{value}'\")\r\n\r\n    some_function()                 # BEFORE\r\n                                    # value = 'original'\r\n                                    # AFTER\r\n\r\n\r\n******************************************************************************\r\nObserver\r\n******************************************************************************\r\n\r\nThe `observer pattern`_ is generally used to inform one or more registered\r\nobjects (observers, subscribers, objects) about selected actions of an\r\nobserved object (observable, publisher, subject).\r\n\r\nThe time of activation is set to ``AFTER`` by default, i.e. the observable\r\nperforms its own activity and then activates all registered observers in the\r\nspecified order. This setting can be adjusted to before, after, both or even\r\nno activation at all via the parameter ``activate`` of ``Observable``.\r\n\r\nThis implementation provides several ways to decorate a function or class\r\nas an observable or observer.\r\n\r\n* `Observable Decoration`_\r\n* `Observer Decoration`_\r\n* `Class Decoration`_\r\n* `Instance Decoration`_\r\n\r\n\r\nObservable Decoration\r\n---------------------\r\n\r\nThe simplest and at the same time the most pythonic variant of decoration\r\nis to *decorate only the observed entities as an* ``Observable``.\r\n\r\nThis is possible because all observer pattern functionalities are concentrated\r\nin the ``Observable.BaseClass = BaseObservable`` of the observable decorator,\r\nwhile the ``Observer.BaseClass = BaseObserver`` of the observer decorator\r\nremains empty here. If necessary, it is possible to inherit from both\r\nBaseClasses to modify their functionalities.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - observable decoration\r\n\r\n    from decoratory.observer import Observable\r\n    from decoratory.basic import F\r\n\r\n    def person(say: str = \"Hello?\"):\r\n        print(f\"{person.__name__} says '{say}'\")\r\n\r\n    @Observable(observers=F(person, 'Hey, dog!'))\r\n    def dog(act: str = \"Woof!\"):\r\n        print(f\"{dog.__name__} acts '{act}'\")\r\n\r\n    # Case 1: Observable decoration\r\n    #    ---> Person as an observer to observable dog\r\n    person()                        # person says 'Hello?'    (person acting)\r\n    dog()                           # dog acts 'Woof!'        (dog acting)\r\n                                    # person says 'Hey, dog!' (observer to dog)\r\n\r\nObviously, the addressed observer, the person, must be declared before\r\nthe observed dog. With the simpler decoration\r\n``@Observable(observers=person)`` the person would always respond with their\r\ndefault action and say ``'Hello?'``. The usage of ``F`` enables the transfer\r\nof individual parameters to the observer.\r\n\r\nDue to hierarchies in stacked observer patterns, a more detailed management\r\nof observed vs. observing objects may be necessary.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - observable decoration\r\n\r\n    def person(say: str = \"Hello?\"):\r\n        print(f\"{person.__name__} says '{say}'\")\r\n\r\n    @Observable(observers=F(person, 'Hey, cat!'))\r\n    def cat(act: str = \"Meow!\"):\r\n        print(f\"{cat.__name__} acts '{act}'\")\r\n\r\n    @Observable(observers=[F(cat, 'Roar!'), F(person, 'Hey, dog!')])\r\n    def dog(act: str = \"Woof!\"):\r\n        print(f\"{dog.__name__} acts '{act}'\")\r\n\r\n    # Case 2: Stacked observable decoration\r\n    #    ---> Cat observes dog, person observes cat and dog\r\n    person()                        # person says 'Hello?'    (person acting)\r\n\r\n    cat()                           # cat acts 'Meow!'        (cat acting)\r\n                                    # person says 'Hey, cat!' (observer to cat)\r\n\r\n    dog()                           # dog acts 'Woof!'        (dog acting)\r\n                                    # cat acts 'Roar!'        (observer to dog)\r\n                                    # person says 'Hey, cat!' (observer to cat)\r\n                                    # person says 'Hey, dog!' (observer to dog)\r\n\r\nPerson is an observer, but not an observable, so the call to ``person()``\r\nreflects only person\u00e2\u20ac\u2122s own activity.\r\nCat is an observable that is observed by person and therefore the activity\r\n``cat()`` triggers a follow-up activity by person.\r\nCalling ``dog()`` results in three activities at the observers, because\r\n``dog()`` is observed by the *observed cat*, which informs the person about\r\nits own activity.\r\n\r\nThe order of reactions is determined by the order in the list in which\r\nthe cat observes the dog prior to the person. If this order is reversed:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - observable decoration\r\n\r\n    @Observable(observers=[F(person, 'Hey, dog!'), F(cat, 'Roar!')])\r\n    def dog(act: str = \"Woof!\"):\r\n        print(f\"{dog.__name__} acts '{act}'\")\r\n\r\n    # Case 3: Stacked observable decoration\r\n    #    ---> Cat observes dog, person observes dog and cat\r\n    dog()                           # dog acts 'Woof!'        (dog acting)\r\n                                    # person says 'Hey, dog!' (observer to dog)\r\n                                    # cat acts 'Roar!'        (observer to dog)\r\n                                    # person says 'Hey, cat!' (observer to cat)\r\n\r\nAgain, calling ``dog()`` results in three activities at the observers,\r\nbut here person reacts first as an observer to dog and later again as an\r\nobserver to cat.\r\n\r\nIf this behavior is not desired, ``dog()`` can instead address the\r\n*original cat* using the ``cat.substitute.callee``, i.e.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - observable decoration\r\n\r\n    @Observable(observers=[F(cat.substitute.callee, 'Roar!'),\r\n                           F(person, 'Hey, dog!')])\r\n    def dog(act: str = \"Woof!\"):\r\n        print(f\"{dog.__name__} acts '{act}'\")\r\n\r\n    # Case 4: Stacked observable decoration\r\n    #    ---> Original cat observes dog, person observes dog and cat\r\n    dog()                           # dog acts 'Woof!'        (dog acting)\r\n                                    # cat acts 'Roar!'        (observer to dog)\r\n                                    # person says 'Hey, dog!' (observer to dog)\r\n\r\nIn this case, cat acts before person because of the order of the observer\r\nlist and because the *original cat* observes dog the ``Hey, cat!`` statement\r\nof person is missing.\r\n\r\n\r\nObserver Decoration\r\n-------------------\r\n\r\nIn this reversed decoration scheme, the observer decorator collects its\r\nobservables. This seems more elaborate at first glance, but some prefer to\r\nexplicitly designate the ``Observer`` and ``Observable`` roles in their code.\r\n\r\nBecause an observer decoration uses observable methods, all\r\nobservable(s) must always be *declared and decorated* before their\r\nobserver(s).\r\n\r\n    **1. Rule:** Declare *Observables before Observers*\r\n\r\n    **2. Rule:** Decorating as *@Observable* before using in an *@Observer*\r\n\r\nThus, the initial example ``Case 1`` from `Observable Decoration`_ translates to:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - observer decoration\r\n\r\n    from decoratory.observer import Observer, Observable\r\n    from decoratory.basic import X\r\n\r\n    @Observable\r\n    def dog(act: str = \"Woof!\"):    # 1. Rule: declare dog before person!\r\n        print(f\"{dog.__name__} acts '{act}'\")\r\n\r\n    @Observer(observables=X(dog, 'Hey, dog!'))\r\n    def person(say: str = \"Hello?\"):\r\n        print(f\"{person.__name__} says '{say}'\")\r\n\r\n    # Case 1: Observer decoration\r\n    #    ---> Person as an observer to observable dog\r\n    person()                        # person says 'Hello?'\r\n    dog()                           # dog acts 'Woof!'        (dog acting)\r\n                                    # person says 'Hey, dog!' (observer to dog)\r\n\r\nThe use of the *semantic cross-function* ``X`` from ``decoratory.basic``\r\ninstead of ``F`` indicates that ``dog`` is the observable, but the ``X``\r\narguments apply for the observer ``person``.\r\n\r\nFor multiple decorations, the *order of decoration* is also relevant here.\r\nThe situation ``Case 2`` from `Observable Decoration`_ with person,\r\ndog and cat would then look like:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - observer decoration\r\n\r\n    @Observable                     # 2. Rule: dog before cat & person\r\n    def dog(act: str = \"Woof!\"):    # 1. Rule: dog before cat & person\r\n        print(f\"{dog.__name__} acts '{act}'\")\r\n\r\n    @Observer(observables=X(dog, 'Roar!'))\r\n    @Observable                     # 2. Rule: observable cat before person\r\n    def cat(act: str = \"Meow!\"):    # 1. Rule: cat before person\r\n        print(f\"{cat.__name__} acts '{act}'\")\r\n\r\n    @Observer(observables=[X(dog, 'Hey, dog!'),\r\n                           X(cat.substitute.callee, say='Hey, cat!')])\r\n    def person(say: str = \"Hello?\"):\r\n        print(f\"{person.__name__} says '{say}'\")\r\n\r\n    # Case 2: Stacked observer decoration\r\n    #    ---> Cat observes dog, person observes cat and dog\r\n    person()                        # person says 'Hello?'    (person acting)\r\n\r\n    cat()                           # cat acts 'Meow!'        (cat acting)\r\n                                    # person says 'Hey, cat!' (observer to cat)\r\n\r\n    dog()                           # dog acts 'Woof!'        (dog acting)\r\n                                    # cat acts 'Roar!'        (observer to dog)\r\n                                    # person says 'Hey, cat!' (observer to cat)\r\n                                    # person says 'Hey, dog!' (observer to dog)\r\n\r\nHere, cat becomes an observer but its callee ``cat.substitute.callee`` is an\r\nobservable which can be observed by person! This *observed cat* observes\r\nthe dog, reacts and triggers the person.\r\n\r\nTo reproduce ``Case 4`` from above, simply swap the order of the decorations\r\nat the cat and the dog then is observed by the *original cat*.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - observer decoration\r\n\r\n    @Observable                     # 2. Rule: dog before cat & person\r\n    def dog(act: str = \"Woof!\"):    # 1. Rule: dog before cat & person\r\n        print(f\"{dog.__name__} acts '{act}'\")\r\n\r\n    @Observable                     # 2. Rule: cat before person\r\n    @Observer(observables=X(dog, 'Roar!'))\r\n    def cat(act: str = \"Meow!\"):    # 1. Rule: cat before person\r\n        print(f\"{cat.__name__} acts '{act}'\")\r\n\r\n    @Observer(observables=[X(dog, 'Hey, dog!'), X(cat, say='Hey, cat!')])\r\n    def person(say: str = \"Hello?\"):\r\n        print(f\"{person.__name__} says '{say}'\")\r\n\r\n    # Case 3: Stacked observer decoration\r\n    #    ---> Cat observes dog, person observes cat and dog\r\n    person()                        # person says 'Hello?'    (person acting)\r\n\r\n    cat()                           # cat acts 'Meow!'        (cat acting)\r\n                                    # person says 'Hey, cat!' (observer to cat)\r\n\r\n    dog()                           # dog acts 'Woof!'        (dog acting)\r\n                                    # cat acts 'Roar!'        (observer to dog)\r\n                                    # person says 'Hey, dog!' (observer to dog)\r\n\r\nNow, both dog and cat end up being observables, observed by the person. But the\r\ncat observing the dog is the *original cat*, which does not inform the person\r\nabout its activities, and so person\u00e2\u20ac\u2122s statement ``Hey, cat!`` is missing.\r\n\r\n\r\nClass Decoration\r\n----------------\r\n\r\nBoth techniques, `Observable Decoration`_ and `Observer Decoration`_,\r\nare static, in the sense, decorations are done e.g. in @-notation evaluated\r\nat compile time. They are applied to *static functions*.\r\n\r\n*Decoration of a class* by default addresses decoration of the\r\n*class initializer*, this means\r\n\r\n.. code-block:: python\r\n\r\n    @Observable\r\n    class Dog:\r\n        def __init__(self):\r\n            pass                    # Some code ...\r\n\r\nshould be understood as\r\n\r\n.. code-block:: python\r\n\r\n    class Dog:\r\n        @Observable\r\n        def __init__(self):\r\n            pass                    # Some code ...\r\n\r\nBut this behavior is insidious, e.g.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - class decoration\r\n\r\n    from decoratory.observer import Observable\r\n\r\n    class Person:\r\n        def __init__(self, name: str = \"Jane Doe\"):\r\n            print(f\"{name} arrived.\")\r\n\r\n    @Observable(observers=Person)\r\n    class Dog:\r\n        def __init__(self, name: str = \"Teddy\"):\r\n            print(f\"Dog {name} arrived.\")\r\n\r\n    # Case 1: Dog is an observable to Person\r\n    prs = Person()                  # Jane Doe arrived.\r\n    dog = Dog()                     # Dog Teddy arrived.\r\n                                    # Jane Doe arrived.\r\n\r\nThe instantiation of ``Dog`` induced an instantiation of ``Person``.\r\n\r\n    **Hint** --- Take care when decorating a class initializer\r\n\r\n    *Notifying the* ``__init__`` *method of an observer results in a new\r\n    instance! This means calling the observable induces instantiation of\r\n    a new observer object, surely in not any case this is the desired\r\n    behavior ...*\r\n\r\nSo the decoration should not address a class but one (or more) target\r\nmethods of the class. As already mentioned, this is easy if this callback\r\nfunction is a ``@staticmethod`` or ``@classmethod``.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - class decoration\r\n\r\n    class Person:\r\n        def __init__(self, name: str = \"Jane Doe\"):\r\n            print(f\"{name} arrived.\")\r\n\r\n        @staticmethod\r\n        def action1(act: str = \"Hello?\"):\r\n            print(f\"Person says {act}\")\r\n\r\n        @classmethod\r\n        def action2(cls, act: str = \"What's up?\"):\r\n            print(f\"Person says {act}\")\r\n\r\n    @Observable(observers=[Person.action1, Person.action2])\r\n    class Dog:\r\n        def __init__(self, name: str = \"Teddy\"):\r\n            print(f\"Dog {name} arrived.\")\r\n\r\n    # Case 2: Dog is an observable to Person.action\r\n    prs = Person()                  # Jane Doe arrived.\r\n    dog = Dog()                     # Dog Teddy arrived.\r\n                                    # Person says Hello?\r\n                                    # Person says What's up?\r\n\r\nThis is how it usually works: *one action of the observable*, here it's\r\nthe instantiation of ``Dog``, triggers *one to many actions at each observer*,\r\nhere ``Person``.\r\n\r\n.. _Class Decoration, Case 3:\r\n\r\nBut often an instance method is also interesting as a callback function:\r\n\r\n- If a *particular instance* ``prs = Person(name=\"John Doe\")`` of a\r\n  person is meant, a decoration like ``@Observable(observers=prs.action)``\r\n  with the *instance method* can be applied to ``Dog``.\r\n- For *any instance* of a person ``@Observable(observers=Person().action)``\r\n  works.\r\n\r\nEven a list of ``F`` structures would be possible to optionally submit\r\ndifferent parameters.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - class decoration\r\n\r\n    from decoratory.observer import Observable\r\n    from decoratory.basic import F\r\n\r\n    class Person:\r\n        def __init__(self, name: str = \"Jane Doe\"):\r\n            self.name = name\r\n            print(f\"{name} arrived.\")\r\n\r\n        def action(self, act: str = \"Hello?\"):\r\n            print(f\"{self.name} says {act}\")\r\n\r\n    prs1 = Person()                 # Jane Doe arrived.\r\n    prs2 = Person(\"John Doe\")       # John Doe arrived.\r\n\r\n    @Observable(observers=[prs1.action, F(prs2.action, \"What's up?\")])\r\n    class Dog:\r\n        def __init__(self, name: str = \"Teddy\"):\r\n            print(f\"Dog {name} arrived.\")\r\n\r\n    # Case 3: Dog is an observable to actions of various person instances.\r\n    dog = Dog()                     # Dog Teddy arrived.\r\n                                    # Jane Doe says Hello?\r\n                                    # John Doe says What's up?\r\n\r\nBut here, *one action of the observable*, the instantiation of ``Dog``, triggers\r\n*one to many actions at each selected resp. instantiated observer*, ``Person``.\r\nIn such situations, a late `dynamic decoration <#dynamic-decoration>`_\r\ncould be a good idea.\r\n\r\nSo far, instantiating ``Dog`` resulted in an information and induced\r\naction at ``Person``. If ``Dog`` has its own actions that need to be\r\nselectively monitored, each of the selected actions can of course be decorated\r\nindividually as an ``Observable``. For the sake of a better overview, this\r\ncan also be done on the class itself.\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - class decoration\r\n\r\n    class Person:\r\n        def __init__(self, name: str = \"Jane Doe\"):\r\n            self.name = name\r\n            print(f\"{name} arrived.\")\r\n\r\n        @classmethod\r\n        def actionA(cls, act: str = \"Hello?\"):\r\n            print(f\"Person says {act}\")\r\n\r\n        def actionB(self, act: str = \"Hello?\"):\r\n            print(f\"{self.name} says {act}\")\r\n\r\n    @Observable(methods=[\"action1\", \"action2\"],\r\n                observers=[Person.actionA, Person(\"Any Doe\").actionB])\r\n    class Dog:\r\n        def __init__(self, name: str = \"Teddy\"):\r\n            self.name = name\r\n            print(f\"Dog {name} arrived.\")\r\n\r\n        @staticmethod\r\n        def action1(act: str = \"Woof!\"):\r\n            print(f\"Dog acts {act}\")\r\n\r\n        def action2(self, act: str = \"Brrr!\"):\r\n            print(f\"{self.name} acts {act}\")\r\n\r\n    # Case 4: Dog is an observable with selected actions.\r\n                                    # Any Doe arrived.\r\n    prs = Person()                  # Jane Doe arrived.\r\n    dog = Dog()                     # Dog Teddy arrived.\r\n\r\n    dog.action1()                   # Dog acts Woof!        (@staticmethod)\r\n                                    # Person says Hello?    (@classmethod)\r\n                                    # Any Doe says Hello?   (Instance 'Any')\r\n\r\n    Dog.action2(dog)                # Teddy acts Brrr!      (Instance 'Teddy')\r\n                                    # Person says Hello?    (@classmethod)\r\n                                    # Any Doe says Hello?   (Instance 'Any')\r\n\r\nThe last line ``Dog.action2(dog)`` provides the instance of ``Teddy`` as *the\r\nfirst argument* ``self``. This works because internally the *class method*\r\n``Dog.action2`` was registered instead of an instance method that didn't\r\nexist at compile time. On the other hand, the call ``dog.action2()`` would\r\nfail because this *instance method* was not registered. But, if this is what\r\nis to be achieved, an instance method can first be created and registered,\r\njust as seen above in `Class Decoration, Case 3`_.\r\n\r\n\r\nInstance Decoration\r\n-------------------\r\n\r\nThe classic way to exchange information between objects with the observer\r\npattern is through the active use of the ``register``, ``dispatch``, and\r\n``unregister`` *interface methods that an observable exposes*. Information can\r\nbe given to the right recipients at relevant places in the code. For this,\r\nthe classes are not decorated and `dynamic decoration <#dynamic-decoration>`_\r\ncomes into play. Dynamic decoration is used often also in connection with\r\ngetter/setter/property constructions since data changes take place\r\nmeaningfully over these methods.\r\n\r\nConsider the following two example classes:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - instance decoration\r\n\r\n    class Note:                             # Observer without decoration!\r\n        def info(self, thing):\r\n            print(f\"Note.info: val = {thing.a}\")\r\n\r\n    class Thing:                            # Observable without decoration!\r\n        def __init__(self, a=0):            # Initializer, defining variabe 'a'\r\n            self._a = a\r\n        def inc(self):                      # Instance method, modifying 'a'\r\n            self._a += 1\r\n        def get_a(self):                    # Getter, setter, property,\r\n            return self._a                  # modifying variable 'a'\r\n        def set_a(self, value):\r\n            self._a = value\r\n        a = property(get_a, set_a)\r\n\r\nInitially, all these classes are undecorated and typical actions might be:\r\n\r\n.. code-block:: python\r\n\r\n    # *** example_observer.py - instance decoration\r\n\r\n    from decoratory.observer import Observable\r\n    from decoratory.basic import F\r\n\r\n    # (1) Setup instances\r\n    nti = Note()                    # Note instance\r\n    tgi = Thing()                   # Thing instance\r\n\r\n    # (2) Dynamic decoration of some methods: Late binding\r\n    tgi.inc = Observable(tgi.inc)           # Late method decoration\r\n    Thing.set_a = Observable(Thing.set_a)   # Late property decoration\r\n    Thing.a = property(Thing.get_a, Thing.set_a)\r\n\r\n    # (3) Register the observer (Note) with the observable (Thing)\r\n    tgi.inc.observable.register(F(nti.info, tgi))\r\n    tgi.set_a.observable.register(F(nti.info, thing=tgi))\r\n\r\n    # Case 1: Change self.a = 0 using inc()\r\n    tgi.inc()                       # Note.info: val = 1\r\n\r\n    # Case 2: Change self.a = 1 using setter via property\r\n    tgi.a = 2                       # Note.info: val = 2\r\n\r\n    # Case 3: Notification from inc() to nti.info() about Thing(3)\r\n    tgi.inc.observable.dispatch(nti.info, Thing(3))\r\n                                    # Note.info: val = 3\r\n\r\n    # Case 4: Notification from set_a() to nti.info() about Thing(4)\r\n    tgi.set_a.observable.dispatch(nti.info, Thing(4))\r\n                                    # Note.info: val = 4\r\n\r\n    # Case 5: Print the current value of tgi.a\r\n    print(f\"a = {tgi.a}\")           # a = 2     (no changes by notification)\r\n\r\n    # Case 6: Print list of all observers\r\n    print(tgi.inc.observable.observers(classbased=True))\r\n    # ---> {'Note': ['F(info, <__main__.Thing object at ..)']}\r\n    print(tgi.set_a.observable.observers(classbased=True))\r\n    # ---> {'Note': ['F(info, thing=<__main__.Thing object at ..)']}\r\n\r\n    # Case 7: Unregister nti.info from tgi\r\n    tgi.inc.observable.unregister(nti.info)\r\n    print(tgi.inc.observable.observers(classbased=True))    # {}\r\n\r\nIn contrast to `Class Decoration`_, this `Instance Decoration`_\r\n\r\n(1) instantiates the native classes (1), then\r\n(2) decorates the relevant instance methods (2), and then\r\n(3) registers the observers with the associated observables (3).\r\n\r\nThis method of instance decoration is certainly the most flexible.\r\nHowever, it bears the risk of losing track of all dependencies.\r\n\r\n\r\n~~~ `Home <#top>`_ ~~~ `Contents <#toc>`_ ~~~ `Singleton <#singleton>`_ ~~~ `Multiton <#multiton>`_ ~~~ `Wrapper <#wrapper>`_ ~~~ `Observer <#observer>`_ ~~~\r\n\r\n\r\n.. ===========================================================================\r\n.. _Project Homepage: https://decoratory.app/index.html?ref=PyPI\r\n.. _singleton pattern: https://en.wikipedia.org/wiki/Singleton_pattern\r\n.. _multiton pattern: https://en.wikipedia.org/wiki/Multiton_pattern\r\n.. _observer pattern: https://en.wikipedia.org/wiki/Observer_pattern\r\n.. _Decorator Arguments Template: https://decoratory.app/Section/ArgumentsTemplate.html?ref=PyPI\r\n.. _Decorator Implementations: https://decoratory.app/Section/Decorators.html?ref=PyPI\r\n\r\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Python Decorators: Singleton, SemiSingleton, Multiton, Observer, Observable, generic Wrapper.",
    "version": "0.9.9.3",
    "project_urls": {
        "Download": "https://decoratory.app/Section/Download.html?ref=PyPI",
        "Homepage": "https://decoratory.app/index.html?ref=PyPI",
        "Projekt": "https://decoratory.app/index.html?ref=PyPI",
        "Release Notes": "https://decoratory.app/Section/ReleaseNotes.html?ref=PyPI"
    },
    "split_keywords": [
        "python",
        "decorators",
        "semi-singleton",
        "singleton",
        "multiton",
        "observer",
        "observable",
        "wrapper"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "472b1575b22494a70fa0fd00fbc3746b26998e95aa5e397eda9f60cb8c3ce35c",
                "md5": "36827399173bf40cafdc02420d54f145",
                "sha256": "488e18c54cac8870fb331fe3e997dc01e626bfe4b3915ef7ad23926ed4a94157"
            },
            "downloads": -1,
            "filename": "decoratory-0.9.9.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "36827399173bf40cafdc02420d54f145",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 68454,
            "upload_time": "2024-01-07T13:51:07",
            "upload_time_iso_8601": "2024-01-07T13:51:07.345130Z",
            "url": "https://files.pythonhosted.org/packages/47/2b/1575b22494a70fa0fd00fbc3746b26998e95aa5e397eda9f60cb8c3ce35c/decoratory-0.9.9.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "98d058bafb0a651df612c02eb485ff3936d9714e8fdd1607ae4979932d399993",
                "md5": "c7c93afdc9955c87cf6e7914510e7028",
                "sha256": "d39295f682f31703cb561ab75d32dfe67d64cd3d1ef3a4dd41b370c267976170"
            },
            "downloads": -1,
            "filename": "decoratory-0.9.9.3.tar.gz",
            "has_sig": false,
            "md5_digest": "c7c93afdc9955c87cf6e7914510e7028",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 68853,
            "upload_time": "2024-01-07T13:51:10",
            "upload_time_iso_8601": "2024-01-07T13:51:10.239108Z",
            "url": "https://files.pythonhosted.org/packages/98/d0/58bafb0a651df612c02eb485ff3936d9714e8fdd1607ae4979932d399993/decoratory-0.9.9.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-01-07 13:51:10",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "decoratory"
}
        
Elapsed time: 0.25268s