Decorator Tools

Tools to support decorators in Vanguard.

In Vanguard, decorators allow for easy, dynamic subclassing of class:~vanguard.base.gpcontroller.GPController instances, to add new functionality in an easily composable way. All new decorators should subclass from class:~basedecorator.Decorator or TopMostDecorator. See Creating a Decorator for more details.

class vanguard.decoratorutils.basedecorator.Decorator(framework_class, required_decorators, ignore_methods=(), ignore_all=False, raise_instead=False)[source]

A base class for a vanguard decorator.

Note

Decorating GPController classes is an extremely practical means of extending functionality. However, many decorators are designed to work with a specific ‘framework class’, and any methods which have been added (or modified) to the decorated class can cause issues which may not be picked up at runtime.

To mitigate this, any unexpected or modified methods (along with any other potential problems that the creator may wish to avoid) will emit a DecoratorWarning or raise a DecoratorError at runtime if the decorator calls the verify_decorated_class() method to ensure that this does not happen. These warnings can be ignored by the user with the ignore_methods or ignore_all parameters.

Example:
>>> from vanguard.base import GPController
>>>
>>> @Decorator(framework_class=GPController, required_decorators=set())
... class NewGPController(GPController):
...     pass
Parameters:
__init__(framework_class, required_decorators, ignore_methods=(), ignore_all=False, raise_instead=False)[source]

Initialise self.

Parameters:
  • framework_class (type[Any]) – All unexpected/overwritten methods are relative to this class.

  • required_decorators (Iterable[type[Decorator]]) – A set (or other iterable) of decorators which must have been applied before (i.e. below) this one.

  • ignore_methods (Iterable[str]) – If these method names are found to have been added or overwritten, then an error or warning will not be raised.

  • ignore_all (bool) – If True, all unexpected/overwritten methods will be ignored.

  • raise_instead (bool) – If True, unexpected/overwritten methods will raise errors instead of emitting warnings.

_decorate_class(cls)[source]

Return a wrapped version of a class.

Parameters:

cls (type[Any])

Return type:

type[Any]

property safe_updates: dict[type, set[str]]

Get a dictionary (class -> set[names]) of overrides/new methods that we consider “safe”.

verify_decorated_class(cls)[source]

Verify that a class can be decorated by this instance.

Parameters:

cls (type[Any]) – The class to be decorated.

Raises:
Return type:

None

class vanguard.decoratorutils.basedecorator.TopMostDecorator(framework_class, required_decorators, ignore_methods=(), ignore_all=False, raise_instead=False)[source]

Bases: Decorator

A specific decorator which cannot be decorated.

Top-most decorators are intended to be just that – decorators which are at the top of the stack. This is often a last resort, when it doesn’t make sense to add any more functionality, and should be used sparingly.

Example:
>>> from typing import Type, TypeVar
>>>
>>> from vanguard.base import GPController
>>> from vanguard.decoratorutils import wraps_class
>>>
>>> ControllerType = TypeVar('ControllerType', bound=GPController)
>>>
>>> class MyDecorator(Decorator):
...     def _decorate_class(self, cls: Type[ControllerType]) -> Type[ControllerType]:
...         @wraps_class(cls)
...         class InnerClass(cls):
...             pass
...         return InnerClass
>>>
>>> class MyTopMostDecorator(TopMostDecorator):
...     def _decorate_class(self, cls: Type[ControllerType]) -> Type[ControllerType]:
...         @wraps_class(cls)
...         class InnerClass(cls):
...             pass
...         return InnerClass
>>>
>>> @MyTopMostDecorator(framework_class=GPController, required_decorators={})
... @MyDecorator(framework_class=GPController, required_decorators={})
... class MyController(GPController):
...     pass
>>>
>>> @MyDecorator(framework_class=GPController, required_decorators={})  
... @MyTopMostDecorator(framework_class=GPController, required_decorators={})
... class MyController(GPController):
...     pass
Traceback (most recent call last):
    ...
vanguard.decoratorutils.errors.TopmostDecoratorError: Cannot decorate this class!
Parameters:

Errors

Errors and warnings corresponding to unstable decorator combinations.

If a decorated class has implemented new functions (or overwritten existing ones) then calling verify_decorated_class() will raise one of these errors or warnings.

exception vanguard.decoratorutils.errors.BadCombinationWarning[source]

This combination of decorators may lead to unexpected issues.

exception vanguard.decoratorutils.errors.DecoratorError[source]

Base class for all decorator errors.

exception vanguard.decoratorutils.errors.DecoratorWarning[source]

Base class for all decorator warnings.

