mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2025-01-04 18:12:12 +01:00
WIP: Get basic idea working
This commit is contained in:
parent
743517ce5c
commit
42103f0c73
2 changed files with 212 additions and 159 deletions
|
@ -20,10 +20,24 @@ import inspect
|
|||
import re
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from typing import Any, Iterator, Union
|
||||
from typing import Any, Iterator, Literal, Union
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
import telegram
|
||||
import telegram.ext
|
||||
import telegram.ext._utils.types
|
||||
import telegram._utils.types
|
||||
import telegram._utils.defaultvalue
|
||||
from socket import socket
|
||||
from apscheduler.job import Job as APSJob
|
||||
|
||||
tg_objects = vars(telegram)
|
||||
tg_objects.update(vars(telegram._utils.types))
|
||||
tg_objects.update(vars(telegram._utils.defaultvalue))
|
||||
tg_objects.update(vars(telegram.ext))
|
||||
tg_objects.update(vars(telegram.ext._utils.types))
|
||||
tg_objects.update(vars(telegram.ext._applicationbuilder))
|
||||
tg_objects.update({"socket": socket, "APSJob": APSJob})
|
||||
|
||||
|
||||
def _iter_own_public_methods(cls: type) -> Iterator[tuple[str, type]]:
|
||||
|
@ -49,6 +63,7 @@ class AdmonitionInserter:
|
|||
CLASS_ADMONITION_TYPES = ("use_in", "available_in", "returned_in")
|
||||
METHOD_ADMONITION_TYPES = ("shortcuts",)
|
||||
ALL_ADMONITION_TYPES = CLASS_ADMONITION_TYPES + METHOD_ADMONITION_TYPES
|
||||
# ALL_ADMONITION_TYPES = ("use_in",)
|
||||
|
||||
FORWARD_REF_PATTERN = re.compile(r"^ForwardRef\('(?P<class_name>\w+)'\)$")
|
||||
""" A pattern to find a class name in a ForwardRef typing annotation.
|
||||
|
@ -66,7 +81,7 @@ class AdmonitionInserter:
|
|||
|
||||
METHOD_NAMES_FOR_BOT_AND_APPBUILDER: typing.ClassVar[dict[type, str]] = {
|
||||
cls: tuple(m[0] for m in _iter_own_public_methods(cls)) # m[0] means we take only names
|
||||
for cls in (telegram.Bot, telegram.ext.ApplicationBuilder)
|
||||
for cls in (telegram.Bot, telegram.ext.ApplicationBuilder, telegram.ext.Application)
|
||||
}
|
||||
"""A dictionary mapping Bot and ApplicationBuilder classes to their relevant methods that will
|
||||
be mentioned in 'Returned in' and 'Use in' admonitions in other classes' docstrings.
|
||||
|
@ -78,7 +93,11 @@ class AdmonitionInserter:
|
|||
# dynamically determine which method to use to create a sub-dictionary
|
||||
admonition_type: getattr(self, f"_create_{admonition_type}")()
|
||||
for admonition_type in self.ALL_ADMONITION_TYPES
|
||||
# "use_in": self._create_use_in(),
|
||||
# "shortcuts": self._create_shortcuts(),
|
||||
# "available_in": self._create_available_in(),
|
||||
}
|
||||
# print(self.admonitions["use_in"])
|
||||
"""Dictionary with admonitions. Contains sub-dictionaries, one per admonition type.
|
||||
Each sub-dictionary matches bot methods (for "Shortcuts") or telegram classes (for other
|
||||
admonition types) to texts of admonitions, e.g.:
|
||||
|
@ -166,39 +185,47 @@ class AdmonitionInserter:
|
|||
name_of_inspected_class_in_docstr = self._generate_class_name_for_link(inspected_class)
|
||||
|
||||
# Parsing part of the docstring with attributes (parsing of properties follows later)
|
||||
docstring_lines = inspect.getdoc(inspected_class).splitlines()
|
||||
lines_with_attrs = []
|
||||
for idx, line in enumerate(docstring_lines):
|
||||
if line.strip() == "Attributes:":
|
||||
lines_with_attrs = docstring_lines[idx + 1 :]
|
||||
break
|
||||
# docstring_lines = inspect.getdoc(inspected_class).splitlines()
|
||||
# lines_with_attrs = []
|
||||
# for idx, line in enumerate(docstring_lines):
|
||||
# if line.strip() == "Attributes:":
|
||||
# lines_with_attrs = docstring_lines[idx + 1 :]
|
||||
# break
|
||||
|
||||
for line in lines_with_attrs:
|
||||
if not (line_match := attr_docstr_pattern.match(line)):
|
||||
continue
|
||||
# for line in lines_with_attrs:
|
||||
# if not (line_match := attr_docstr_pattern.match(line)):
|
||||
# continue
|
||||
|
||||
target_attr = line_match.group("attr_name")
|
||||
# a typing description of one attribute can contain multiple classes
|
||||
for match in single_class_name_pattern.finditer(line):
|
||||
name_of_class_in_attr = match.group("class_name")
|
||||
# target_attr = line_match.group("attr_name")
|
||||
# # a typing description of one attribute can contain multiple classes
|
||||
# for match in single_class_name_pattern.finditer(line):
|
||||
# name_of_class_in_attr = match.group("class_name")
|
||||
|
||||
# Writing to dictionary: matching the class found in the docstring
|
||||
# and its subclasses to the attribute of the class being inspected.
|
||||
# The class in the attribute docstring (or its subclass) is the key,
|
||||
# ReST link to attribute of the class currently being inspected is the value.
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
arg=name_of_class_in_attr,
|
||||
dict_of_methods_for_class=attrs_for_class,
|
||||
link=f":attr:`{name_of_inspected_class_in_docstr}.{target_attr}`",
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Available in' admonition "
|
||||
f"(admonition_inserter.py). Class {name_of_class_in_attr} present in "
|
||||
f"attribute {target_attr} of class {name_of_inspected_class_in_docstr}"
|
||||
f" could not be resolved. {e!s}"
|
||||
) from e
|
||||
# Writing to dictionary: matching the class found in the docstring
|
||||
# and its subclasses to the attribute of the class being inspected.
|
||||
# The class in the attribute docstring (or its subclass) is the key,
|
||||
# ReST link to attribute of the class currently being inspected is the value.
|
||||
|
||||
# best effort - args of __init__ means not all attributes are covered, but there is no
|
||||
# other way to get type hints of all attributes, other than doing ast parsing maybe.
|
||||
# (Docstring parsing was discontinued with the closing of #4414)
|
||||
type_hints = typing.get_type_hints(inspected_class.__init__, localns=tg_objects)
|
||||
class_attrs = [slot for slot in mro_slots(inspected_class) if not slot.startswith("_")]
|
||||
for target_attr in class_attrs:
|
||||
try:
|
||||
print(f"{inspected_class=},")
|
||||
self._resolve_arg_and_add_link(
|
||||
dict_of_methods_for_class=attrs_for_class,
|
||||
link=f":attr:`{name_of_inspected_class_in_docstr}.{target_attr}`",
|
||||
type_hints={target_attr: type_hints.get(target_attr)},
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Available in' admonition "
|
||||
f"(admonition_inserter.py). Class {inspected_class} present in "
|
||||
f"attribute {target_attr} of class {name_of_inspected_class_in_docstr}"
|
||||
f" could not be resolved. {e!s}"
|
||||
) from e
|
||||
|
||||
# Properties need to be parsed separately because they act like attributes but not
|
||||
# listed as attributes.
|
||||
|
@ -209,39 +236,26 @@ class AdmonitionInserter:
|
|||
if prop_name not in inspected_class.__dict__:
|
||||
continue
|
||||
|
||||
# 1. Can't use typing.get_type_hints because double-quoted type hints
|
||||
# (like "Application") will throw a NameError
|
||||
# 2. Can't use inspect.signature because return annotations of properties can be
|
||||
# hard to parse (like "(self) -> BD").
|
||||
# 3. fget is used to access the actual function under the property wrapper
|
||||
docstring = inspect.getdoc(getattr(inspected_class, prop_name).fget)
|
||||
if docstring is None:
|
||||
continue
|
||||
# fget is used to access the actual function under the property wrapper
|
||||
type_hints = typing.get_type_hints(getattr(inspected_class, prop_name).fget, localns=tg_objects)
|
||||
|
||||
first_line = docstring.splitlines()[0]
|
||||
if not prop_docstring_pattern.match(first_line):
|
||||
continue
|
||||
|
||||
for match in single_class_name_pattern.finditer(first_line):
|
||||
name_of_class_in_prop = match.group("class_name")
|
||||
|
||||
# Writing to dictionary: matching the class found in the docstring and its
|
||||
# subclasses to the property of the class being inspected.
|
||||
# The class in the property docstring (or its subclass) is the key,
|
||||
# ReST link to property of the class currently being inspected is the value.
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
arg=name_of_class_in_prop,
|
||||
dict_of_methods_for_class=attrs_for_class,
|
||||
link=f":attr:`{name_of_inspected_class_in_docstr}.{prop_name}`",
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Available in' admonition "
|
||||
f"(admonition_inserter.py). Class {name_of_class_in_prop} present in "
|
||||
f"property {prop_name} of class {name_of_inspected_class_in_docstr}"
|
||||
f" could not be resolved. {e!s}"
|
||||
) from e
|
||||
# Writing to dictionary: matching the class found in the docstring and its
|
||||
# subclasses to the property of the class being inspected.
|
||||
# The class in the property docstring (or its subclass) is the key,
|
||||
# ReST link to property of the class currently being inspected is the value.
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
dict_of_methods_for_class=attrs_for_class,
|
||||
link=f":attr:`{name_of_inspected_class_in_docstr}.{prop_name}`",
|
||||
type_hints={prop_name: type_hints.get(target_attr)},
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Available in' admonition "
|
||||
f"(admonition_inserter.py). Class {inspected_class} present in "
|
||||
f"property {prop_name} of class {name_of_inspected_class_in_docstr}"
|
||||
f" could not be resolved. {e!s}"
|
||||
) from e
|
||||
|
||||
return self._generate_admonitions(attrs_for_class, admonition_type="available_in")
|
||||
|
||||
|
@ -256,22 +270,22 @@ class AdmonitionInserter:
|
|||
|
||||
for cls, method_names in self.METHOD_NAMES_FOR_BOT_AND_APPBUILDER.items():
|
||||
for method_name in method_names:
|
||||
sig = inspect.signature(getattr(cls, method_name))
|
||||
ret_annot = sig.return_annotation
|
||||
|
||||
method_link = self._generate_link_to_method(method_name, cls)
|
||||
arg = getattr(cls, method_name)
|
||||
print(arg, method_name)
|
||||
ret_type_hint = typing.get_type_hints(arg, localns=tg_objects)
|
||||
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
arg=ret_annot,
|
||||
dict_of_methods_for_class=methods_for_class,
|
||||
link=method_link,
|
||||
type_hints={"return": ret_type_hint.get("return")},
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Returned in' admonition "
|
||||
f"(admonition_inserter.py). {cls}, method {method_name}. "
|
||||
f"Couldn't resolve type hint in return annotation {ret_annot}. {e!s}"
|
||||
f"Couldn't resolve type hint in return annotation {ret_type_hint}. {e!s}"
|
||||
) from e
|
||||
|
||||
return self._generate_admonitions(methods_for_class, admonition_type="returned_in")
|
||||
|
@ -330,22 +344,20 @@ class AdmonitionInserter:
|
|||
for method_name in method_names:
|
||||
method_link = self._generate_link_to_method(method_name, cls)
|
||||
|
||||
sig = inspect.signature(getattr(cls, method_name))
|
||||
parameters = sig.parameters
|
||||
|
||||
for param in parameters.values():
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
arg=param.annotation,
|
||||
dict_of_methods_for_class=methods_for_class,
|
||||
link=method_link,
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Use in' admonition "
|
||||
f"(admonition_inserter.py). {cls}, method {method_name}, parameter "
|
||||
f"{param}: Couldn't resolve type hint {param.annotation}. {e!s}"
|
||||
) from e
|
||||
arg = getattr(cls, method_name)
|
||||
param_type_hints = typing.get_type_hints(arg, localns=tg_objects)
|
||||
param_type_hints.pop("return", None)
|
||||
try:
|
||||
self._resolve_arg_and_add_link(
|
||||
dict_of_methods_for_class=methods_for_class,
|
||||
link=method_link,
|
||||
type_hints=param_type_hints,
|
||||
)
|
||||
except NotImplementedError as e:
|
||||
raise NotImplementedError(
|
||||
"Error generating Sphinx 'Use in' admonition "
|
||||
f"(admonition_inserter.py). {cls}, method {method_name}, parameter "
|
||||
) from e
|
||||
|
||||
return self._generate_admonitions(methods_for_class, admonition_type="use_in")
|
||||
|
||||
|
@ -448,6 +460,8 @@ class AdmonitionInserter:
|
|||
|
||||
@staticmethod
|
||||
def _iter_subclasses(cls: type) -> Iterator:
|
||||
if not hasattr(cls, "__subclasses__") or cls is telegram.TelegramObject:
|
||||
return iter([])
|
||||
return (
|
||||
# exclude private classes
|
||||
c
|
||||
|
@ -457,9 +471,9 @@ class AdmonitionInserter:
|
|||
|
||||
def _resolve_arg_and_add_link(
|
||||
self,
|
||||
arg: Any,
|
||||
dict_of_methods_for_class: defaultdict,
|
||||
link: str,
|
||||
type_hints: dict[str, type],
|
||||
) -> None:
|
||||
"""A helper method. Tries to resolve the arg into a valid class. In case of success,
|
||||
adds the link (to a method, attribute, or property) for that class' and its subclasses'
|
||||
|
@ -467,7 +481,9 @@ class AdmonitionInserter:
|
|||
|
||||
**Modifies dictionary in place.**
|
||||
"""
|
||||
for cls in self._resolve_arg(arg):
|
||||
type_hints.pop("self", None)
|
||||
|
||||
for cls in self._resolve_arg(type_hints):
|
||||
# When trying to resolve an argument from args or return annotation,
|
||||
# the method _resolve_arg returns None if nothing could be resolved.
|
||||
# Also, if class was resolved correctly, "telegram" will definitely be in its str().
|
||||
|
@ -479,88 +495,123 @@ class AdmonitionInserter:
|
|||
for subclass in self._iter_subclasses(cls):
|
||||
dict_of_methods_for_class[subclass].add(link)
|
||||
|
||||
def _resolve_arg(self, arg: Any) -> Iterator[Union[type, None]]:
|
||||
def _resolve_arg(self, type_hints: dict[str, type]) -> list[type]:
|
||||
"""Analyzes an argument of a method and recursively yields classes that the argument
|
||||
or its sub-arguments (in cases like Union[...]) belong to, if they can be resolved to
|
||||
telegram or telegram.ext classes.
|
||||
|
||||
Raises `NotImplementedError`.
|
||||
"""
|
||||
telegram_classes = set()
|
||||
|
||||
origin = typing.get_origin(arg)
|
||||
def recurse_type(typ):
|
||||
if hasattr(typ, '__origin__'): # For generic types like Union, List, etc.
|
||||
# Make sure it's not a telegram.ext generic type (e.g. ContextTypes[...])
|
||||
org = typing.get_origin(typ)
|
||||
if "telegram.ext" in str(org):
|
||||
telegram_classes.add(org)
|
||||
|
||||
if (
|
||||
origin in (collections.abc.Callable, typing.IO)
|
||||
or arg is None
|
||||
# no other check available (by type or origin) for these:
|
||||
or str(type(arg)) in ("<class 'typing._SpecialForm'>", "<class 'ellipsis'>")
|
||||
):
|
||||
pass
|
||||
|
||||
# RECURSIVE CALLS
|
||||
# for cases like Union[Sequence....
|
||||
elif origin in (
|
||||
Union,
|
||||
collections.abc.Coroutine,
|
||||
collections.abc.Sequence,
|
||||
):
|
||||
for sub_arg in typing.get_args(arg):
|
||||
yield from self._resolve_arg(sub_arg)
|
||||
|
||||
elif isinstance(arg, typing.TypeVar):
|
||||
# gets access to the "bound=..." parameter
|
||||
yield from self._resolve_arg(arg.__bound__)
|
||||
# END RECURSIVE CALLS
|
||||
|
||||
elif isinstance(arg, typing.ForwardRef):
|
||||
m = self.FORWARD_REF_PATTERN.match(str(arg))
|
||||
# We're sure it's a ForwardRef, so, unless it belongs to known exceptions,
|
||||
# the class must be resolved.
|
||||
# If it isn't resolved, we'll have the program throw an exception to be sure.
|
||||
try:
|
||||
cls = self._resolve_class(m.group("class_name"))
|
||||
except AttributeError as exc:
|
||||
# skip known ForwardRef's that need not be resolved to a Telegram class
|
||||
if self.FORWARD_REF_SKIP_PATTERN.match(str(arg)):
|
||||
pass
|
||||
else:
|
||||
raise NotImplementedError(f"Could not process ForwardRef: {arg}") from exc
|
||||
args = typing.get_args(typ)
|
||||
# print(f"In recurse_type, found __origin__ {typ=}, {args=}")
|
||||
for ar in args:
|
||||
recurse_type(ar)
|
||||
elif isinstance(typ, typing.TypeVar):
|
||||
# gets access to the "bound=..." parameter
|
||||
recurse_type(typ.__bound__)
|
||||
elif inspect.isclass(typ) and "telegram" in inspect.getmodule(typ).__name__:
|
||||
# print(f"typ is a class and inherits from TelegramObject: {typ=}")
|
||||
telegram_classes.add(typ)
|
||||
else:
|
||||
yield cls
|
||||
pass
|
||||
# print(f"typ is not a class or doesn't inherit from TelegramObject: {typ=}. The "
|
||||
# f"type is: {type(typ)=}")
|
||||
# print(f"{inspect.isclass(typ)=}")
|
||||
# if inspect.isclass(typ):
|
||||
# print(f"{inspect.getmodule(typ).__name__=}")
|
||||
|
||||
# For custom generics like telegram.ext._application.Application[~BT, ~CCT, ~UD...].
|
||||
# This must come before the check for isinstance(type) because GenericAlias can also be
|
||||
# recognized as type if it belongs to <class 'types.GenericAlias'>.
|
||||
elif str(type(arg)) in (
|
||||
"<class 'typing._GenericAlias'>",
|
||||
"<class 'types.GenericAlias'>",
|
||||
"<class 'typing._LiteralGenericAlias'>",
|
||||
):
|
||||
if "telegram" in str(arg):
|
||||
# get_origin() of telegram.ext._application.Application[~BT, ~CCT, ~UD...]
|
||||
# will produce <class 'telegram.ext._application.Application'>
|
||||
yield origin
|
||||
print()
|
||||
print(f"in _resolve_arg {type_hints=}")
|
||||
for param_name, type_hint in type_hints.items():
|
||||
print(f"{param_name=}", f"{type_hint=}")
|
||||
if type_hint is None:
|
||||
continue
|
||||
recurse_type(type_hint)
|
||||
print(f"{telegram_classes=}")
|
||||
return list(telegram_classes)
|
||||
# origin = typing.get_origin(arg)
|
||||
|
||||
elif isinstance(arg, type):
|
||||
if "telegram" in str(arg):
|
||||
yield arg
|
||||
# if (
|
||||
# origin in (collections.abc.Callable, typing.IO)
|
||||
# or arg is None
|
||||
# # no other check available (by type or origin) for these:
|
||||
# or str(type(arg)) in ("<class 'typing._SpecialForm'>", "<class 'ellipsis'>")
|
||||
# ):
|
||||
# pass
|
||||
|
||||
# For some reason "InlineQueryResult", "InputMedia" & some others are currently not
|
||||
# recognized as ForwardRefs and are identified as plain strings.
|
||||
elif isinstance(arg, str):
|
||||
# args like "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]" can be recognized as strings.
|
||||
# Remove whatever is in the square brackets because it doesn't need to be parsed.
|
||||
arg = re.sub(r"\[.+]", "", arg)
|
||||
# # RECURSIVE CALLS
|
||||
# # for cases like Union[Sequence....
|
||||
# elif origin in (
|
||||
# Union,
|
||||
# collections.abc.Coroutine,
|
||||
# collections.abc.Sequence,
|
||||
# ):
|
||||
# for sub_arg in typing.get_args(arg):
|
||||
# yield from self._resolve_arg(sub_arg)
|
||||
|
||||
cls = self._resolve_class(arg)
|
||||
# Here we don't want an exception to be thrown since we're not sure it's ForwardRef
|
||||
if cls is not None:
|
||||
yield cls
|
||||
# elif isinstance(arg, typing.TypeVar):
|
||||
# # gets access to the "bound=..." parameter
|
||||
# yield from self._resolve_arg(arg.__bound__)
|
||||
# # END RECURSIVE CALLS
|
||||
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Cannot process argument {arg} of type {type(arg)} (origin {origin})"
|
||||
)
|
||||
# elif isinstance(arg, typing.ForwardRef):
|
||||
# m = self.FORWARD_REF_PATTERN.match(str(arg))
|
||||
# # We're sure it's a ForwardRef, so, unless it belongs to known exceptions,
|
||||
# # the class must be resolved.
|
||||
# # If it isn't resolved, we'll have the program throw an exception to be sure.
|
||||
# try:
|
||||
# cls = self._resolve_class(m.group("class_name"))
|
||||
# except AttributeError as exc:
|
||||
# # skip known ForwardRef's that need not be resolved to a Telegram class
|
||||
# if self.FORWARD_REF_SKIP_PATTERN.match(str(arg)):
|
||||
# pass
|
||||
# else:
|
||||
# raise NotImplementedError(f"Could not process ForwardRef: {arg}") from exc
|
||||
# else:
|
||||
# yield cls
|
||||
|
||||
# # For custom generics like telegram.ext._application.Application[~BT, ~CCT, ~UD...].
|
||||
# # This must come before the check for isinstance(type) because GenericAlias can also be
|
||||
# # recognized as type if it belongs to <class 'types.GenericAlias'>.
|
||||
# elif str(type(arg)) in (
|
||||
# "<class 'typing._GenericAlias'>",
|
||||
# "<class 'types.GenericAlias'>",
|
||||
# "<class 'typing._LiteralGenericAlias'>",
|
||||
# ):
|
||||
# if "telegram" in str(arg):
|
||||
# # get_origin() of telegram.ext._application.Application[~BT, ~CCT, ~UD...]
|
||||
# # will produce <class 'telegram.ext._application.Application'>
|
||||
# yield origin
|
||||
|
||||
# elif isinstance(arg, type):
|
||||
# if "telegram" in str(arg):
|
||||
# yield arg
|
||||
|
||||
# # For some reason "InlineQueryResult", "InputMedia" & some others are currently not
|
||||
# # recognized as ForwardRefs and are identified as plain strings.
|
||||
# elif isinstance(arg, str):
|
||||
# # args like "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]" can be recognized as strings.
|
||||
# # Remove whatever is in the square brackets because it doesn't need to be parsed.
|
||||
# arg = re.sub(r"\[.+]", "", arg)
|
||||
|
||||
# cls = self._resolve_class(arg)
|
||||
# # Here we don't want an exception to be thrown since we're not sure it's ForwardRef
|
||||
# if cls is not None:
|
||||
# yield cls
|
||||
|
||||
# else:
|
||||
# raise NotImplementedError(
|
||||
# f"Cannot process argument {arg} of type {type(arg)} (origin {origin})"
|
||||
# )
|
||||
|
||||
@staticmethod
|
||||
def _resolve_class(name: str) -> Union[type, None]:
|
||||
|
|
|
@ -17,10 +17,12 @@
|
|||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
|
||||
# This module is intentionally named without "test_" prefix.
|
||||
# These tests are supposed to be run on GitHub when building docs.
|
||||
# The tests require Python 3.9+ (just like AdmonitionInserter being tested),
|
||||
# so they cannot be included in the main suite while older versions of Python are supported.
|
||||
"""
|
||||
This module is intentionally named without "test_" prefix.
|
||||
These tests are supposed to be run on GitHub when building docs.
|
||||
The tests require Python 3.10+ (just like AdmonitionInserter being tested),
|
||||
so they cannot be included in the main suite while older versions of Python are supported.
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
|
||||
|
|
Loading…
Reference in a new issue