Skip to content

Commit

Permalink
Extract generic decorator into a reusable class (#569)
Browse files Browse the repository at this point in the history
The only current use of the decorator pattern in colcon-core is the
argument parser, but the concept can be applied generically in a variety
of scenarios.
  • Loading branch information
cottsay authored Aug 17, 2023
1 parent 18ac286 commit b21dd93
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 38 deletions.
43 changes: 8 additions & 35 deletions colcon_core/argument_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import types
import warnings

from colcon_core.generic_decorator import GenericDecorator
from colcon_core.logging import colcon_logger
from colcon_core.plugin_system import instantiate_extensions
from colcon_core.plugin_system import order_extensions_by_priority
Expand Down Expand Up @@ -85,7 +86,7 @@ def decorate_argument_parser(parser):
return parser


class ArgumentParserDecorator:
class ArgumentParserDecorator(GenericDecorator):
"""
Decorate an argument parser as well as all recursive subparsers.
Expand All @@ -107,15 +108,13 @@ def __init__(self, parser, **kwargs):
instance
"""
assert '_parser' not in kwargs
kwargs['_parser'] = parser
assert '_nested_decorators_' not in kwargs
kwargs['_nested_decorators_'] = []
assert '_group_decorators' not in kwargs
kwargs['_group_decorators'] = []
assert '_recursive_decorators' not in kwargs
kwargs['_recursive_decorators'] = []
for k, v in kwargs.items():
self.__dict__[k] = v
super().__init__(parser, **kwargs)

@property
def _nested_decorators(self): # pragma: no cover
Expand All @@ -125,40 +124,14 @@ def _nested_decorators(self): # pragma: no cover
'deprecated', stacklevel=2)
return self._nested_decorators_

def __getattr__(self, name):
"""
Get an attribute from this decorator if it exists or the decoree.
:param str name: The name of the attribute
:returns: The attribute value
:raises AttributeError: if the attribute doesn't exist in either of the
two instances
"""
# when argcomplete changes self.__class__ at runtime
# the instance might not have a _parser attribute anymore
if '_parser' not in self.__dict__:
raise AttributeError(name)
# get attribute from decoree
return getattr(self.__dict__['_parser'], name)

def __setattr__(self, name, value):
@property
def _parser(self):
"""
Set an attribute value on this decorator if it exists or the decoree.
Get the parser that this instance decorates (the decoree).
:param str name: The name of the attribute
:param value: The attribute value
TODO: Deprecate _parser in favor of _decoree
"""
# overwrite existing attribute
if name in self.__dict__:
self.__dict__[name] = value
return
# when argcomplete changes self.__class__ at runtime
# the instance might not have a _parser attribute anymore
if '_parser' not in self.__dict__:
self.__dict__[name] = value
return
# get attribute on decoree
setattr(self.__dict__['_parser'], name, value)
return self._decoree

def add_argument(self, *args, **kwargs):
"""
Expand Down
50 changes: 50 additions & 0 deletions colcon_core/generic_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright 2023 Open Source Robotics Foundation, Inc.
# Licensed under the Apache License, Version 2.0


class GenericDecorator:
"""A generic class decorator."""

def __init__(self, decoree, **kwargs):
"""
Create a new decorated class instance.
:param decoree: The instance to decorate
:param **kwargs: The keyword arguments are set as attributes on this
instance
"""
assert '_decoree' not in kwargs
kwargs['_decoree'] = decoree
for k, v in kwargs.items():
self.__dict__[k] = v

def __getattr__(self, name):
"""
Get an attribute from this decorator if it exists or the decoree.
:param str name: The name of the attribute
:returns: The attribute value
:raises AttributeError: if the attribute doesn't exist in either of the
two instances
"""
if '_decoree' not in self.__dict__:
raise AttributeError(name)
return getattr(self.__dict__['_decoree'], name)

def __setattr__(self, name, value):
"""
Set an attribute value on this decorator if it exists or the decoree.
:param str name: The name of the attribute
:param value: The attribute value
"""
assert name != '_decoree'
# overwrite existing attribute
if name in self.__dict__:
self.__dict__[name] = value
return
if '_decoree' not in self.__dict__:
self.__dict__[name] = value
return
# set attribute on decoree
setattr(self.__dict__['_decoree'], name, value)
1 change: 0 additions & 1 deletion test/spell_check.words
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
addopts
apache
argcomplete
argparse
asyncio
autouse
Expand Down
4 changes: 2 additions & 2 deletions test/test_argument_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def test_argument_parser_decorator():
decorator = ArgumentParserDecorator(parser)
assert decorator.format_help == parser.format_help

del decorator.__dict__['_parser']
del decorator.__dict__['_decoree']
with pytest.raises(AttributeError):
decorator.format_help

Expand All @@ -108,7 +108,7 @@ def test_argument_parser_decorator():
assert parser.add_argument is True

assert 'bar' not in decorator.__dict__
del decorator.__dict__['_parser']
del decorator.__dict__['_decoree']
decorator.bar = 'baz'
assert 'bar' in decorator.__dict__
assert decorator.__dict__['bar'] == 'baz'
Expand Down

0 comments on commit b21dd93

Please sign in to comment.