exception vanguard.decoratorutils.errors.MissingRequirementsError[source]

Missing decorator requirements.

exception vanguard.decoratorutils.errors.OverwrittenMethodError[source]

An existing method has been overwritten.

exception vanguard.decoratorutils.errors.OverwrittenMethodWarning[source]

An existing method has been overwritten.

exception vanguard.decoratorutils.errors.TopmostDecoratorError[source]

Attempting to decorate a top-level decorator.

exception vanguard.decoratorutils.errors.UnexpectedMethodError[source]

A new, unexpected method has been implemented.

exception vanguard.decoratorutils.errors.UnexpectedMethodWarning[source]

A new, unexpected method has been implemented.

Wrapping

Wrapping functions for use in Vanguard decorators.

Applying the wraps_class() decorator to a class will update all method names and docstrings with those of the super class. The process_args() function is a helper function for organising arguments to a function into a dictionary for straightforward access.

vanguard.decoratorutils.wrapping.process_args(func, *args, **kwargs)[source]

Process the arguments for a function.

This is just a wrapper on inspect.Signature.bind() that also applies any default arguments and folds any additional kwargs into the returned dictionary.

Note that when passed a bound method, "self" will not be a key in the returned dictionary, and should not be passed as an argument (as a TypeError will be raised).

Conversely, when passed an unbound method, "self" _must_ be passed as an argument if it’s an instance method, and will be included in the returned dictionary.

As such, if you need to use the result of applying this function on an unbound method as an argument list to a bound method, or vice versa, you’ll have to handle the “self” parameter specially.

Example:
>>> class MyClass:
...     def __init__(self, x: int):
...         self.x = x
...     def multiply(self, y: int) -> int:
...         return self.x * y
>>> my_instance = MyClass(x=2)
>>> process_args(my_instance.multiply, y=3)
{'y': 3}
>>> process_args(MyClass.multiply, y=3)
Traceback (most recent call last):
...
TypeError: missing a required argument: 'self'
>>> process_args(MyClass.multiply, my_instance, y=3)  
{'self': <...MyClass object at 0x...>, 'y': 3}
Parameters:
  • func (Callable) – The function for which to process the arguments.

  • args (Any) – Arguments to be passed to the function. Must be passed as args, i.e. process_args(func, 1, 2).

  • kwargs (Any) – Keyword arguments to be passed to the function. Must be passed as kwargs, i.e. process_args(func, c=1).

Return type:

dict[str, Any]

Returns:

A mapping of parameter name to value for all parameters (including default ones) of the function.

Example:
>>> def f(a, b, c=3, **kwargs):
...     pass
>>>
>>> process_args(f, 1, 2)
{'a': 1, 'b': 2, 'c': 3}
>>> process_args(f, a=1, b=2, c=4)
{'a': 1, 'b': 2, 'c': 4}
>>> process_args(f, a=1, b=2, c=4, e=5)
{'a': 1, 'b': 2, 'c': 4, 'e': 5}
>>> process_args(f, *(1,), **{'b': 2, 'c': 4})
{'a': 1, 'b': 2, 'c': 4}
>>> process_args(f, 1)
Traceback (most recent call last):
...
TypeError: missing a required argument: 'b'
vanguard.decoratorutils.wrapping.wraps_class(base_class, *, decorator_source=None)[source]

Update the names and docstrings of an inner class to those of a base class.

This decorator controls the wrapping of an inner class, ensuring that all methods of the final class maintain the same names and docstrings as the inner class. Very similar to functools.wraps().

Note

This decorator will return a class which seems almost identical to the base class, but a __wrapped__ attribute will be added to point to the original class. All methods will be wrapped using functools.wraps().

Example:
>>> import inspect
>>>
>>> class First:
...     '''This is the first class.'''
...     def __init__(self, a, b):
...         pass
>>>
>>> @wraps_class(First)
... class Second(First):
...     '''This is the second class.'''
...     def __init__(self, *args, **kwargs):
...         super().__init__(*args, **kwargs)
>>>
>>> Second.__name__
'First'
>>> Second.__doc__
'This is the first class.'
>>> str(inspect.signature(Second.__init__))
'(self, a, b)'
>>> Second.__wrapped__
<class 'vanguard.decoratorutils.wrapping.First'>
Parameters:
  • base_class (type[Any]) – The base class to wrap.

  • decorator_source (Optional[Decorator]) – If present, any wrapped functions on the class have the attribute __vanguard_wrap_source__ set to this value.

Return type:

Callable[[type[Any]], type[Any]]

Returns:

A function that wraps the class.