decoratory 0.9.9.3
Python Decorators: Singleton, SemiSingleton, Multiton, Observer, Observable, generic Wrapper.
License: MIT
Requires Python: >=3.7
Keywords: python decorators semi-singleton singleton multiton observer observable wrapper


.. _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