From 8111cce3401c5dcf1dca61d0c8599d512b42da16 Mon Sep 17 00:00:00 2001 From: Andrew Ross Date: Thu, 19 May 2022 12:50:19 -0400 Subject: [PATCH] Allow multiple delegations to stack, as well as optional prefixes --- README.md | 23 +++++++++++++++++++++++ delegate/__init__.py | 38 ++++++++++++++++++++++++-------------- test-delegate.py | 6 ++++++ 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cb8b482..8dbf2ad 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,26 @@ except e: assert raised ``` + +Delegation can also be called multiple times on the same class, and there is an +optional `prefix` option which allows the attribute name to be prefixed on the +delegating class: + +```python +from delegate import delegate + +class Parent: + def __init__(self): + self.a = "a" + self.b = "b" + +@delegate("a", to="parent") +@delegate("b", to="parent", prefix="_") +class Child: + def __init__(self): + self.parent = Parent() + self.c = "c" + +instance.a +instance._b +``` diff --git a/delegate/__init__.py b/delegate/__init__.py index 1cb4f84..df4e4bb 100644 --- a/delegate/__init__.py +++ b/delegate/__init__.py @@ -1,18 +1,21 @@ #!/usr/bin/env python3 -def bind(instance, func, name): - """Turn a function into a bound method on instance""" - setattr(instance, name, func.__get__(instance, instance.__class__)) - def delegate(*args, **named_args): - dest = named_args.get('to') - if dest is None: + _dest = named_args.get('to') + if _dest is None: raise ValueError( "the named argument 'to' is required on the delegate function") + + _prefix = named_args.get('prefix', '') + + print(_dest, _prefix) + def wraps(cls, *wrapped_args, **wrapped_opts): """Wrap the target class up in something that modifies.""" class Wrapped(cls): + _delegates = [(a, _dest, _prefix) for a in args] + getattr(cls, '_delegates', []) + def __getattr__(self, name): """ Return the selected name from the destination if the name is one @@ -21,7 +24,10 @@ def __getattr__(self, name): error when `name` is not one of the selected args to be delegated. """ - if name in args: return getattr(self.__dict__[dest], name) + for attr, dest, prefix in self._delegates: + if name == prefix + attr: + return getattr(self.__dict__[dest], name[len(prefix):]) + raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'") def __setattr__(self, name, value): @@ -29,19 +35,23 @@ def __setattr__(self, name, value): If this name is one of those selected, set it on the destination property. Otherwise, set it on self. """ - if name in args: setattr(getattr(self, dest), name, value) - else: self.__dict__[name] = value + for attr, dest, prefix in self._delegates: + if name == prefix + attr: + setattr(getattr(self, dest), name[len(prefix):], value) + return + self.__dict__[name] = value def __delattr__(self, name): """Delete name from `dest` or `self`""" - if name in args: delattr(getattr(self, dest), name) - else: del self.__dict__[name] + for attr, dest, prefix in self._delegates: + if name == prefix + attr: + delattr(getattr(self, dest), name[len(prefix):]) + return - def __init__(self, *wrapped_args, **wrapped_opts): - super().__init__(*wrapped_args, **wrapped_opts) + del self.__dict__[name] Wrapped.__doc__ = cls.__doc__ or \ - f"{cls.__class__} wrapped to delegate {args} to its {dest} property" + f"{cls.__class__} wrapped to delegate {args} to its {_dest} property" Wrapped.__repr__ = cls.__repr__ Wrapped.__str__ = cls.__str__ Wrapped.__name__ = cls.__name__ diff --git a/test-delegate.py b/test-delegate.py index fcd83fb..2698122 100644 --- a/test-delegate.py +++ b/test-delegate.py @@ -4,8 +4,10 @@ class Parent: def __init__(self): self.a = "a" self.b = "b" + self.d = "d" @delegate("a", "b", to="parent") +@delegate("d", to="parent", prefix="_") class Child: """A class with a delegate""" def __init__(self): @@ -41,5 +43,9 @@ def expect_raises(errtype): assert instance.__class__.__name__ == "Child" with expect_raises(AttributeError): instance.z +with expect_raises(AttributeError): + instance.d +instance._d +assert len(Child._delegates) == 3 print("TESTING delegate()...done")