@@ -201,6 +201,122 @@ This is an inherent limitation of the transparent proxy pattern: the
201201proxy can override ``__class__ `` at the Python level, but it cannot
202202change the object's C-level type.
203203
204+ Deriving from ObjectProxy alongside an ABCMeta-based class
205+ ----------------------------------------------------------
206+
207+ A custom proxy that derives from both ``ObjectProxy `` and a second base
208+ class whose metaclass is ``abc.ABCMeta `` will fail when used with
209+ ``isinstance() `` or ``issubclass() ``::
210+
211+ from abc import ABC
212+ from wrapt import ObjectProxy
213+
214+ class Base(ABC):
215+ pass
216+
217+ class Proxy(ObjectProxy, Base):
218+ pass
219+
220+ isinstance(1, Proxy)
221+ # TypeError: descriptor '__subclasscheck__' for '_wrappers.ObjectProxy'
222+ # objects doesn't apply to a 'type' object
223+
224+ The same failure occurs when the second base class is one of the
225+ abstract base classes exported from ``collections.abc `` (for example
226+ ``Hashable ``, ``Iterable ``, ``Container ``), since they too use
227+ ``ABCMeta `` as their metaclass.
228+
229+ The cause is the way ``ObjectProxy `` implements ``__instancecheck__ ``
230+ and ``__subclasscheck__ ``. These are defined as instance methods on the
231+ proxy class so that an ``ObjectProxy `` instance can appear on the right
232+ hand side of an ``isinstance() `` or ``issubclass() `` check and have the
233+ check delegate to the wrapped type. They rely on ``self `` being a real
234+ proxy instance, and the C extension enforces this at the descriptor
235+ level.
236+
237+ When ``ObjectProxy `` is mixed in as a base class alongside an
238+ ``ABCMeta ``-based class, those methods are inherited as ordinary
239+ instance methods on the resulting class. Unlike the default
240+ ``type.__instancecheck__ ``, ``ABCMeta.__instancecheck__ `` performs its
241+ work by calling ``cls.__subclasscheck__(...) `` via normal attribute
242+ access on the class. That attribute access finds the inherited
243+ ``__subclasscheck__ `` from ``ObjectProxy `` and invokes it with a class
244+ as the first argument. The C descriptor sees that the first argument is
245+ not an ``ObjectProxy `` instance and raises the ``TypeError `` shown
246+ above.
247+
248+ Mixing ``ObjectProxy `` with one of these abstract base classes at
249+ runtime is almost always the wrong approach to begin with.
250+ ``ObjectProxy `` is designed to be used as a single base class, with
251+ derived classes overriding only the specific methods that need to
252+ change. Adding a second, unrelated base class brings in extra
253+ protocol-level behaviour which interacts poorly with what
254+ ``ObjectProxy `` already does internally.
255+
256+ The usual motivation for adding an abstract base class such as
257+ ``Hashable `` to the base list is to satisfy a static type checker which
258+ has been told to expect the proxy to be declared as a subtype of that
259+ abstract base class. At runtime the inheritance is typically redundant.
260+ The abstract base classes in ``collections.abc `` use a structural
261+ ``__subclasshook__ `` (``Hashable `` is satisfied by anything that
262+ defines ``__hash__ ``, ``Iterable `` by anything that defines
263+ ``__iter__ ``, and so on), and ``ObjectProxy `` already defines those
264+ methods where appropriate, forwarding to the wrapped object. So
265+ ``isinstance(proxy, Hashable) `` is already ``True `` for an
266+ ``ObjectProxy `` instance without any explicit inheritance::
267+
268+ from collections.abc import Hashable
269+ import wrapt
270+
271+ isinstance(wrapt.ObjectProxy("s"), Hashable) # True
272+
273+ The runtime inheritance from the abstract base class adds nothing
274+ useful in this case, and brings in the ``ABCMeta `` metaclass which then
275+ collides with ``ObjectProxy `` as described above.
276+
277+ The recommended approach is to keep the runtime class hierarchy clean
278+ and present the type-checker-required relationship using typing
279+ constructs rather than runtime inheritance. When the annotation site is
280+ under your own control, the cleanest option is to define a
281+ ``typing.Protocol `` that captures the required structural shape and use
282+ that as the annotation, instead of inheriting from an abstract base
283+ class. ``ObjectProxy `` will structurally satisfy such a ``Protocol ``
284+ through the dunder methods it already forwards, with no inheritance and
285+ no runtime change at all.
286+
287+ When the annotation site is not under your own control and demands a
288+ nominal subtype of a specific abstract base class, the class can be
289+ declared twice in the same file, guarded by ``typing.TYPE_CHECKING ``::
290+
291+ from typing import TYPE_CHECKING
292+
293+ from wrapt import ObjectProxy
294+
295+ if TYPE_CHECKING:
296+ from collections.abc import Hashable
297+
298+ class Proxy(ObjectProxy, Hashable):
299+ ...
300+ else:
301+ class Proxy(ObjectProxy):
302+ pass
303+
304+ ``TYPE_CHECKING `` is ``False `` at runtime and ``True `` during static
305+ analysis, and both mypy and pyright honour this. The type checker sees
306+ the multi-base version, which satisfies whatever annotation required
307+ ``Hashable `` to appear in the inheritance chain. The Python interpreter
308+ only ever executes the ``else `` branch, so at runtime ``Proxy `` is a
309+ plain ``ObjectProxy `` subclass with no ``ABCMeta `` in the picture and
310+ the original ``TypeError `` does not occur.
311+
312+ The same trick generalises to any case where the view of a class
313+ presented to a static type checker needs to differ from the runtime
314+ class hierarchy. If several such declarations need to be maintained
315+ together it can be cleaner to lift them into a sibling ``.pyi `` stub
316+ file, which the type checker will honour in preference to the ``.py ``
317+ source. For a single case the inline ``TYPE_CHECKING `` form is usually
318+ enough.
319+
204320Using the json module with ObjectProxy
205321--------------------------------------
206322
0 commit comments