Skip to content

Commit

Permalink
91: fix delegate subclasses overriding base class methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-mitchell committed Apr 27, 2023
1 parent 82337ef commit afe49a8
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 12 deletions.
19 changes: 17 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,9 @@ the ``Delegate`` class assists with this task reducing boiler plate code such as
def method(self):
return self.impl.method()

The ``Delegate`` class provides 3 special attributes to route attributes to a child object.
The ``Delegate`` class provides 3 special attributes to route attributes to a child object. Only attributes and mothods
not defined on the class (or super-classes) are routed. (Attributes and methods defined on an interface sub-class are not
considered part of the implementation and these attributes are routed.)
Any one or combination of attributes is allowed.

pi_attr_delegates
Expand Down Expand Up @@ -537,7 +539,20 @@ Methods and properties defined on the delegate class itself take precedence (as
return 'my bar'

However, attempting to set an instance attribute as an override will just set the attribute on the underlying delegate
instead.
instead. If you want to override using an instance attribute, first define it as a class attribute::

class MyDelegate(Delegate, IFoo):
pi_attr_delegates = {'impl': IFoo}
foo = None # prevents delegation of foo to `impl`

def __init__(self, impl):
self.impl = impl
self.foo = 3

If you supply more than one delegation rule (e.g. both ``pi_attr_mapping`` and ``pi_attr_fallack``) then
``pi_attr_delegates`` delegates are created first and any attributes defined there are now part of the class.
Then ``pi_attr_mapping`` delegates are created (and become part of the class) and finally ``pi_attr_fallback`` is processed.
Thus if there are duplicate delegates defined, the one defined first takes precedence.

Composition
-----------
Expand Down
2 changes: 1 addition & 1 deletion pure_interface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
except ImportError:
pass

__version__ = '7.0.0'
__version__ = '7.0.1'
26 changes: 22 additions & 4 deletions pure_interface/delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,31 @@ def bar(self, baz):
return 'my bar'
However, attempting to set an instance attribute as an override will just set the property on the underlying
delegate.
delegate. If you want to override using an instance attribute, first define it as a class attribute
class MyDelegate(Delegate, IFoo):
pi_attr_delegates = {'impl': IFoo}
foo = None # prevents delegation of foo to `impl`
def __init__(self, impl):
self.impl = impl
self.foo = 3
"""
pi_attr_fallback = None
pi_attr_delegates = {}
pi_attr_mapping = {}

def __init_subclass__(cls, **kwargs):
# get non-interface base class ignoring abc.ABC and object.
non_interface_bases = [base for base in cls.mro()[:-2] if not type_is_interface(base)]

def i_have_attribute(attrib):
for klass in non_interface_bases:
if attrib in klass.__dict__:
return True
return False

for delegate, attr_list in cls.pi_attr_delegates.items():
if isinstance(attr_list, type):
attr_list = list(get_interface_names(attr_list))
Expand All @@ -113,19 +131,19 @@ def __init_subclass__(cls, **kwargs):
for attr in attr_list:
if attr in cls.pi_attr_mapping:
raise ValueError(f'{attr} in pi_attr_map and handled by delegate {delegate}')
if attr in cls.__dict__:
if i_have_attribute(attr):
continue
dotted_name = f'{delegate}.{attr}'
setattr(cls, attr, _Delegated(dotted_name))
for attr, dotted_name in cls.pi_attr_mapping.items():
if attr not in cls.__dict__:
if not i_have_attribute(attr):
setattr(cls, attr, _Delegated(dotted_name))
if cls.pi_attr_fallback:
fallback = cls.pi_attr_fallback
for interface in get_type_interfaces(cls):
interface_names = get_interface_names(interface)
for attr in interface_names:
if attr not in cls.__dict__:
if not i_have_attribute(attr):
dotted_name = f'{fallback}.{attr}'
setattr(cls, attr, _Delegated(dotted_name))

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = pure_interface
version = 7.0.0
version = 7.0.1
description = A Python interface library that disallows function body content on interfaces and supports adaption.
keywords = abc interface adapt adaption mapper structural typing dataclass
author = Tim Mitchell
Expand Down
60 changes: 56 additions & 4 deletions tests/test_delegate.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import dataclasses

import pure_interface
import unittest
from unittest import mock
Expand Down Expand Up @@ -29,12 +31,29 @@ class IPoint(pure_interface.Interface):
x: int
y: int

def to_str(self) -> str:
pass


class IPoint3(IPoint):
z: int


@dataclasses.dataclass
class PointImpl:
x: int
y: int
z: int


class Point(IPoint, object):
def __init__(self, x=0, y=1):
self.x = int(x)
self.y = int(y)

def to_str(self) -> str:
return f'{self.x}, {self.y}'


class DFallback(delegation.Delegate, ITalker):
pi_attr_fallback = 'impl'
Expand All @@ -45,17 +64,18 @@ def __init__(self, impl):

class DAttrMap(delegation.Delegate, IPoint):
pi_attr_mapping = {'x': 'a.x',
'y': 'b.y',
}
'y': 'b.y',
'to_str': 'b.to_str',
}

def __init__(self, a, b):
self.a = a
self.b = b


class DDelegateList(delegation.Delegate, IPoint):
pi_attr_delegates = {'a': ['x'],
'b': ['x', 'y']}
pi_attr_delegates = {'a': ['x', 'to_str'],
'b': ['x', 'y']}

def __init__(self, a, b):
self.a = a
Expand Down Expand Up @@ -110,6 +130,28 @@ def __init__(self):
self.a = a


class ScaledPoint(pure_interface.Delegate, IPoint):
pi_attr_fallback = '_p'

def __init__(self, point):
self._p = point

@property
def y(self):
return self._p.y * 2

@y.setter
def y(self, value):
self._p.y = int(value // 2)

def to_str(self) -> str:
return f'{self.x}, {self.y}'


class ScaledPoint3(ScaledPoint, IPoint3):
pi_attr_fallback = '_p'


class DelegateTest(unittest.TestCase):
def test_descriptor_get_class(self):
d = pure_interface.delegation._Delegated('foo.bar')
Expand Down Expand Up @@ -223,6 +265,16 @@ def test_double_dotted_delegate(self):
d.x = 1
self.assertEqual(1, d.a.b.x)

def test_delegate_subclass(self):
"""test that subclass methods are not delegated """
p = PointImpl(1, 2, 3)
d3 = ScaledPoint3(p)
self.assertEqual(4, d3.y)
self.assertEqual('1, 4', d3.to_str())
d3.y = 8 # delegates to p
self.assertEqual(4, p.y)
self.assertEqual(3, d3.z)


class CompositionTest(unittest.TestCase):

Expand Down

0 comments on commit afe49a8

Please sign in to comment.