Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
Release Notes
=============

Version 2.5.2
-------------

**Bugs Fixed**

* When the object wrapped by ``wrapt.AutoObjectProxy``, or created by the
factory of a ``wrapt.LazyObjectProxy``, was a class, the special methods
the class defines for its instances were added to the generated proxy
class, even though Python looks up special methods on the type of an
object, which for a class is its metaclass. A proxy for a class defining
``__set_name__()`` therefore raised ``TypeError`` when assigned to an
attribute in a class body, a proxy for a class defining ``__get__()``
was bound as a descriptor on attribute access, and a proxy for a class
defining ``__await__()`` or ``__iter__()`` was reported as awaitable or
iterable by ``inspect.isawaitable()`` and ``collections.abc`` checks.
Conversely, special methods provided by a custom metaclass, such as
``__iter__()``, were not added when an ``AutoObjectProxy`` was created
for the class. For a class, the special methods of its
metaclass are now used, so the proxy behaves the same as the class it
wraps.

Version 2.5.1
-------------

Expand Down
40 changes: 28 additions & 12 deletions src/wrapt/proxies.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ def __wrapper_set_name__(self, owner, name):
return self.__wrapped__.__set_name__(owner, name)


def _special_method_source(wrapped):
"""Returns the object to check for the special dunder methods which apply
to `wrapped`. Python looks these up on the type of an object. For most
objects checking the object itself gives the same answer, but a class
lists the methods it defines for its instances, which do not apply to the
class itself. For a class the methods of its metaclass are used instead.
"""

if isinstance(wrapped, type):
return type(wrapped)

return wrapped


