• Odd types.get_original_bases() behavior for classes with generic bases

    From Chris Bouchard@21:1/5 to All on Mon Jul 24 22:59:17 2023
    (Apologies if this has already been discussed. I tried to search and didn't find
    anything relevant.)

    I was playing around with 3.12.0b4 this evening and noticed an odd (to me, at least) behavior with types.get_original_bases(). I hesitate to call it a bug because I think I understand *why* it's behaving this way—I'm just not sure it's
    desirable.


    T = typing.TypeVar("T")
    class FirstBase(typing.Generic[T]):
    ... pass
    ...
    class SecondBase(typing.Generic[T]):
    ... pass
    ...
    class First(FirstBase[int]):
    ... pass
    ...
    class Second(SecondBase[int]):
    ... pass
    ...
    class Example(First, Second):
    ... pass
    ...
    types.get_original_bases(Example)
    (FirstBase[int],)
    Example.__bases__
    (First, Second)
    types.get_original_bases(First)
    (FirstBase[int],)

    In other words, types.get_original_bases(Example) is returning the original base
    types for First, rather than its own.

    I believe this happens because __orig_bases__ is only set when one or more bases
    are not types. In this case both bases are types, so Example doesn't get its own
    __orig_bases__. Then when types.get_original_bases() tries to get __orig_bases__
    with getattr(cls, "__orig_bases__") or something morally equivalent, it searches
    the MRO and finds __orig_bases__ on First.

    The same thing also happens if all the bases are "bare" generic types.

    class First(typing.Generic[T]):
    ... pass
    ...
    class Second(typing.Generic[T]):
    ... pass
    ...
    class Example(First, Second):
    ... pass
    ...
    types.get_original_bases(Example)
    (typing.Generic[~T],)

    As I said, I'm not clear if this is a bug, or just an unfortunate but intended behavior. I would personally expect types.get_original_bases() to check if the type has its *own* __orig_bases__ attribute, and to fall back to __bases__ otherwise. The way it works today makes it unnecessarily difficult to write a function that, for example, recurses down a type's inheritance tree inspecting the original bases—I currently have to work around this behavior via hacks like
    checking "__orig_bases__" in cls.__dict__ or any(types.get_original_bases(cls) == types.get_original_bases(base) for base in cls.__bases__). (Unless I'm missing some simpler solution.)

    Is this something that could (should?) be addressed before 3.12 lands?

    Thanks,
    Chris Bouchard

    --- SoupGate-Win32 v1.05
    * Origin: fsxNet Usenet Gateway (21:1/5)