Skip to main content

Support for object reuse with slight modifications

Project description

Utilities to reuse (slightly modified) objects in new contexts.

Currently, there is are two utilities: rebindFunction and OverridingProxy.

rebindFunction

rebindFunction allows to reuse the code of a function while changing name, globals, default arguments, properties and/or names used.

Lets look at a trivial example. Function f accesses global variables i and j.

Examples

>>> i = 1; j = 2
>>> def f(): return i, j
...
>>> f()
(1, 2)

We want to derive a new function g which binds i to -1:

>>> from dm.reuse import rebindFunction
>>> g=rebindFunction(f, i=-1)
>>> g()
(-1, 2)

We can specify the rebinds not only via keyword arguments but via a dictionary as well:

>>> g=rebindFunction(f, dict(i=-1, j=-2))
>>> g()
(-1, -2)

Usually, the function name is taken over from the original function, but it can be changed:

>>> f.__name__
'f'
>>> g.__name__
'f'
>>> g=rebindFunction(f, dict(i=-1, j=-2), funcName='g')
>>> g.__name__
'g'
>>> g()
(-1, -2)

The originals function docstring is taken over, too – unless overridden:

>>> f.func_doc = 'some documentation'
>>> g=rebindFunction(f, dict(i=-1, j=-2))
>>> f.__doc__ is g.__doc__
True
>>> g=rebindFunction(f, dict(i=-1, j=-2), funcDoc='some new documentation')
>>> g.__doc__
'some new documentation'

Default values for arguments can be added, removed or changed. Unknown arguments are recognized:

>>> def f(a1, a2=2): return a1, a2
...
>>> g=rebindFunction(f, argRebindDir=dict(a1=1))
>>> g()
(1, 2)
>>> from dm.reuse import REQUIRED
>>> g=rebindFunction(f, argRebindDir=dict(a2=REQUIRED))
>>> g(1) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
  ...
TypeError: f() takes exactly 2 arguments (1 given)
>>> g=rebindFunction(f, argRebindDir=dict(a2=10))
>>> g(1)
(1, 10)
>>> g=rebindFunction(f, argRebindDir=dict(a3=10))
Traceback (most recent call last):
  ...
ValueError: unknown arguments in `argRebindDir`: a3

Finally, function properties can be rebound with propRebindDir. We are careful, to give the new function a separate new property dict.

>>> f.prop='p'
>>> g=rebindFunction(f)
>>> g.prop
'p'
>>> g=rebindFunction(f, propRebindDir=dict(prop='P', prop2='p2'))
>>> g.prop, g.prop2
('P', 'p2')
>>> f.__dict__
{'prop': 'p'}

Occationally, functions use local imports which are not adequate in the new context. In order to provide control over them, names used inside the function code can be changed.

>>> def f(a): import codecs; return codecs, a
...
>>> g=rebindFunction(f, nameRebindDir=dict(codecs='urllib'))
>>> r = g(1)
>>> r[0].__name__, r[1]
('urllib', 1)

This way, references to global variables can be changed as well.

>>> i1, i2 = 1, 2
>>> def f(): return i1
...
>>> g=rebindFunction(f, nameRebindDir=dict(i1='i2'))
>>> g()
2

OverridingProxy

Occasionally, you must work with an object, whose overall functionality is mostly adequate but small aspects need to be changed. In those cases, OverridingProxy might be of help. It allows to proxy an object. The proxy mostly behaves like the proxied object but some attributes/methods are overridden.

Examples

We set up a play object c and pretend that we must use it with modified attribute attr1. We achieve this with a proxy which behaves almost like c but has the attribute changed.

>>> from dm.reuse.proxy import OverridingProxy, make_proxy
>>>
>>> class C(object):
...   attr1 = 1
...   attr2 = 2
...   def f(self): return self.attr1, self.attr2
...   def g(self): return self.f()
...
>>> c = C()
>>> p = make_proxy(c, attr1='overridden attr1')
>>> p.__class__ is C
True
>>> isinstance(p, C)
True
>>> p.attr1
'overridden attr1'

The change is also effective when we call methods which use the overrriden attribute.

>>> p.f()
('overridden attr1', 2)

If we assign a new value to a proxy attribute, then the corresponding attribute on the proxied object is changed. As a consequence, changing overridden attributes have apparently no effect – this is unintuitive and might be changed in the future.

>>> p.attr2 = "new attr2"
>>> p.f()
('overridden attr1', 'new attr2')
>>> p.attr1 = "new attr1"
>>> c.attr1
'new attr1'
>>> p.f()
('overridden attr1', 'new attr2')

You can use the method set_proxy_attribute to change the proxies attributes rather than those of the proxied method.

>>> p.set_proxy_attribute("attr1", "new overridden attr1")
>>> p.f()
('new overridden attr1', 'new attr2')

A proxy compares usually equal to its proxied object. Sometimes, this works also for the inverse. But, two proxies for the same proxied object compare usually equal.

>>> p == c
True
>>> c == p
True
>>> make_proxy(c) == p
True

Usually, you would not override simple attributes but methods. This is easier to achieve with a custom proxy class. In many cases, an overriding method will want to call the overridden method; this is possible via call_proxied_method.

>>> class MyProxy(OverridingProxy):
...   def g(self):
...     print ("g called")
...     return self.call_proxied_method("g")
...
>>> p = make_proxy(c, MyProxy, attr1="overridden attr1")
>>> p.g()
g called
('overridden attr1', 'new attr2')

The proxies have limited support for special methods and thereby support e.g. subscription. Note that this support is incomplete and surprises are possible.

>>> class MyDict(dict, C): pass
...
>>> md = MyDict(dict(a=1, b=2))
>>> p = make_proxy(md, attr1="overridden attr1")
>>> p.f()
('overridden attr1', 2)
>>> p["a"]
1
>>> class MySequence(C):
...   seq = 0, 1, 2
...   def __getitem__(self, i): return self.seq[i]
...
>>> c = MySequence()
>>> p = make_proxy(c, seq=(10, 11, 12))
>>> p[0]
10
>>> p = make_proxy(c)
>>> p[0]
0

Usually, descriptors, too, see the modified state.

>>> class WithProperty(C):
...   @property
...   def attr1_prop(self): return self.attr1
...
>>> c = WithProperty()
>>> p = make_proxy(c, attr1="overridden attr1")
>>> p.attr1_prop
'overridden attr1'

History

2.1
added OverridingProxy, allowing to reuse objects with small modifications
2.0

added partial support for Python 3 (keyword only arguments and annotations are not yet supported)

dropped support for Python before 2.7

1.1
nameRebindDir support added

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Files for dm.reuse, version 2.1.1
Filename, size File type Python version Upload date Hashes
Filename, size dm.reuse-2.1.1.tar.gz (9.7 kB) File type Source Python version None Upload date Hashes View hashes

Supported by

Elastic Elastic Search Pingdom Pingdom Monitoring Google Google BigQuery Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN SignalFx SignalFx Supporter DigiCert DigiCert EV certificate StatusPage StatusPage Status page