GrahamDumpleton / wrapt

A Python module for decorators, wrappers and monkey patching.
BSD 2-Clause "Simplified" License
2.04k stars 230 forks source link

Wrapped object is identified as an iterable when it is not #93

Open nolar opened 7 years ago

nolar commented 7 years ago

Problem:

>>> import wrapt, collections
>>> class MyObject(object): pass

>>> obj = MyObject()
>>> isinstance(obj, collections.Iterable)
False

>>> wrp = wrapt.ObjectProxy(obj)
>>> isinstance(wrp, collections.Iterable)
True

Cause:

This is caused by collections.Iterable to actually check the MRO of the instance for the presence of __iter__ in one of the base classes (i.e. not via getattr()/hasattr()), and the ObjectProxy happens to have this and all other magic methods predefined.

See the checking logic here:

Suggestions:

I guess, the issue cannot be solved for the generic case. There is no way to intercept such checks in the proxy class or its descendant or in their metaclasses (i.e., no __instancecheck__, __subclasscheck__, __subclasshook__ tricks are possible).

But at least, can it be possible to define partial wrappers, similar to how namedtuples are created at runtime? I.e., to create a wrapper base class with the selected set of magic methods enabled/disabled:

>>> MyLimitedProxy = wrapt.create_proxy_class(iter=False, comparisons=False)
>>> wrp = MyLimitedProxy(obj)
>>> isinstance(wrp, collections.Iterable)
False
class MyWrapper(wrapt.create_proxy_class(iter=False)):
    pass

Something similar to this: http://code.activestate.com/recipes/496741-object-proxying/ — but with the filtering of the properties.

PS: I really do not want to create yet another wrapper library when there are so many of them, and prefer to use the already used library.

GrahamDumpleton commented 7 years ago

I already use the partial wrapper approach to break out __call__() as it caused similar issues early on. Trying to create even more variations and the combinations will get messy, as also have to deal with __hash__(), __len__(), __contains__() and possibly others.

An alternative approach is for a base proxy object not to have any of these special methods which cause problems and have the __init__() method of the proxy object to only add them into the instance at the time of creation if the wrapped object has them. This means extra checks every time a proxy object is created, but also complicates the C implementation as the proxy object couldn't be in C, only the base proxy object. This is because you can't update the slots of the C object struct to add them in __init__().

I'll keep thinking about it and do some tests to see how practical it is for it to handle it more dynamically just for these special methods for which there are special isinstance() types.

nolar commented 7 years ago

What about creating the wrapping methods or returning the original methods on __getattribute__/__getattr__, and escalating the AttributeError if they are absent in the original wrapped object? Consider also the case when the wrapped instance has some magic methods added at runtime, not defined in its class (rare, but possible case).

Another way would be to create specialised wrapper classes, e.g. MyObjectWrapper = creater_wrapper_class(MyObject), or via inheritance: class MyObjectWrapper(MetaWrapper): CLS=MyObject. By default, the wrapper wraps the instances of arbitrary classes as it does now.

GrahamDumpleton commented 7 years ago

Overriding __getattribute__() is too expensive as impacts every attribute lookup and not just missing ones. Am not sure if overriding __getattr__() will have desired effect on a C implementation as C methods for some things from memory only look up the slots structure for special methods. Will need to experiment with using it for that.