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
GPControllerclasses 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
DecoratorWarningor raise aDecoratorErrorat runtime if the decorator calls theverify_decorated_class()method to ensure that this does not happen. These warnings can be ignored by the user with theignore_methodsorignore_allparameters.- 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.
- 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:
- Raises:
TypeError – If cls is not a subclass of the framework_class.
TopmostDecoratorError – If cls is already decorated with a
TopMostDecorator.MissingRequirementsError – If cls is missing a required decorator.
- Return type:
- class vanguard.decoratorutils.basedecorator.TopMostDecorator(framework_class, required_decorators, ignore_methods=(), ignore_all=False, raise_instead=False)[source]¶
Bases:
DecoratorA 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.
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:
- Return type:
- 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 usingfunctools.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:
- Return type:
- Returns:
A function that wraps the class.