python / cpython

The Python programming language
https://www.python.org
Other
62.28k stars 29.92k forks source link

`get_original_bases` does not return what `cls.__orig_bases__` returns #122988

Open TiborVoelcker opened 4 weeks ago

TiborVoelcker commented 4 weeks ago

Bug report

Bug description:

The types documentation states for the types.get_original_bases function:

For classes that have an __orig_bases__ attribute, this function returns the value of cls.__orig_bases__. For classes without the __orig_bases__ attribute, cls.__bases__ is returned.

I need the functionality of cls.__orig_bases__, but need to use types.get_original_bases to make the type checker happy.

A short MRE:

from types import get_original_bases
from typing import Generic, TypeVar

T = TypeVar("T")

class One(Generic[T]):
    pass

class Two(One[int]):
    pass

class Three(Two):
    pass

assert get_original_bases(One) == One.__orig_bases__
assert get_original_bases(Two) == Two.__orig_bases__
assert get_original_bases(Three) == Three.__orig_bases__

# Traceback (most recent call last):
#   File "c:\<cut>\test.py", line 34, in <module>
#     assert get_original_bases(Three) == Three.__orig_bases__
#            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# AssertionError

This is a easy solve, I would be happy to add a PR. Change here:

    try:
-       return cls.__dict__.get("__orig_bases__", cls.__bases__)
+       return getattr(cls, "__orig_bases__", cls.__bases__)
    except AttributeError:
        raise TypeError(
            f"Expected an instance of type, not {type(cls).__name__!r}"
        ) from None

To show this works:

from typing import Generic, TypeVar

T = TypeVar("T")

class One(Generic[T]):
    pass

class Two(One[int]):
    pass

class Three(Two):
    pass

def better_get_original_bases(cls):
    try:
        return getattr(cls, "__orig_bases__", cls.__bases__)
    except AttributeError:
        raise TypeError(
            f"Expected an instance of type, not {type(cls).__name__!r}"
        ) from None

assert better_get_original_bases(One) == One.__orig_bases__
assert better_get_original_bases(Two) == Two.__orig_bases__
assert better_get_original_bases(Three) == Three.__orig_bases__

# <No output>

CPython versions tested on:

3.12

Operating systems tested on:

Windows

JelleZijlstra commented 4 weeks ago

This is behaving as it should; it returns the original bases of the class it is called on, not its base classes. Perhaps we should improve the documentation to make it clear that the function is not necessarily completely equivalent to accessing the attribute.

Accessing the attribute directly is also likely to cause incorrect behavior when metaclasses are involved.

TiborVoelcker commented 4 weeks ago

Is there a way to get the generic type from the Three object in my example then? The __orig_bases__ seems to be exactly what I want, but its use seems to be not really documented/discouraged. Also, the issue with the type checkers remains.

JelleZijlstra commented 4 weeks ago

I don't know what your exact use case is, but the general answer would be to iterate over the bases of the class you're interested in and call get_original_bases() on each base class. Accessing the attribute directly may do what you want in simple cases, but likely returns wrong results in cases like multiple inheritance or custom metaclasses.

TiborVoelcker commented 4 weeks ago

Iterating over the bases would work, yes. Thank you.

Also, now I finally get what you meant with "the original bases of the class it is called on, not its base classes". get_original_bases() is meant to operate only on the direct bases (parents) of the supplied class. If it does not contain a generic, there is nothing to do, so it just returns the normal bases. I was confused as I somehow thought that "bases" means "the last parent" / "the end of the class mro".

Reading the docs now, I see that this is specifically said.

I would close this issue, but maybe keep it open for the documentation clarification?