Skip to content

Calling issubclass in __init_subclass__ with ABCMeta uses wrong _abc_cache. #116093

@timleslie

Description

@timleslie

Bug report

Bug description:

In the following minimal example, we get unexpected output:

from abc import ABCMeta

class BaseA(metaclass=ABCMeta):
    ...

class BaseB(metaclass=ABCMeta):
    def __init_subclass__(cls):
        issubclass(BaseA, BaseA)
        issubclass(BaseA, BaseB)

class DerivedClass(BaseA, BaseB):
    ...

print(f"Is BaseA a subclass of BaseB? (expect False): {issubclass(BaseA, BaseB)}")

Output:

 Is BaseA a subclass of BaseB? (expect False): True

We can drill in a bit deeper with the following expanded example:

from abc import ABCMeta

class BaseA(metaclass=ABCMeta):
    ...

class BaseB(metaclass=ABCMeta):
    def __init_subclass__(cls):
        _DerivedClass = BaseB.__subclasses__()[0]
        print("issubclass(BaseA, BaseA):", issubclass(BaseA, BaseA), "<-- Obviously true")
        print("issubclass(BaseA, _DerivedClass):", issubclass(BaseA, _DerivedClass), "<-- This is unexpected!!!")
        print("issubclass(_DerivedClass, BaseB)", issubclass(_DerivedClass, BaseB), "<-- This is expected")
        # c.f. https://github.com/python/cpython/blob/3.11/Lib/_py_abc.py#L140
        print("issubclass(BaseA, BaseB)", issubclass(BaseA, BaseB), "<-- This is true, because we end up checking issubclass(BaseA, _DerivedClass) within this check.")
        print("And now the result issubclass(BaseA, BaseB) = True is cached, so...")

class DerivedClass(BaseA, BaseB):
    ...

print(f"Is BaseA a subclass of BaseB? (expect False): {issubclass(BaseA, BaseB)}")

Output:

issubclass(BaseA, BaseA): True <-- Obviously true
issubclass(BaseA, _DerivedClass): True <-- This is unexpected!!!
issubclass(_DerivedClass, BaseB) True <-- This is expected
issubclass(BaseA, BaseB) True <-- This is true, because we end up checking issubclass(BaseA, _DerivedClass) within this check.
And now the result issubclass(BaseA, BaseB) = True is cached, so...
Is BaseA a subclass of BaseB? (expect False): True

Because issubclass(BaseA, _DerivedClass) is True, issubclass(BaseA, BaseB) is also True (c.f. checking subclasses at https://github.com/python/cpython/blob/3.11/Lib/_py_abc.py#L140).

To understand why issubclass(BaseA, _DerivedClass) is True, we can consider the following version of the script:

from abc import ABCMeta

class BaseA(metaclass=ABCMeta):
    ...

class BaseB(metaclass=ABCMeta):
    def __init_subclass__(cls):
        _DerivedClass = BaseB.__subclasses__()[0]
        print("BaseA:       ", BaseA._abc_impl)
        print("DerivedClass:", _DerivedClass._abc_impl, "<-- Same as BaseA!")
        print("BaseB:       ", BaseB._abc_impl)
        print()

        issubclass(BaseA, BaseA)

        ABCMeta._dump_registry(BaseA)
        print()
        ABCMeta._dump_registry(_DerivedClass)

        issubclass(BaseA, _DerivedClass)

class DerivedClass(BaseA, BaseB):
    ...

Output:

BaseA:        <_abc._abc_data object at 0x7fa12e543f00>
DerivedClass: <_abc._abc_data object at 0x7fa12e543f00> <-- Same as BaseA!
BaseB:        <_abc._abc_data object at 0x7fa12e541e80>

Class: __main__.BaseA
Inv. counter: 24
_abc_registry: set()
_abc_cache: {<weakref at 0x7fa12e510ef0; to 'ABCMeta' at 0x1c4d790 (BaseA)>}
_abc_negative_cache: set()
_abc_negative_cache_version: 24

Class: __main__.DerivedClass
Inv. counter: 24
_abc_registry: set()
_abc_cache: {<weakref at 0x7fa12e510ef0; to 'ABCMeta' at 0x1c4d790 (BaseA)>}
_abc_negative_cache: set()
_abc_negative_cache_version: 24

We can see that BaseA and DerivedClass are sharing the same ._abc_impl object! Because we have already called issubclass(BaseA, BaseA), this cache is populated, saying "BaseA is a subclass of BaseA". But because DerivedClass is sharing that same cache, the cache also implies "BaseA is a subclass of DerivedClass".

The reason BaseA and DerivedClass are sharing the same ._abc_impl has to do with the fact that we're inside the __init_subclass__ method. This method is called as part of type.__new__. Looking at ABCMeta.__new__(), we can see that type.__new__ is called before the code that sets up the various caches/registries (c.f. https://github.com/python/cpython/blob/3.11/Lib/_py_abc.py#L35).

As such, when looking for DerivedClass._abc_impl, we fall back to the parent class, BaseA._abc_impl.

Summary

The bug here is that ABCMeta.__new__ calls type.__new__ (which in turn calls the user supplied .__init_subclass__ before it's had a chance to set up the various pieces of ._abc_impl. This can cause issubclass to provide incorrect responses when called inside __init_subclass__, and for these incorrect response to be cached, and therefore propagated to calls outside of __init_subclass__.

CPython versions tested on:

3.11

Operating systems tested on:

macOS

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy