2023-02-05 18:09:55 +01:00
|
|
|
#
|
|
|
|
# A library that provides a Python interface to the Telegram Bot API
|
2024-02-19 20:06:25 +01:00
|
|
|
# Copyright (C) 2015-2024
|
2023-02-05 18:09:55 +01:00
|
|
|
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Lesser Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Lesser Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Lesser Public License
|
|
|
|
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
|
|
|
import collections.abc
|
2024-08-07 21:56:46 +02:00
|
|
|
import contextlib
|
2023-02-05 18:09:55 +01:00
|
|
|
import inspect
|
|
|
|
import re
|
|
|
|
import typing
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from sphinx.application import Sphinx
|
|
|
|
|
|
|
|
import telegram
|
|
|
|
import telegram.ext
|
|
|
|
from docs.auxil.admonition_inserter import AdmonitionInserter
|
|
|
|
from docs.auxil.kwargs_insertion import (
|
|
|
|
check_timeout_and_api_kwargs_presence,
|
|
|
|
find_insert_pos_for_kwargs,
|
2023-11-27 18:24:21 +01:00
|
|
|
get_updates_read_timeout_addition,
|
2023-02-05 18:09:55 +01:00
|
|
|
keyword_args,
|
2023-11-26 16:44:18 +01:00
|
|
|
media_write_timeout_deprecation,
|
|
|
|
media_write_timeout_deprecation_methods,
|
2023-02-05 18:09:55 +01:00
|
|
|
)
|
|
|
|
from docs.auxil.link_code import LINE_NUMBERS
|
|
|
|
|
|
|
|
ADMONITION_INSERTER = AdmonitionInserter()
|
|
|
|
|
|
|
|
# Some base classes are implementation detail
|
|
|
|
# We want to instead show *their* base class
|
|
|
|
PRIVATE_BASE_CLASSES = {
|
|
|
|
"_ChatUserBaseFilter": "MessageFilter",
|
|
|
|
"_Dice": "MessageFilter",
|
|
|
|
"_BaseThumbedMedium": "TelegramObject",
|
|
|
|
"_BaseMedium": "TelegramObject",
|
|
|
|
"_CredentialsBase": "TelegramObject",
|
2024-05-29 19:59:23 +02:00
|
|
|
"_ChatBase": "TelegramObject",
|
2023-02-05 18:09:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
FILE_ROOT = Path(inspect.getsourcefile(telegram)).parent.parent.resolve()
|
|
|
|
|
|
|
|
|
|
|
|
def autodoc_skip_member(app, what, name, obj, skip, options):
|
|
|
|
"""We use this to not document certain members like filter() or check_update() for filters.
|
|
|
|
See https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#skipping-members"""
|
|
|
|
|
|
|
|
included = {"MessageFilter", "UpdateFilter"} # filter() and check_update() only for these.
|
|
|
|
included_in_obj = any(inc in repr(obj) for inc in included)
|
|
|
|
|
|
|
|
if included_in_obj: # it's difficult to see if check_update is from an inherited-member or not
|
|
|
|
for frame in inspect.stack(): # From https://github.com/sphinx-doc/sphinx/issues/9533
|
|
|
|
if frame.function == "filter_members":
|
|
|
|
docobj = frame.frame.f_locals["self"].object
|
|
|
|
if not any(inc in str(docobj) for inc in included) and name == "check_update":
|
|
|
|
return True
|
|
|
|
break
|
|
|
|
|
2024-02-29 19:11:03 +01:00
|
|
|
if name == "filter" and obj.__module__ == "telegram.ext.filters" and not included_in_obj:
|
|
|
|
return True # return True to exclude from docs.
|
|
|
|
return None
|
2023-02-05 18:09:55 +01:00
|
|
|
|
|
|
|
|
|
|
|
def autodoc_process_docstring(
|
|
|
|
app: Sphinx, what, name: str, obj: object, options, lines: list[str]
|
|
|
|
):
|
|
|
|
"""We do the following things:
|
|
|
|
1) Use this method to automatically insert the Keyword Args and "Shortcuts" admonitions
|
|
|
|
for the Bot methods.
|
|
|
|
|
|
|
|
2) Use this method to automatically insert "Returned in" admonition into classes
|
|
|
|
that are returned from the Bot methods
|
|
|
|
|
|
|
|
3) Use this method to automatically insert "Available in" admonition into classes
|
|
|
|
whose instances are available as attributes of other classes
|
|
|
|
|
|
|
|
4) Use this method to automatically insert "Use in" admonition into classes
|
|
|
|
whose instances can be used as arguments of the Bot methods
|
|
|
|
|
|
|
|
5) Misuse this autodoc hook to get the file names & line numbers because we have access
|
|
|
|
to the actual object here.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# 1) Insert the Keyword Args and "Shortcuts" admonitions for the Bot methods
|
|
|
|
method_name = name.split(".")[-1]
|
|
|
|
if (
|
|
|
|
name.startswith("telegram.Bot.")
|
|
|
|
and what == "method"
|
|
|
|
and method_name.islower()
|
|
|
|
and check_timeout_and_api_kwargs_presence(obj)
|
|
|
|
):
|
|
|
|
insert_index = find_insert_pos_for_kwargs(lines)
|
|
|
|
if not insert_index:
|
|
|
|
raise ValueError(
|
|
|
|
f"Couldn't find the correct position to insert the keyword args for {obj}."
|
|
|
|
)
|
|
|
|
|
2023-11-27 18:24:21 +01:00
|
|
|
get_updates: bool = method_name == "get_updates"
|
2023-02-05 18:09:55 +01:00
|
|
|
# The below can be done in 1 line with itertools.chain, but this must be modified in-place
|
2023-11-26 16:44:18 +01:00
|
|
|
insert_idx = insert_index
|
2023-02-05 18:09:55 +01:00
|
|
|
for i in range(insert_index, insert_index + len(keyword_args)):
|
2023-11-26 16:44:18 +01:00
|
|
|
to_insert = keyword_args[i - insert_index]
|
|
|
|
|
|
|
|
if (
|
|
|
|
"post.write_timeout`. Defaults to" in to_insert
|
|
|
|
and method_name in media_write_timeout_deprecation_methods
|
|
|
|
):
|
|
|
|
effective_insert: list[str] = media_write_timeout_deprecation
|
2023-11-27 18:24:21 +01:00
|
|
|
elif get_updates and to_insert.lstrip().startswith("read_timeout"):
|
2024-02-29 19:11:03 +01:00
|
|
|
effective_insert = [to_insert, *get_updates_read_timeout_addition]
|
2023-11-26 16:44:18 +01:00
|
|
|
else:
|
|
|
|
effective_insert = [to_insert]
|
|
|
|
|
|
|
|
lines[insert_idx:insert_idx] = effective_insert
|
|
|
|
insert_idx += len(effective_insert)
|
2023-02-05 18:09:55 +01:00
|
|
|
|
|
|
|
ADMONITION_INSERTER.insert_admonitions(
|
|
|
|
obj=typing.cast(collections.abc.Callable, obj),
|
|
|
|
docstring_lines=lines,
|
|
|
|
)
|
|
|
|
|
|
|
|
# 2-4) Insert "Returned in", "Available in", "Use in" admonitions into classes
|
|
|
|
# (where applicable)
|
|
|
|
if what == "class":
|
|
|
|
ADMONITION_INSERTER.insert_admonitions(
|
|
|
|
obj=typing.cast(type, obj), # since "what" == class, we know it's not just object
|
|
|
|
docstring_lines=lines,
|
|
|
|
)
|
|
|
|
|
|
|
|
# 5) Get the file names & line numbers
|
|
|
|
# We can't properly handle ordinary attributes.
|
|
|
|
# In linkcode_resolve we'll resolve to the `__init__` or module instead
|
|
|
|
if what == "attribute":
|
|
|
|
return
|
|
|
|
|
|
|
|
# Special casing for properties
|
|
|
|
if hasattr(obj, "fget"):
|
|
|
|
obj = obj.fget
|
|
|
|
|
|
|
|
# Special casing for filters
|
|
|
|
if isinstance(obj, telegram.ext.filters.BaseFilter):
|
|
|
|
obj = obj.__class__
|
|
|
|
|
2024-08-07 21:56:46 +02:00
|
|
|
with contextlib.suppress(Exception):
|
2023-02-05 18:09:55 +01:00
|
|
|
source_lines, start_line = inspect.getsourcelines(obj)
|
|
|
|
end_line = start_line + len(source_lines)
|
|
|
|
file = Path(inspect.getsourcefile(obj)).relative_to(FILE_ROOT)
|
|
|
|
LINE_NUMBERS[name] = (file, start_line, end_line)
|
|
|
|
|
|
|
|
# Since we don't document the `__init__`, we call this manually to have it available for
|
|
|
|
# attributes -- see the note above
|
|
|
|
if what == "class":
|
|
|
|
autodoc_process_docstring(app, "method", f"{name}.__init__", obj.__init__, options, lines)
|
|
|
|
|
|
|
|
|
2024-02-29 19:11:03 +01:00
|
|
|
def autodoc_process_bases(app, name, obj, option, bases: list) -> None:
|
2023-02-05 18:09:55 +01:00
|
|
|
"""Here we fine tune how the base class's classes are displayed."""
|
2024-02-29 19:11:03 +01:00
|
|
|
for idx, raw_base in enumerate(bases):
|
2023-02-05 18:09:55 +01:00
|
|
|
# let's use a string representation of the object
|
2024-02-29 19:11:03 +01:00
|
|
|
base = str(raw_base)
|
2023-02-05 18:09:55 +01:00
|
|
|
|
|
|
|
# Special case for abstract context managers which are wrongly resoled for some reason
|
|
|
|
if base.startswith("typing.AbstractAsyncContextManager"):
|
|
|
|
bases[idx] = ":class:`contextlib.AbstractAsyncContextManager`"
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Special case because base classes are in std lib:
|
|
|
|
if "StringEnum" in base == "<enum 'StringEnum'>":
|
|
|
|
bases[idx] = ":class:`enum.Enum`"
|
|
|
|
bases.insert(0, ":class:`str`")
|
|
|
|
continue
|
|
|
|
|
|
|
|
if "IntEnum" in base:
|
|
|
|
bases[idx] = ":class:`enum.IntEnum`"
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Drop generics (at least for now)
|
|
|
|
if base.endswith("]"):
|
|
|
|
base = base.split("[", maxsplit=1)[0]
|
|
|
|
bases[idx] = f":class:`{base}`"
|
|
|
|
|
|
|
|
# Now convert `telegram._message.Message` to `telegram.Message` etc
|
2023-06-29 18:17:47 +02:00
|
|
|
if (
|
|
|
|
not (match := re.search(pattern=r"(telegram(\.ext|))\.[_\w\.]+", string=base))
|
|
|
|
or "_utils" in base
|
|
|
|
):
|
2023-02-05 18:09:55 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
parts = match.group(0).split(".")
|
|
|
|
|
|
|
|
# Remove private paths
|
|
|
|
for index, part in enumerate(parts):
|
|
|
|
if part.startswith("_"):
|
|
|
|
parts = parts[:index] + parts[-1:]
|
|
|
|
break
|
|
|
|
|
|
|
|
# Replace private base classes with their respective parent
|
|
|
|
parts = [PRIVATE_BASE_CLASSES.get(part, part) for part in parts]
|
|
|
|
|
|
|
|
base = ".".join(parts)
|
|
|
|
|
|
|
|
bases[idx] = f":class:`{base}`"
|