wrapt icon indicating copy to clipboard operation
wrapt copied to clipboard

Inconsistent behavior in issubclass

Open Atry opened this issue 1 month ago • 8 comments

$ python
Python 3.13.8 (main, Oct  7 2025, 12:01:51) [GCC 14.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
Ctrl click to launch VS Code Native REPL
>>> import wrapt
>>> import enum
>>> issubclass(wrapt.BaseObjectProxy(enum.StrEnum),enum.Enum)
True
>>> issubclass(wrapt.BaseObjectProxy(enum.StrEnum),enum.StrEnum)
False
>>> issubclass(enum.StrEnum,enum.StrEnum)
True
>>> issubclass(enum.StrEnum,wrapt.BaseObjectProxy(enum.StrEnum))
False

wrapt version: 2.0.1

Atry avatar Nov 13 '25 17:11 Atry

Will need to investigate, wrapping type objects with a proxy can be problematic.

What should work okay are tests like:

from enum import StrEnum

class State(StrEnum):
    STARTED = "started"
    STOPPED = "stopped"
    PAUSED = "paused"

isinstance(wrapt.BaseObjectProxy(State.STARTED), enum.Enum)

isinstance(wrapt.BaseObjectProxy(State.STARTED), enum.StrEnum)

That is, wrapping an instance of an enum.

Can you give the real world example demonstrating why you are wrapping the type object?

Wrapping types, such as applying a decorator to a type, has always had issues. Eg.,

  • https://wrapt.readthedocs.io/en/master/issues.html#deriving-from-decorated-class

GrahamDumpleton avatar Nov 13 '25 21:11 GrahamDumpleton

Okay, looks like I may have tried to deal with issubclass() checks in decorators by having in the function wrappers used for decorators:

    def __subclasscheck__(self, subclass):
        # This is a special method used by issubclass() to make checks
        # about inheritance of classes. We need to upwrap any object
        # proxy. Not wanting to add this to ObjectProxy as not sure of
        # broader implications of doing that. Thus restrict to
        # FunctionWrapper used by decorators.

        if hasattr(subclass, "__wrapped__"):
            return issubclass(subclass.__wrapped__, self.__wrapped__)
        else:
            return issubclass(subclass, self.__wrapped__)

As the comment says, this wasn't added to main ObjectProxy class as wasn't sure what implications of doing so would be.

So would need to investigate what implications might be of adding this to BaseObjectProxy.

GrahamDumpleton avatar Nov 13 '25 21:11 GrahamDumpleton

Can you give the real world example demonstrating why you are wrapping the type object?

I am building a recording/replaying testing framework (similar to https://vcrpy.readthedocs.io/) to monkey patch classes that would be used in abitrary ways (including instantiations, class method calls and issubclass calls) in functions being tested.

Atry avatar Nov 13 '25 21:11 Atry

Can you add that implementation of __subclasscheck__ above to your custom object proxy class and see if the issue is resolved? If it works, or we can work out a variation of it that does, then we can look at adding it to BaseObjectProxy.

GrahamDumpleton avatar Nov 13 '25 21:11 GrahamDumpleton

issubclass(proxy := wrapt.BaseObjectProxy(enum.StrEnum), enum.StrEnum) -> triggers proxy.__bases__, which does not include StrEnum, thus returning False. The reason is that it actually triggers StrEnum.__subclasscheck__, not proxy.__subclasscheck__ and StrEnum.__subclasscheck__ internally accesses proxy.__bases__ property.

Atry avatar Nov 16 '25 16:11 Atry

I think issubclass(wrapt.BaseObjectProxy(enum.StrEnum),enum.StrEnum) returning False might be an accepted behavior.

Supposing in tests, you do monkey patching:

my_lib.MyClass = MyProxy(my_lib.MyClass)

Then the original my_lib.MyClass should become inaccessible from user code, therefore the user code would never trigger issubclass(<patched my_lib.MyClass>, <original my_lib.MyClass>)

Atry avatar Nov 16 '25 16:11 Atry

issubclass(enum.StrEnum, proxy := wrapt.BaseObjectProxy(enum.StrEnum)) triggers proxy.__subclasscheck__, the instance method, because BaseObjectProxy is now the metaclass of the proxy if you consider proxy itself as a type. However currently BaseObjectProxy does not forward __subclasscheck__ to __wrapped__.

Atry avatar Nov 16 '25 16:11 Atry

Since for issubclass(enum.StrEnum, enum.StrEnum) you get True, I would not expect issubclass(wrapt.BaseObjectProxy(enum.StrEnum), enum.StrEnum) to return False.

FWIW, the whole issubclass() issue and why never did it on ObjectProxy may have been complicated by:

  • https://github.com/python/cpython/issues/89010

I can't remember. I still need to sit down and properly work through this issue. My simple first check using __subclasscheck__ override didn't work, but I may have not added it in properly or constructed my test wrongly.

GrahamDumpleton avatar Nov 17 '25 05:11 GrahamDumpleton