From ed26d00e1ffb30f23368ca59b040583b2c41099e Mon Sep 17 00:00:00 2001 From: Graham Dumpleton Date: Sun, 20 Sep 2026 19:56:55 +1000 Subject: [PATCH] Add change note and further tests for optional owner in proxy __get__(). --- docs/changes.rst | 12 ++++++++++++ tests/core/test_auto_object_proxy.py | 20 ++++++++++++++++++++ tests/core/test_lazy_object_proxy.py | 19 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 0fa5992d..25420b6c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -63,6 +63,18 @@ Version 2.4.2 attribute of a ``PartialCallableObjectProxy`` which was initialized, but with no keyword arguments, continues to be an empty dictionary. +* Calling ``__get__()`` on an ``AutoObjectProxy`` or ``LazyObjectProxy`` + wrapping a descriptor, with only the ``instance`` argument supplied, + failed with a ``TypeError`` as the ``owner`` argument was required. The + descriptor protocol makes ``owner`` optional, and builtin descriptors + such as functions and ``property`` accept the one argument form, so + manual binding through the proxy did not behave the same as manual + binding of the wrapped descriptor. The ``owner`` argument now defaults + to ``None``, consistent with ``__get__()`` on function wrappers, and + ``None`` is what the wrapped descriptor is passed when it is omitted. + Normal attribute lookup was not affected as Python always supplies both + arguments in that case. + Version 2.4.1 ------------- diff --git a/tests/core/test_auto_object_proxy.py b/tests/core/test_auto_object_proxy.py index 19f21345..da6d498e 100644 --- a/tests/core/test_auto_object_proxy.py +++ b/tests/core/test_auto_object_proxy.py @@ -187,6 +187,25 @@ def method(instance): proxy.__get__(None, object), descriptor.__get__(None, object) ) + def test_descriptor_optional_owner_required_by_wrapped(self): + # A descriptor implemented in Python can require the owner argument, + # as Python always supplies it on attribute lookup. When the owner + # is omitted in a manual call through the proxy, it is None which + # must be passed to the wrapped descriptor. + + class Descriptor: + def __get__(self, instance, owner): + return (instance, owner) + + instance = object() + + proxy = wrapt.AutoObjectProxy(Descriptor()) + + self.assertEqual(proxy.__get__(instance), (instance, None)) + self.assertEqual(proxy.__get__(instance, None), (instance, None)) + self.assertEqual(proxy.__get__(instance, object), (instance, object)) + self.assertEqual(proxy.__get__(None, object), (None, object)) + def test_descriptor(self): class Descriptor: def __init__(self, value): @@ -409,6 +428,7 @@ def __get__(self, instance, owner): self.assertFalse(hasattr(proxy, "__get__")) proxy.__wrapped__ = Descriptor() self.assertTrue(hasattr(proxy, "__get__")) + self.assertEqual(proxy.__get__(object()), 42) def test_get_removed_on_reassignment(self): class Descriptor: diff --git a/tests/core/test_lazy_object_proxy.py b/tests/core/test_lazy_object_proxy.py index 4f1d08d6..4597f812 100644 --- a/tests/core/test_lazy_object_proxy.py +++ b/tests/core/test_lazy_object_proxy.py @@ -42,6 +42,25 @@ def method(instance): proxy.__get__(None, object), descriptor.__get__(None, object) ) + def test_descriptor_optional_owner_required_by_wrapped(self): + # A descriptor implemented in Python can require the owner argument, + # as Python always supplies it on attribute lookup. When the owner + # is omitted in a manual call through the proxy, it is None which + # must be passed to the wrapped descriptor. + + class Descriptor: + def __get__(self, instance, owner): + return (instance, owner) + + instance = object() + + proxy = wrapt.LazyObjectProxy(Descriptor, interface=Descriptor) + + self.assertEqual(proxy.__get__(instance), (instance, None)) + self.assertEqual(proxy.__get__(instance, None), (instance, None)) + self.assertEqual(proxy.__get__(instance, object), (instance, object)) + self.assertEqual(proxy.__get__(None, object), (None, object)) + def test_lazy_import(self): if "sched" in sys.modules: del sys.modules["sched"]