Object proxy for python
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Paweł Płazieński f5925cb8bd prepare for future development 7 years ago
src/proxable descriptors partially handled 7 years ago
tests descriptors partially handled 7 years ago
.gitignore adjusted project structure 7 years ago
README.rst descriptors partially handled 7 years ago
setup.py prepare for future development 7 years ago

README.rst

===========
proxyable
===========

Proxy implementation for python.

About
-----

Lets say you want to change single behavior of an **existing** object. Python offers many ways to do that: you could
replace some attribute, you could install lambda instead of method, you could replace the whole object (well, that would
be a little too much, but hey, you can!). Ok, bad example.

Lets get more specific: you want to change single behavior of single existing object for **part** of the system, but you
want that part to be unaware of the difference. There's pretty much only one solution to that problem: Decorator_.
That's good when you have class with single method, but gets long, descriptive and boring as number of methods
increases. And, as Python does not natively enforce proper implementation of interfaces, is prone to API
desynchronization.

So how about automatic, *dynamic* decorator? Well, that's a Proxy_!

.. _Proxy: http://en.wikipedia.org/wiki/Proxy_pattern
.. _Decorator: http://en.wikipedia.org/wiki/Decorator_pattern

Why would I use a proxy?
------------------------

Because it's fun: `sourcemaking has some idea about proxies`__. Because it's magic (the good type). Because it provides
another level of indirection, and we all know the `problems it solves`__.

__ http://sourcemaking.com/design_patterns/proxy
__ http://www.dmst.aueb.gr/dds/pubs/inbook/beautiful_code/html/Spi07g.html

Ok, example: Suppose you want to count how many times specific attribute was requested from object.

First, some imports (these will be explained later)::

>>> from proxable.object import ResolvingDelegate
>>> from proxable.resolver import StaticResolver
>>> from proxable.proxy import Proxy

Lets keep things OO, declaring a simple counter::

>>> class Counter(object):
... def __init__(self, value = 0):
... self._value = value
... def increment(self):
... self._value += 1
... def value(self):
... return self._value

Now the glorius Delegate_ (actually, a Delegate subclass, ResolvingDelegate_)::

>>> class AttributeCountingDelegate(ResolvingDelegate):
... def __init__(self, instance, attribute_name, counter):
... ResolvingDelegate.__init__(self, StaticResolver(instance))
... self._attribute_name = attribute_name
... self._counter = counter
... def attribute_get(self, name):
... if name == self._attribute_name:
... self._counter.increment()
... return ResolvingDelegate.attribute_get(self, name)

Well, lets use it!::

>>> watched_instance = dict(a = 1, b = 2)
>>> counter = Counter()
>>> delegate = AttributeCountingDelegate(watched_instance, 'keys', counter)
>>> proxied_instance = Proxy(delegate)
>>> proxied_instance.keys()
dict_keys(['a', 'b'])
>>> counter.value()
1
>>> proxied_instance['c'] = 3
>>> len(proxied_instance)
3
>>> proxied_instance.keys()
dict_keys(['a', 'c', 'b'])
>>> counter.value()
2

Well, that's not very useful. I don't think there's a real world usage of that. Let's try again:

Let's write a proxy, for which we could swap proxied object later::

>>> swappable_resolver = StaticResolver(watched_instance)
>>> resolving_delegate = ResolvingDelegate(swappable_resolver)
>>> swappable_proxy = Proxy(resolving_delegate)
>>> swappable_proxy['a']
1
>>> swappable_proxy.update({'d': 10})
>>> len(swappable_proxy)
4
>>> swappable_resolver.set(dict(z = 10000))
>>> len(swappable_proxy)
1
>>> swappable_proxy['z']
10000
>>> swappable_resolver.set(None)
>>> swappable_proxy == None
True
>>> swappable_proxy is None
False

Now that's nice.

About that last identity check: that's one of few limitations which is probably inescapable, see Limitations_.

How it works
------------

So, how does this work?

In short: Proxy delegates special method calls to Delegate, which actually does stuff for it. ObjectDelegate (parent of
ResolvingDelegate) do the same stuff that was done on proxy on another object. ResolvingDelegate provides that object
using TargetResolver (parent of StaticResolver).

Proxy
-----

The cherry on top of a cake. To construct, pass a Delegate_ object and your proxy is ready.

Proxy class also provides some nice construction method (see ``for_*`` classmethods), and method of extracting delegate
from proxy object::

>>> Proxy.delegate(swappable_proxy) is resolving_delegate
True

There is no ``is_proxy`` method, because you really don't need one. ``Proxy.delegate`` throws TypeError when passed
parameter is not a proxy - test for that.

Delegate
--------

First don't look at the class, it'll scare you. It has something like 80 (eighty) abstract methods.

This class (interface, actually), describes what to do when some special methods are called on proxy.

You probably don't want to directly implement it. Look for ObjectDelegate_ or UnifiedDelegate_, one of them should suit
your need.