class AutoObjectProxy(BaseObjectProxy):
"""An object proxy which can automatically adjust to the wrapped object
and add special dunder methods as needed. Note that this creates a new
Expand All @@ -101,7 +115,7 @@ def __new__(cls, wrapped, *args, **kwargs):

namespace = {}

wrapped_attrs = dir(wrapped)
wrapped_attrs = dir(_special_method_source(wrapped))
class_attrs = set(dir(cls))

if callable(wrapped) and "__call__" not in class_attrs:
Expand Down Expand Up @@ -162,73 +176,75 @@ def __wrapped_setattr_fixups__(self):
cls = type(self)
class_attrs = set(dir(cls))

wrapped = _special_method_source(self.__wrapped__)

if callable(self.__wrapped__):
if "__call__" not in class_attrs:
cls.__call__ = __wrapper_call__
elif getattr(cls, "__call__", None) is __wrapper_call__:
delattr(cls, "__call__")

if hasattr(self.__wrapped__, "__iter__"):
if hasattr(wrapped, "__iter__"):
if "__iter__" not in class_attrs:
cls.__iter__ = __wrapper_iter__
elif getattr(cls, "__iter__", None) is __wrapper_iter__:
delattr(cls, "__iter__")

if hasattr(self.__wrapped__, "__next__"):
if hasattr(wrapped, "__next__"):
if "__next__" not in class_attrs:
cls.__next__ = __wrapper_next__
elif getattr(cls, "__next__", None) is __wrapper_next__:
delattr(cls, "__next__")

if hasattr(self.__wrapped__, "__aiter__"):
if hasattr(wrapped, "__aiter__"):
if "__aiter__" not in class_attrs:
cls.__aiter__ = __wrapper_aiter__
elif getattr(cls, "__aiter__", None) is __wrapper_aiter__:
delattr(cls, "__aiter__")

if hasattr(self.__wrapped__, "__anext__"):
if hasattr(wrapped, "__anext__"):
if "__anext__" not in class_attrs:
cls.__anext__ = __wrapper_anext__
elif getattr(cls, "__anext__", None) is __wrapper_anext__:
delattr(cls, "__anext__")

if hasattr(self.__wrapped__, "__length_hint__"):
if hasattr(wrapped, "__length_hint__"):
if "__length_hint__" not in class_attrs:
cls.__length_hint__ = __wrapper_length_hint__
elif getattr(cls, "__length_hint__", None) is __wrapper_length_hint__:
delattr(cls, "__length_hint__")

if hasattr(self.__wrapped__, "__fspath__"):
if hasattr(wrapped, "__fspath__"):
if "__fspath__" not in class_attrs:
cls.__fspath__ = __wrapper_fspath__
elif getattr(cls, "__fspath__", None) is __wrapper_fspath__:
delattr(cls, "__fspath__")

if hasattr(self.__wrapped__, "__await__"):
if hasattr(wrapped, "__await__"):
if "__await__" not in class_attrs:
cls.__await__ = __wrapper_await__
elif getattr(cls, "__await__", None) is __wrapper_await__:
delattr(cls, "__await__")

if hasattr(self.__wrapped__, "__get__"):
if hasattr(wrapped, "__get__"):
if "__get__" not in class_attrs:
cls.__get__ = __wrapper_get__
elif getattr(cls, "__get__", None) is __wrapper_get__:
delattr(cls, "__get__")

if hasattr(self.__wrapped__, "__set__"):
if hasattr(wrapped, "__set__"):
if "__set__" not in class_attrs:
cls.__set__ = __wrapper_set__
elif getattr(cls, "__set__", None) is __wrapper_set__:
delattr(cls, "__set__")

if hasattr(self.__wrapped__, "__delete__"):
if hasattr(wrapped, "__delete__"):
if "__delete__" not in class_attrs:
cls.__delete__ = __wrapper_delete__
elif getattr(cls, "__delete__", None) is __wrapper_delete__:
delattr(cls, "__delete__")

if hasattr(self.__wrapped__, "__set_name__"):
if hasattr(wrapped, "__set_name__"):
if "__set_name__" not in class_attrs:
cls.__set_name__ = __wrapper_set_name__
elif getattr(cls, "__set_name__", None) is __wrapper_set_name__:
Expand Down
95 changes: 95 additions & 0 deletions tests/core/test_auto_object_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,73 @@ class Owner:
del owner.attr
self.assertRaises(AttributeError, lambda: owner.attr)

def test_wrapped_class_is_not_descriptor(self):
class Descriptor:
def __get__(self, instance, owner=None):
return 42

def __set__(self, instance, value):
pass

def __delete__(self, instance):
pass

def __set_name__(self, owner, name):
pass

# Only instances of the class are descriptors, not the class itself,
# so a proxy for the class stored in a class body must not apply the
# descriptor protocol methods the class defines for its instances.

class Owner:
attr = wrapt.AutoObjectProxy(Descriptor)

owner = Owner()

self.assertIs(Owner.attr.__wrapped__, Descriptor)
self.assertIs(owner.attr.__wrapped__, Descriptor)

owner.attr = 100
self.assertEqual(owner.attr, 100)

del owner.attr
self.assertIs(owner.attr.__wrapped__, Descriptor)

def test_wrapped_class_is_not_awaitable_or_iterable(self):
import collections.abc
import inspect

class Awaitable:
def __await__(self):
yield
return 42

class Iterable:
def __iter__(self):
return iter([1, 2, 3])

self.assertFalse(inspect.isawaitable(Awaitable))
self.assertFalse(inspect.isawaitable(wrapt.AutoObjectProxy(Awaitable)))

self.assertNotIsInstance(Iterable, collections.abc.Iterable)
self.assertNotIsInstance(
wrapt.AutoObjectProxy(Iterable), collections.abc.Iterable
)

def test_wrapped_class_metaclass_iter(self):
class Meta(type):
def __iter__(cls):
return iter([1, 2, 3])

class Iterable(metaclass=Meta):
pass

self.assertEqual(list(Iterable), [1, 2, 3])

proxy = wrapt.AutoObjectProxy(Iterable)

self.assertEqual(list(proxy), [1, 2, 3])


class TestAutoObjectProxyReassignment(unittest.TestCase):

Expand Down Expand Up @@ -499,3 +566,31 @@ def __set_name__(self, owner, name):
self.assertTrue(hasattr(proxy, "__set_name__"))
proxy.__wrapped__ = object()
self.assertFalse(hasattr(proxy, "__set_name__"))

def test_wrapped_class_is_not_descriptor_on_reassignment(self):
class Descriptor:
def __get__(self, instance, owner=None):
return 42

def __set_name__(self, owner, name):
pass

proxy = wrapt.AutoObjectProxy(object())
proxy.__wrapped__ = Descriptor

class Owner:
attr = proxy

self.assertIs(Owner().attr, proxy)

def test_wrapped_class_metaclass_iter_on_reassignment(self):
class Meta(type):
def __iter__(cls):
return iter([1, 2, 3])

class Iterable(metaclass=Meta):
pass

proxy = wrapt.AutoObjectProxy(object())
proxy.__wrapped__ = Iterable
self.assertEqual(list(proxy), [1, 2, 3])
17 changes: 17 additions & 0 deletions tests/core/test_lazy_object_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,23 @@ def test_lazy_import_iterable(self):

self.assertEqual("".join(digits2), "0123456789")

def test_lazy_class_is_not_descriptor(self):
class Descriptor:
def __get__(self, instance, owner=None):
return 42

def __set_name__(self, owner, name):
pass

proxy = wrapt.LazyObjectProxy(lambda: Descriptor)

self.assertIs(proxy.__wrapped__, Descriptor)

class Owner:
attr = proxy

self.assertIs(Owner().attr, proxy)

def test_pathlike_interface_is_lazy(self):
for interface in (os.PathLike, Path):
with self.subTest(interface=interface):
Expand Down