Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add typing.get_orig_bases and typing.get_orig_class #101688

Closed
Gobot1234 opened this issue Feb 8, 2023 · 8 comments
Closed

Add typing.get_orig_bases and typing.get_orig_class #101688

Gobot1234 opened this issue Feb 8, 2023 · 8 comments
Labels
3.12 bugs and security fixes stdlib Python modules in the Lib dir topic-typing type-feature A feature request or enhancement

Comments

@Gobot1234
Copy link
Contributor

Gobot1234 commented Feb 8, 2023

Feature or enhancement

typing has had type.__orig_bases__ and type.__orig_class__ for quite some time now, there is no stable API to access these attributes.

Pitch

I would like to propose adding typing.get_orig_bases as something like

@overload
def get_orig_bases(cls: type[object]) -> tuple[type[Any], ...] | None: ...
@overload
def get_orig_bases(cls: Any) -> None: ...
def get_orig_bases(cls: Any) -> tuple[type[Any], ...] | None:
	return getattr(cls, "__orig_bases__", None)

and typing.get_orig_class

@overload
def get_orig_class(cls: type[object]) -> GenericAlias | None: ...
@overload
def get_orig_class(cls: Any) -> None: ...
def get_orig_class(cls: Any) -> GenericAlias | None:
	return getattr(cls, "__orig_class__", None)

(side note, it might be possible to fully type get_orig_class it types.GenericAlias was generic over the __origin__ and __args__ i.e. Foo[int] == GenericAlias[Foo, int])

Linked PRs

@Gobot1234 Gobot1234 added the type-feature A feature request or enhancement label Feb 8, 2023
@AlexWaygood
Copy link
Member

AlexWaygood commented Feb 8, 2023

I'd be happy to accept this enhancement to typing.py. Users have previously requested this functionality in microsoft/pyright#3442 and python/typeshed#7811, but it's not really fixable by pyright or typeshed. The solution is to add the APIs suggested here to typing.py in CPython.

@AlexWaygood AlexWaygood added stdlib Python modules in the Lib dir 3.12 bugs and security fixes labels Feb 8, 2023
@sobolevn
Copy link
Member

sobolevn commented Feb 8, 2023

Aren't __orig_bases__ and __orig_class__ enough?

@Gobot1234
Copy link
Contributor Author

They aren't documented anywhere AFAIK, that would also be a decent solution to the problem

@AlexWaygood
Copy link
Member

AlexWaygood commented Feb 8, 2023

Aren't __orig_bases__ and __orig_class__ enough?

No — type checkers emit errors if you try to access them, as they're not included in the stub. As discussed in python/typeshed#7811 and python/typeshed#7827, adding them to the stub would in fact be impossible.

@hmc-cs-mdrissi
Copy link
Contributor

The main issue with defining those dunders in a stub file is they are defined for generic types but not other types. And Generic class is very special cased by type checkers/runtime that you can’t add it to the stub definition for Generic without risking type checker either ignoring it or causing buggy behavior.

Would be nice to have a public recommended way to access these for runtime type reflection. Simple implementation seems fine to me.

AlexWaygood added a commit that referenced this issue Apr 23, 2023
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
@AlexWaygood
Copy link
Member

#101827 has now been merged: types.get_original_bases will be available for retrieving a class's __orig_bases__ attribute in 3.12+.

Remaining issues to discuss

(1) Should we add a "getter function" for the __orig_class__ attribute? If not, should we document its existence?

As a refresher, these are the circumstances in which an object gets an __orig_class__ attribute:

>>> import typing
>>> T = typing.TypeVar("T")
>>> class Foo(typing.Generic[T]): ...
...
>>> f = Foo[int]()
>>> f.__orig_class__
__main__.Foo[int]
>>> m = typing.ChainMap[int, str]()
>>> m.__orig_class__
typing.ChainMap[int, str]
>>> import collections
>>> m2 = collections.ChainMap[int, str]()
>>> m2.__orig_class__
collections.ChainMap[int, str]

However, it only exists on instances of classes that don't have slots.

>>> d1 = dict[int, str]()
>>> d1.__orig_class__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'dict' object has no attribute '__orig_class__'

The inconsistency in whether this attribute is available or not might be quite confusing for users, so I'm not sure I like the idea of documenting the attribute or adding a getter function. It also doesn't appear to have been as much of a pain point for users as __orig_bases__ has been. @Gobot1234, could you elaborate more on your use case for accessing the __orig_class__ attribute?

(2) Should we document __orig_bases__ in the data model? Or is having types.get_original_bases() enough?

We could possibly document the attribute formally in the data model here: https://docs.python.org/3.12/reference/datamodel.html#resolving-mro-entries. However, doing so would be equivalent to a formal promise that the attribute will always exist as part of the language. I'm not sure I'm convinced that it's necessary for us to do that. PEP 560 describes the attribute as an implementation detail of the typing module, and it is not currently formally documented anywhere:

If an object that is not a class object appears in the tuple of bases of a class definition, then method __mro_entries__ is searched on it [...] The original bases are stored as orig_bases in the class namespace [...] NOTE: These two method names are reserved for use by the typing module and the generic types machinery, and any other use is discouraged.

@Gobot1234
Copy link
Contributor Author

So for now I think documenting __orig_class__ isn't probably worth it as as you pointed out, it's only conditionally available (I'm currently drafting up a PEP for better runtime type checking features and it will probably become public API following that if it's accepted or submitted ;)) but I currently use it for getting the specialised value of a type parameter at runtime.

Currently in a module I maintain there is an Inventory class which is generic over the types of items it stores and I want to get that at runtime so that I can instantiate the correct item class.
This either goes through subscription e.g. Inventory[SomeItemClass]() or class InventorySubclass(Inventory[AlwaysTheSameItem]): ... the first of which requires checking __orig_class__ the other runs through __orig_bases__ hence why I made this issue (full code here https://github.com/Gobot1234/steam.py/blob/82e74825d55db905dcb7414fa3f9313b67e31aba/steam/trade.py#L337-L340 if you're interested)

@AlexWaygood
Copy link
Member

Okay, great. I'll close this as completed for now, in that case, as I think adding a new slot for all Python objects will require a lot more discussion, so deserves to be treated separately :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.12 bugs and security fixes stdlib Python modules in the Lib dir topic-typing type-feature A feature request or enhancement
Projects
None yet
Development

No branches or pull requests

4 participants