If you really need to implement it directly (because you have some brilliant and whacky idea), keep in mind that you
don't need to implement all methods. Any not implemented method will raise NotImplementedError at runtime. So if you
don't think somebody will use *inplace left shift* (that's ``proxy <<= arg``), just don't implement it. If you just use
proxy for method-operated-objects, you should be fine with only ``attribute_get`` and ``cast_string``.

If you actually implemented Delegate, because you had some brilliant and/or whacky idea, please, send me a message.

ObjectDelegate
--------------

Applies delegate methods as thier counterpart special methods to object returned by ``target``. This method can return
anything, even different thing between calls. Result is not cached.

ResolvingDelegate
-----------------

Descending from ObjectDelegate_, it externalizes target resolving to TargetResolver. Look in ``proxable.resolver`` for
examples.

UnifiedDelegate
---------------

This delegate unifies call to every method to ``invoke``. It receives a method name and its parameters, should return
whatever result is expected from delegation. Method name received is something like special method name of and object,
see `python special method names`__.

First thing that comes into mind when thinking about an example for this delegate is remoting. You can serialize every
call, send it over the network, handle it on the other side and send the result back. I'm actually using it that way.

__ http://docs.python.org/3/reference/datamodel.html#special-method-names

Features, which could be treated as bugs
----------------------------------------

1. Calling methods on proxy

Use caution when calling methods on proxy, because instance bound to method probably won't be the same proxy::

>>> swappable_resolver.set(counter)
>>> method = swappable_proxy.increment
>>> method
<bound method Counter.increment of <__main__.Counter object at ...>>
>>> method.__self__ is swappable_proxy
False
>>> method.__self__ is counter
True

This thing occur, because method you get (an attribute of counter) is actually extracted from counter, and it's bound
right there.

If you don't do anything clever with method ``self``, you are pretty safe, method still works as expected::

>>> method()
>>> counter.value()
3
>>> swappable_proxy.increment()
>>> counter.value()
4

If you could, you could replace ``__self__`` and it would (probably) work::

>>> method.__self__ = swappable_proxy
Traceback (most recent call last):
...
AttributeError: readonly attribute
>>> method()
>>> counter.value()
5

I should probably write a decorator which would handle this behavior well.

2. Finalization

Proxy_ actually delegate every special method, even the ``__del__`` (finalizer), but this one is not implemented in
ObjectDelegate_ like the others. Instead, it's implemented as no-op.

That's because finalization is very special. Proxy itself does not require any finalization (no resources held), and it
most definitively shouldn't finalize it's target. UnifiedDelegate_ will still catch it.

3. Inplace operators

These should return ``self`` as result, and left hand side of the operator will get replaced by what was returned. This
means that after first call, proxy will get replaced with what delegation returned::

>>> class WrappedInteger(object):
... def __init__(self, value):
... self._value = value
... def __add__(self, other):
... return self._value + other
... def __iadd__(self, other):
... self._value += other
... return self
... def __int__(self):
... return self._value
>>> wrapped_integer = WrappedInteger(10)
>>> wrapped_integer += 10
>>> int(wrapped_integer)
20
>>> swappable_resolver.set(wrapped_integer)
>>> int(swappable_proxy)
20
>>> swappable_proxy + 10
30
>>> swappable_proxy += 10
>>> swappable_proxy
<__main__.WrappedInteger object at ...>
>>> swappable_proxy is wrapped_integer
True
>>> int(swappable_proxy)
30
>>> repr(swappable_proxy)
'<__main__.WrappedInteger object at ...>'
>>> Proxy.delegate(swappable_proxy)
Traceback (most recent call last):
...
TypeError: object is not a proxy

This will be fixed in the same decorator as method call ``__self__`` wrapping

4. *Descriptor* (or *property accessor*) handling

That's ``__get__``, ``__set__`` and ``__delete__``. These are pretty tricky, because they cannot be translated to some
object operation. They are delegated from Proxy_ to it's delegate as ``descriptor_get``, ``descriptor_set`` and
``descriptor_delete``, but they are not handled by ObjectDelegate_, because it does not apply. UnifiedDelegate_ handles
them just fine.

To actually use them you would need to implement DescriptorDelegate which minimally would handle these three methods.
Then, it would translate calls to actual descriptor access - you would need to provide descriptor as delegate instance
variable. Also, you would need to declare descriptor on class which it should handle - that's
``descriptor = Proxy(DescriptorDelegate(other_descriptor))`` in class scope. Also, this would probably be needed in the
same class as ``other_descriptor``. I'm far out at this point, I don't see point of doing that.

Obviously, this might be useful way to do that thing that you need, but you probably need very specific Delegate_.
If you do think that universal delegating descritor Delegate is viable, please, send me a message.

Limitations
-----------

1. Object identity

Obviously, proxy object is different (in terms of identity) object than what it points to. Especially, when there is no
backing object.

2. Reduce

I really didn't want to make two delegate handles for both ``__reduce__`` and ``__reduce_ex__``. They use the same
delegate handle, ``reduce``. Calls to ``__reduce__`` will send ``None`` as protocol version, which is, as I understood
it, how python should handle it. Proxy reduction should work just fine if you have that in mind.