decoratory 0.9.9.3

Creator: bradpython12

Last updated:

Add to Cart

Description:

decoratory 0.9.9.3

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
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.
# *** 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.
# *** 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
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.
# *** 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.
# *** 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.
# *** 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:
# *** 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.
# *** 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.
# *** 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.
# *** 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:
# *** 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:
# *** 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:
# *** 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.
# *** 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:
# *** 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
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:
# *** 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.
# *** 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.
# *** 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.
# *** 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.
# *** 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.
# *** 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:
# *** 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.
# *** 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:
# *** 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:
# *** 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.
# *** 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
@Observable
class Dog:
def __init__(self):
pass # Some code ...
should be understood as
class Dog:
@Observable
def __init__(self):
pass # Some code ...
But this behavior is insidious, e.g.
# *** 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.
# *** 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.
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.
# *** 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
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.
# *** 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
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:
# *** 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:
# *** 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

instantiates the native classes (1), then
decorates the relevant instance methods (2), and then
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 ~~~ Contents ~~~ Singleton ~~~ Multiton ~~~ Wrapper ~~~ Observer ~~~

License

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Customer Reviews

There are no reviews.