Python 3.7: проверьте, является ли аннотация типа "подклассом" универсального

Я пытаюсь найти надежный/кросс-версия (3. 5+) способ проверки, является ли аннотация типа "подклассом" данного универсального типа (т.е. получить универсальный тип из объекта аннотации типа).

На Python 3.5/3.6 он работает на одном дыхании, как и следовало ожидать:

>>> from typing import List

>>> isinstance(List[str], type)
True

>>> issubclass(List[str], List)
True

Хотя на 3.7 похоже, что экземпляры универсальных типов более не являются экземплярами type, поэтому произойдет сбой:

>>> from typing import List

>>> isinstance(List[str], type)
False

>>> issubclass(List[str], List)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.7/typing.py", line 716, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

Другие идеи, которые приходят на ум, проверяют фактический тип экземпляра, но:

Python 3.6/3.5:

>>> type(List[str])
<class 'typing.GenericMeta'>

Python 3.7:

>>> type(List[str])
<class 'typing._GenericAlias'>

Но это на самом деле не дает каких-либо дополнительных указаний относительно того, какой из них является действительным универсальным типом (может быть и не List); кроме того, кажется, что делать проверку таким образом неправильно, тем более что _GenericAlias теперь стал "закрытым" типом (обратите внимание на подчеркивание).

Еще одна вещь, которую можно проверить, это аргумент __origin__ для типа, но это тоже не похоже на правильный способ сделать это.

И это все еще отличается на 3.7:

>>> List[str].__origin__
<class 'list'>

в то время как 3.5/3.6:

>>> List[str].__origin__
typing.List

Я искал "правильный" способ сделать это, но не нашел его в поиске Python docs/google.

Теперь я предполагаю, что должен быть чистый способ сделать эту проверку, так как такие инструменты, как mypy, будут полагаться на него при выполнении проверок типов..?

Обновление: о сценарии использования

Хорошо, добавив немного больше контекста здесь..

Итак, мой вариант использования для этого - использовать интроспекцию для сигнатур функций (типы аргументов/значения по умолчанию, тип возвращаемого значения, строка документации), чтобы автоматически генерировать для них схему GraphQL (таким образом уменьшая количество шаблонов).

Я все еще немного обеспокоен тем, будет ли это хорошей идеей или нет.

Мне это нравится с точки зрения удобства использования (нет необходимости изучать еще один способ объявления сигнатуры вашей функции: просто аннотируйте ваши типы обычным способом); посмотрите два примера кода, чтобы понять, что я имею в виду: https://github.com/rshk/pyql

Интересно, добавляет ли поддержка общих типов (списков, диктов, объединений,...) с использованием типов из typing слишком много "черной магии", которая может неожиданно оборваться. (На данный момент это не большая проблема, но как насчет будущих версий Python после 3.7? Это станет кошмаром по обслуживанию?).

Конечно, альтернативой было бы просто использовать пользовательскую аннотацию типа, которая поддерживает более надежную/будущую проверку, например: https://github.com/rshk/pyql/blob/master/pyql/schema/types/core.py#L337-L339

... но с другой стороны, это заставит людей помнить, что они должны использовать пользовательскую аннотацию типа. Более того, я не уверен, как mypy справится с этим (я предполагаю, что где-то должно быть объявление, чтобы сказать, что пользовательский тип полностью совместим с typing.List..? По-прежнему звучит хакерски).

(Я в основном спрашиваю предложения по двум подходам и, самое главное, любые плюсы/минусы двух альтернатив, которые я мог бы пропустить. Надеюсь, это не станет "слишком широким" для SO..).

Ответ 1

Прежде всего: не существует API, определенного для интроспективных объектов хинтинга, как определено модулем typing. Ожидается, что инструменты подсказки типов будут иметь дело с исходным кодом, то есть с текстом, а не с объектами Python во время выполнения; mypy не анализирует объекты List[str], а обрабатывает абстрактное синтаксическое дерево вашего исходного кода.

Таким образом, хотя вы всегда можете получить доступ к таким атрибутам, как __origin__, вы по существу имеете дело с деталями реализации (внутренняя бухгалтерия), и эти детали реализации могут и будут меняться от версии к версии.

Тем не менее, основной участник mypy/typing создал модуль typing_inspect для разработки API самоанализа для подсказок типов. Проект все еще документирует себя как экспериментальный, и вы можете ожидать, что со временем он тоже изменится, пока он больше не будет экспериментальным. Здесь это не решит вашу проблему, так как он не поддерживает Python 3.5, и get_origin() возвращает те же самые значения, __origin__ предоставляет атрибут __origin__.

Со всеми этими предостережениями в Python 3.5/Python 3.6 вы хотите получить доступ к __extra__; это базовый встроенный тип, используемый для поддержки поддержки issubclass()/isinstance() которую библиотека изначально реализовала (но с тех пор удалила в 3.7):

def get_type_class(typ):
    try:
        # Python 3.5 / 3.6
        return typ.__extra__
    except AttributeError:
        # Python 3.7
        return typ.__origin__

Это создает <class 'list'> в Python 3.5 и выше, независимо. Он по-прежнему использует внутренние детали реализации и вполне может сломаться в будущих версиях Python.

Ответ 2

Обратите внимание, что Python 3.8 добавляет typing.get_origin() и typing.get_args() для поддержки базового самоанализа.