Documentation Improvements (#3464, #3483, #3484, #3497, #3512, #3501, #3515, #3523, #3498, #3529)

Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
Co-authored-by: Shivam Saini <51438830+shivamsn97@users.noreply.github.com>
Co-authored-by: Aditya Yadav <adityayadav11082@gmail.com>
Co-authored-by: Dmitry Kolomatskiy <58207913+lemontree210@users.noreply.github.com>
Co-authored-by: Crsi <47722349+CrsiX@users.noreply.github.com>
Co-authored-by: poolitzer <github@poolitzer.eu>
Co-authored-by: Aditya <clot27@apx_managed.vanilla>
This commit is contained in:
Bibo-Joshi 2023-02-05 18:09:55 +01:00 committed by GitHub
parent bacdeb37fd
commit 9953216980
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
232 changed files with 1905 additions and 1010 deletions

View file

@ -185,7 +185,7 @@ doc strings don't have a separate documentation site they generate, instead, the
User facing documentation
-------------------------
We use `sphinx`_ to generate static HTML docs. To build them, first make sure you have the required dependencies:
We use `sphinx`_ to generate static HTML docs. To build them, first make sure you're running Python 3.9 or above and have the required dependencies:
.. code-block:: bash

View file

@ -30,6 +30,8 @@ jobs:
run: |
python -W ignore -m pip install --upgrade pip
python -W ignore -m pip install -r requirements-all.txt
- name: Test autogeneration of admonitions
run: pytest -v --tb=short tests/docs/admonition_inserter.py
- name: Build docs
run: sphinx-build docs/source docs/build/html -W --keep-going -j auto
- name: Upload docs

View file

@ -68,7 +68,7 @@ repos:
rev: v3.3.1
hooks:
- id: pyupgrade
files: ^(telegram|examples|tests)/.*\.py$
files: ^(telegram|examples|tests|docs)/.*\.py$
args:
- --py37-plus
- repo: https://github.com/pycqa/isort

View file

@ -102,6 +102,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Sahil Sharma <https://github.com/sahilsharma811>`_
- `Sascha <https://github.com/saschalalala>`_
- `Shelomentsev D <https://github.com/shelomentsevd>`_
- `Shivam Saini <https://github.com/shivamsn97>`_
- `Simon Schürrle <https://github.com/SitiSchu>`_
- `sooyhwang <https://github.com/sooyhwang>`_
- `syntx <https://github.com/syntx>`_

View file

@ -56,6 +56,8 @@ html:
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
rebuild: clean html
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo

View file

@ -0,0 +1,599 @@
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2023
# 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
import inspect
import re
import typing
from collections import defaultdict
from typing import Any, Iterator, Union
import telegram
import telegram.ext
def _iter_own_public_methods(cls: type) -> Iterator[tuple[str, type]]:
"""Iterates over methods of a class that are not protected/private,
not camelCase and not inherited from the parent class.
Returns pairs of method names and methods.
This function is defined outside the class because it is used to create class constants.
"""
return (
m
for m in inspect.getmembers(cls, predicate=inspect.isfunction) # not .ismethod
if not m[0].startswith("_")
and m[0].islower() # to avoid camelCase methods
and m[0] in cls.__dict__ # method is not inherited from parent class
)
class AdmonitionInserter:
"""Class for inserting admonitions into docs of Telegram classes."""
CLASS_ADMONITION_TYPES = ("use_in", "available_in", "returned_in")
METHOD_ADMONITION_TYPES = ("shortcuts",)
ALL_ADMONITION_TYPES = CLASS_ADMONITION_TYPES + METHOD_ADMONITION_TYPES
FORWARD_REF_PATTERN = re.compile(r"^ForwardRef\('(?P<class_name>\w+)'\)$")
""" A pattern to find a class name in a ForwardRef typing annotation.
Class name (in a named group) is surrounded by parentheses and single quotes.
Note that since we're analyzing argument by argument, the pattern can be strict, with
start and end markers.
"""
FORWARD_REF_SKIP_PATTERN = re.compile(r"^ForwardRef\('DefaultValue\[\w+]'\)$")
"""A pattern that will be used to skip known ForwardRef's that need not be resolved
to a Telegram class, e.g.:
ForwardRef('DefaultValue[None]')
ForwardRef('DefaultValue[DVValueType]')
"""
METHOD_NAMES_FOR_BOT_AND_APPBUILDER: 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)
}
"""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.
Methods must be public, not aliases, not inherited from TelegramObject.
"""
def __init__(self):
self.admonitions: dict[str, dict[Union[type, collections.abc.Callable], str]] = {
# 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
}
"""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.:
```
{
"use_in": {<class 'telegram._chatinvitelink.ChatInviteLink'>:
<"Use in" admonition for ChatInviteLink>, ...},
"available_in": {<class 'telegram._chatinvitelink.ChatInviteLink'>:
<"Available in" admonition">, ...},
"returned_in": {...}
}
```
"""
def insert_admonitions(
self,
obj: Union[type, collections.abc.Callable],
docstring_lines: list[str],
):
"""Inserts admonitions into docstring lines for a given class or method.
**Modifies lines in place**.
"""
# A better way would be to copy the lines and return them, but that will not work with
# docs.auxil.sphinx_hooks.autodoc_process_docstring()
for admonition_type in self.ALL_ADMONITION_TYPES:
# If there is no admonition of the given type for the given class or method,
# continue to the next admonition type, maybe the class/method is listed there.
if obj not in self.admonitions[admonition_type]:
continue
insert_idx = self._find_insert_pos_for_admonition(docstring_lines)
admonition_lines = self.admonitions[admonition_type][obj].splitlines()
for idx in range(insert_idx, insert_idx + len(admonition_lines)):
docstring_lines.insert(idx, admonition_lines[idx - insert_idx])
def _create_available_in(self) -> dict[type, str]:
"""Creates a dictionary with 'Available in' admonitions for classes that are available
in attributes of other classes.
"""
# Generate a mapping of classes to ReST links to attributes in other classes that
# correspond to instances of a given class
# i.e. {telegram._files.sticker.Sticker: {":attr:`telegram.Message.sticker`", ...}}
attrs_for_class = defaultdict(set)
# The following regex is supposed to capture a class name in a line like this:
# media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send.
#
# Note that even if such typing description spans over multiple lines but each line ends
# with a backslash (otherwise Sphinx will throw an error)
# (e.g. EncryptedPassportElement.data), then Sphinx will combine these lines into a single
# line automatically, and it will contain no backslash (only some extra many whitespaces
# from the indentation).
attr_docstr_pattern = re.compile(
r"^\s*(?P<attr_name>[a-z_]+)" # Any number of spaces, named group for attribute
r"\s?\(" # Optional whitespace, opening parenthesis
r".*" # Any number of characters (that could denote a built-in type)
r":class:`.+`" # Marker of a classref, class name in backticks
r".*\):" # Any number of characters, closing parenthesis, colon.
# The ^ colon above along with parenthesis is important because it makes sure that
# the class is mentioned in the attribute description, not in free text.
r".*$", # Any number of characters, end of string (end of line)
re.VERBOSE,
)
# for properties: there is no attr name in docstring. Just check if there's a class name.
prop_docstring_pattern = re.compile(r":class:`.+`.*:")
# pattern for iterating over potentially many class names in docstring for one attribute.
# Tilde is optional (sometimes it is in the docstring, sometimes not).
single_class_name_pattern = re.compile(r":class:`~?(?P<class_name>[\w.]*)`")
classes_to_inspect = inspect.getmembers(telegram, inspect.isclass) + inspect.getmembers(
telegram.ext, inspect.isclass
)
for class_name, inspected_class in classes_to_inspect:
# We need to make "<class 'telegram._files.sticker.StickerSet'>" into
# "telegram.StickerSet" because that's the way the classes are mentioned in
# docstrings.
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
for line in lines_with_attrs:
line_match = attr_docstr_pattern.match(line)
if not line_match:
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")
# 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(
f"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. {str(e)}"
)
# Properties need to be parsed separately because they act like attributes but not
# listed as attributes.
properties = inspect.getmembers(inspected_class, lambda o: isinstance(o, property))
for prop_name, _ in properties:
# Make sure this property is really defined in the class being inspected.
# A property can be inherited from a parent class, then a link to it will not work.
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
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(
f"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. {str(e)}"
)
return self._generate_admonitions(attrs_for_class, admonition_type="available_in")
def _create_returned_in(self) -> dict[type, str]:
"""Creates a dictionary with 'Returned in' admonitions for classes that are returned
in Bot's and ApplicationBuilder's methods.
"""
# Generate a mapping of classes to ReST links to Bot methods which return it,
# i.e. {<class 'telegram._message.Message'>: {:meth:`telegram.Bot.send_message`, ...}}
methods_for_class = defaultdict(set)
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)
try:
self._resolve_arg_and_add_link(
arg=ret_annot,
dict_of_methods_for_class=methods_for_class,
link=method_link,
)
except NotImplementedError as e:
raise NotImplementedError(
f"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}. {str(e)}"
)
return self._generate_admonitions(methods_for_class, admonition_type="returned_in")
def _create_shortcuts(self) -> dict[collections.abc.Callable, str]:
"""Creates a dictionary with 'Shortcuts' admonitions for Bot methods that
have shortcuts in other classes.
"""
# pattern for looking for calls to Bot methods only
bot_method_pattern = re.compile(
r"""\s* # any number of whitespaces
(?<=return\sawait\sself\.get_bot\(\)\.) # lookbehind
\w+ # the method name we are looking for, letters/underscores
(?=\() # lookahead: opening bracket before the args of the method start
""",
re.VERBOSE,
)
# Generate a mapping of methods of classes to links to Bot methods which they are shortcuts
# for, i.e. {<function Bot.send_voice at ...>: {:meth:`telegram.User.send_voice`, ...}
shortcuts_for_bot_method = defaultdict(set)
# inspect methods of all telegram classes for return statements that indicate
# that this given method is a shortcut for a Bot method
for class_name, cls in inspect.getmembers(telegram, predicate=inspect.isclass):
# no need to inspect Bot's own methods, as Bot can't have shortcuts in Bot
if cls is telegram.Bot:
continue
for method_name, method in _iter_own_public_methods(cls):
# .getsourcelines() returns a tuple. Item [1] is an int
for line in inspect.getsourcelines(method)[0]:
if not (bot_method_match := bot_method_pattern.search(line)):
continue
bot_method = getattr(telegram.Bot, bot_method_match.group())
link_to_shortcut_method = self._generate_link_to_method(method_name, cls)
shortcuts_for_bot_method[bot_method].add(link_to_shortcut_method)
return self._generate_admonitions(shortcuts_for_bot_method, admonition_type="shortcuts")
def _create_use_in(self) -> dict[type, str]:
"""Creates a dictionary with 'Use in' admonitions for classes whose instances are
accepted as arguments for Bot's and ApplicationBuilder's methods.
"""
# Generate a mapping of classes to links to Bot methods which accept them as arguments,
# i.e. {<class 'telegram._inline.inlinequeryresult.InlineQueryResult'>:
# {:meth:`telegram.Bot.answer_inline_query`, ...}}
methods_for_class = defaultdict(set)
for cls, method_names in self.METHOD_NAMES_FOR_BOT_AND_APPBUILDER.items():
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(
f"Error generating Sphinx 'Use in' admonition "
f"(admonition_inserter.py). {cls}, method {method_name}, parameter "
f"{param}: Couldn't resolve type hint {param.annotation}. {str(e)}"
)
return self._generate_admonitions(methods_for_class, admonition_type="use_in")
@staticmethod
def _find_insert_pos_for_admonition(lines: list[str]) -> int:
"""Finds the correct position to insert the class admonition and returns the index.
The admonition will be insert above "See also", "Examples:", version added/changed notes
and args, whatever comes first.
If no key phrases are found, the admonition will be inserted at the very end.
"""
for idx, value in list(enumerate(lines)):
if (
value.startswith(".. seealso:")
# The docstring contains heading "Examples:", but Sphinx will have it converted
# to ".. admonition: Examples":
or value.startswith(".. admonition:: Examples")
or value.startswith(".. version")
# The space after ":param" is important because docstring can contain ":paramref:"
# in its plain text in the beginning of a line (e.g. ExtBot):
or value.startswith(":param ")
# some classes (like "Credentials") have no params, so insert before attrs:
or value.startswith(".. attribute::")
):
return idx
return len(lines) - 1
def _generate_admonitions(
self,
attrs_or_methods_for_class: dict[type, set[str]],
admonition_type: str,
) -> dict[type, str]:
"""Generates admonitions of a given type.
Takes a dictionary of classes matched to ReST links to methods or attributes, e.g.:
```
{<class 'telegram._files.sticker.StickerSet'>:
[":meth: `telegram.Bot.get_sticker_set`", ...]}.
```
Returns a dictionary of classes matched to full admonitions, e.g.
for `admonition_type` "returned_in" (note that title and CSS class are generated
automatically):
```
{<class 'telegram._files.sticker.StickerSet'>:
".. admonition:: Returned in:
:class: returned-in
:meth: `telegram.Bot.get_sticker_set`"}.
```
"""
if admonition_type not in self.ALL_ADMONITION_TYPES:
raise TypeError(f"Admonition type {admonition_type} not supported.")
admonition_for_class = {}
for cls, attrs in attrs_or_methods_for_class.items():
if cls is telegram.ext.ApplicationBuilder:
# ApplicationBuilder is only used in and returned from its own methods,
# so its page needs no admonitions.
continue
attrs = sorted(attrs)
# e.g. for admonition type "use_in" the title will be "Use in" and CSS class "use-in".
admonition = f"""
.. admonition:: {admonition_type.title().replace("_", " ")}
:class: {admonition_type.replace("_", "-")}
"""
if len(attrs) > 1:
for target_attr in attrs:
admonition += "\n * " + target_attr
else:
admonition += f"\n {attrs[0]}"
admonition += "\n " # otherwise an unexpected unindent warning will be issued
admonition_for_class[cls] = admonition
return admonition_for_class
@staticmethod
def _generate_class_name_for_link(cls: type) -> str:
"""Generates class name that can be used in a ReST link."""
# Check for potential presence of ".ext.", we will need to keep it.
ext = ".ext" if ".ext." in str(cls) else ""
return f"telegram{ext}.{cls.__name__}"
def _generate_link_to_method(self, method_name: str, cls: type) -> str:
"""Generates a ReST link to a method of a telegram class."""
return f":meth:`{self._generate_class_name_for_link(cls)}.{method_name}`"
@staticmethod
def _iter_subclasses(cls: type) -> Iterator:
return (
# exclude private classes
c
for c in cls.__subclasses__()
if not str(c).split(".")[-1].startswith("_")
)
def _resolve_arg_and_add_link(
self,
arg: Any,
dict_of_methods_for_class: defaultdict,
link: str,
) -> 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'
sets of links in the dictionary of admonitions.
**Modifies dictionary in place.**
"""
for cls in self._resolve_arg(arg):
# 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().
if cls is None or "telegram" not in str(cls):
continue
dict_of_methods_for_class[cls].add(link)
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]]:
"""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`.
"""
origin = typing.get_origin(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
# 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:
# 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}")
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'>"):
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]:
"""The keys in the admonitions dictionary are not strings like "telegram.StickerSet"
but classes like <class 'telegram._files.sticker.StickerSet'>.
This method attempts to resolve a PTB class from a name that does or does not
contain the word 'telegram', e.g.
<class 'telegram._files.sticker.StickerSet'> from "telegram.StickerSet" or "StickerSet".
Returns a class on success, :obj:`None` if nothing could be resolved.
"""
for option in (
name,
f"telegram.{name}",
f"telegram.ext.{name}",
f"telegram.ext.filters.{name}",
):
try:
return eval(option)
# NameError will be raised if trying to eval just name and it doesn't work, e.g.
# "Name 'ApplicationBuilder' is not defined".
# AttributeError will be raised if trying to e.g. eval f"telegram.{name}" when the
# class denoted by `name` actually belongs to `telegram.ext`:
# "module 'telegram' has no attribute 'ApplicationBuilder'".
# If neither option works, this is not a PTB class.
except (NameError, AttributeError):
continue
if __name__ == "__main__":
# just try instantiating for debugging purposes
AdmonitionInserter()

View file

@ -0,0 +1,79 @@
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2023
# 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 inspect
keyword_args = [
":keyword _sphinx_paramlinks_telegram.Bot.{method}.read_timeout: Value to pass to "
":paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to {read_timeout}.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.read_timeout: {read_timeout_type}, optional",
":keyword _sphinx_paramlinks_telegram.Bot.{method}.write_timeout: Value to pass to "
":paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to {write_timeout}.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.write_timeout: :obj:`float` | :obj:`None`, "
"optional",
":keyword _sphinx_paramlinks_telegram.Bot.{method}.connect_timeout: Value to pass to "
":paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to "
":attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.connect_timeout: :obj:`float` | "
":obj:`None`, optional",
":keyword _sphinx_paramlinks_telegram.Bot.{method}.pool_timeout: Value to pass to "
":paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to "
":attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.pool_timeout: :obj:`float` | :obj:`None`, "
"optional",
":keyword _sphinx_paramlinks_telegram.Bot.{method}.api_kwargs: Arbitrary keyword arguments "
"to be passed to the Telegram API.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.api_kwargs: :obj:`dict`, optional",
"",
]
write_timeout_sub = [":attr:`~telegram.request.BaseRequest.DEFAULT_NONE`", "``20``"]
read_timeout_sub = [
":attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.",
"``2``. :paramref:`timeout` will be added to this value",
]
read_timeout_type = [":obj:`float` | :obj:`None`", ":obj:`float`"]
def find_insert_pos_for_kwargs(lines: list[str]) -> int:
"""Finds the correct position to insert the keyword arguments and returns the index."""
for idx, value in reversed(list(enumerate(lines))): # reversed since :returns: is at the end
if value.startswith(":returns:"):
return idx
else:
return False
def is_write_timeout_20(obj: object) -> int:
"""inspects the default value of write_timeout parameter of the bot method."""
sig = inspect.signature(obj)
return 1 if (sig.parameters["write_timeout"].default == 20) else 0
def check_timeout_and_api_kwargs_presence(obj: object) -> int:
"""Checks if the method has timeout and api_kwargs keyword only parameters."""
sig = inspect.signature(obj)
params_to_check = (
"read_timeout",
"write_timeout",
"connect_timeout",
"pool_timeout",
"api_kwargs",
)
return all(
param in sig.parameters and sig.parameters[param].kind == inspect.Parameter.KEYWORD_ONLY
for param in params_to_check
)

77
docs/auxil/link_code.py Normal file
View file

@ -0,0 +1,77 @@
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2023
# 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/].
"""Functionality in this file is used for getting the [source] links on the classes, methods etc
to link to the correct files & lines on github. Can be simplified once
https://github.com/sphinx-doc/sphinx/issues/1556 is closed
"""
import subprocess
from sphinx.util import logging
# get the sphinx(!) logger
# Makes sure logs render in red and also plays nicely with e.g. the `nitpicky` option.
sphinx_logger = logging.getLogger(__name__)
# must be a module-level variable so that it can be written to by the `autodoc-process-docstring`
# event handler in `sphinx_hooks.py`
LINE_NUMBERS = {}
def _git_branch() -> str:
"""Get's the current git sha if available or fall back to `master`"""
try:
output = subprocess.check_output( # skipcq: BAN-B607
["git", "describe", "--tags", "--always"], stderr=subprocess.STDOUT
)
return output.decode().strip()
except Exception as exc:
sphinx_logger.exception(
"Failed to get a description of the current commit. Falling back to `master`.",
exc_info=exc,
)
return "master"
git_branch = _git_branch()
base_url = "https://github.com/python-telegram-bot/python-telegram-bot/blob/"
def linkcode_resolve(_, info):
"""See www.sphinx-doc.org/en/master/usage/extensions/linkcode.html"""
combined = ".".join((info["module"], info["fullname"]))
# special casing for ExtBot which is due to the special structure of extbot.rst
combined = combined.replace("ExtBot.ExtBot", "ExtBot")
line_info = LINE_NUMBERS.get(combined)
if not line_info:
# Try the __init__
line_info = LINE_NUMBERS.get(f"{combined.rsplit('.', 1)[0]}.__init__")
if not line_info:
# Try the class
line_info = LINE_NUMBERS.get(f"{combined.rsplit('.', 1)[0]}")
if not line_info:
# Try the module
line_info = LINE_NUMBERS.get(info["module"])
if not line_info:
return
file, start_line, end_line = line_info
return f"{base_url}{git_branch}/{file}#L{start_line}-L{end_line}"

209
docs/auxil/sphinx_hooks.py Normal file
View file

@ -0,0 +1,209 @@
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2023
# 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
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,
is_write_timeout_20,
keyword_args,
read_timeout_sub,
read_timeout_type,
write_timeout_sub,
)
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",
}
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
if name == "filter" and obj.__module__ == "telegram.ext.filters":
if not included_in_obj:
return True # return True to exclude from docs.
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}."
)
long_write_timeout = is_write_timeout_20(obj)
get_updates_sub = 1 if (method_name == "get_updates") else 0
# The below can be done in 1 line with itertools.chain, but this must be modified in-place
for i in range(insert_index, insert_index + len(keyword_args)):
lines.insert(
i,
keyword_args[i - insert_index].format(
method=method_name,
write_timeout=write_timeout_sub[long_write_timeout],
read_timeout=read_timeout_sub[get_updates_sub],
read_timeout_type=read_timeout_type[get_updates_sub],
),
)
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__
try:
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)
except Exception:
pass
# 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)
def autodoc_process_bases(app, name, obj, option, bases: list):
"""Here we fine tune how the base class's classes are displayed."""
for idx, base in enumerate(bases):
# let's use a string representation of the object
base = str(base)
# 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
match = re.search(pattern=r"(telegram(\.ext|))\.[_\w\.]+", string=base)
if not match or "_utils" in base:
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}`"

View file

@ -0,0 +1,92 @@
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2023
# 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/].
from enum import Enum
from docutils.nodes import Element
from sphinx.domains.python import PyXRefRole
from sphinx.environment import BuildEnvironment
from sphinx.util import logging
import telegram
# get the sphinx(!) logger
# Makes sure logs render in red and also plays nicely with e.g. the `nitpicky` option.
sphinx_logger = logging.getLogger(__name__)
CONSTANTS_ROLE = "tg-const"
class TGConstXRefRole(PyXRefRole):
"""This is a bit of Sphinx magic. We add a new role type called tg-const that allows us to
reference values from the `telegram.constants.module` while using the actual value as title
of the link.
Example:
:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` renders as `4096` but links to
the constant.
"""
def process_link(
self,
env: BuildEnvironment,
refnode: Element,
has_explicit_title: bool,
title: str,
target: str,
) -> tuple[str, str]:
title, target = super().process_link(env, refnode, has_explicit_title, title, target)
try:
# We use `eval` to get the value of the expression. Maybe there are better ways to
# do this via importlib or so, but it does the job for now
value = eval(target)
# Maybe we need a better check if the target is actually from tg.constants
# for now checking if it's an Enum suffices since those are used nowhere else in PTB
if isinstance(value, Enum):
# Special casing for file size limits
if isinstance(value, telegram.constants.FileSizeLimit):
return f"{int(value.value / 1e6)} MB", target
return repr(value.value), target
# Just for (Bot API) versions number auto add in constants:
if isinstance(value, str) and target in (
"telegram.constants.BOT_API_VERSION",
"telegram.__version__",
):
return value, target
if isinstance(value, tuple) and target in (
"telegram.constants.BOT_API_VERSION_INFO",
"telegram.__version_info__",
):
return repr(value), target
sphinx_logger.warning(
f"%s:%d: WARNING: Did not convert reference %s. :{CONSTANTS_ROLE}: is not supposed"
" to be used with this type of target.",
refnode.source,
refnode.line,
refnode.rawsource,
)
return title, target
except Exception as exc:
sphinx_logger.exception(
"%s:%d: WARNING: Did not convert reference %s due to an exception.",
refnode.source,
refnode.line,
refnode.rawsource,
exc_info=exc,
)
return title, target

View file

@ -1,6 +1,7 @@
sphinx==5.3.0
sphinx==6.1.3
sphinx-pypi-upload
furo==2022.12.7
git+https://github.com/harshil21/furo-sphinx-search@be5cfa221a01f6e259bb2bb1f76d6ede7ffc1f11#egg=furo-sphinx-search
git+https://github.com/harshil21/furo-sphinx-search@01efc7be422d7dc02390aab9be68d6f5ce1a5618#egg=furo-sphinx-search
sphinx-paramlinks==0.5.4
sphinxcontrib-mermaid==0.7.1
sphinx-copybutton==0.5.1

View file

@ -0,0 +1,65 @@
:root {
--icon--shortcuts: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-signpost' viewBox='0 0 16 16'%3E%3Cpath d='M7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414a1 1 0 0 0-2 0zM12.532 5l1.666 2-1.666 2H2V5h10.532z'/%3E%3C/svg%3E");
--icon--returned-in: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-arrow-return-right' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z'/%3E%3C/svg%3E");
--icon--available-in: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-geo-fill' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4 4a4 4 0 1 1 4.5 3.969V13.5a.5.5 0 0 1-1 0V7.97A4 4 0 0 1 4 3.999zm2.493 8.574a.5.5 0 0 1-.411.575c-.712.118-1.28.295-1.655.493a1.319 1.319 0 0 0-.37.265.301.301 0 0 0-.057.09V14l.002.008a.147.147 0 0 0 .016.033.617.617 0 0 0 .145.15c.165.13.435.27.813.395.751.25 1.82.414 3.024.414s2.273-.163 3.024-.414c.378-.126.648-.265.813-.395a.619.619 0 0 0 .146-.15.148.148 0 0 0 .015-.033L12 14v-.004a.301.301 0 0 0-.057-.09 1.318 1.318 0 0 0-.37-.264c-.376-.198-.943-.375-1.655-.493a.5.5 0 1 1 .164-.986c.77.127 1.452.328 1.957.594C12.5 13 13 13.4 13 14c0 .426-.26.752-.544.977-.29.228-.68.413-1.116.558-.878.293-2.059.465-3.34.465-1.281 0-2.462-.172-3.34-.465-.436-.145-.826-.33-1.116-.558C3.26 14.752 3 14.426 3 14c0-.599.5-1 .961-1.243.505-.266 1.187-.467 1.957-.594a.5.5 0 0 1 .575.411z'/%3E%3C/svg%3E");
--icon--use-in:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-funnel' viewBox='0 0 16 16'%3E%3Cpath d='M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z'/%3E%3C/svg%3E");
}
.admonition.shortcuts {
border-color: rgb(43, 155, 70);
}
.admonition.shortcuts > .admonition-title {
background-color: rgba(43, 155, 70, 0.1);
border-color: rgb(43, 155, 70);
}
.admonition.shortcuts > .admonition-title::before {
background-color: rgb(43, 155, 70);
-webkit-mask-image: var(--icon--shortcuts);
mask-image: var(--icon--shortcuts);
}
.admonition.returned-in {
border-color: rgb(230, 109, 15);
}
.admonition.returned-in > .admonition-title {
background-color: rgba(177, 108, 51, 0.1);
border-color: rgb(230, 109, 15);
}
.admonition.returned-in > .admonition-title::before {
background-color: rgb(230, 109, 15);
-webkit-mask-image: var(--icon--returned-in);
mask-image: var(--icon--returned-in);
}
.admonition.available-in {
border-color: rgb(183, 4, 215);
}
.admonition.available-in > .admonition-title {
background-color: rgba(165, 99, 177, 0.1);
border-color: rgb(183, 4, 215);
}
.admonition.available-in > .admonition-title::before {
background-color: rgb(183, 4, 215);
-webkit-mask-image: var(--icon--available-in);
mask-image: var(--icon--available-in);
}
.admonition.use-in {
border-color: rgb(203, 147, 1);
}
.admonition.use-in > .admonition-title {
background-color: rgba(176, 144, 60, 0.1);
border-color: rgb(203, 147, 1);
}
.admonition.use-in > .admonition-title::before {
background-color: rgb(203, 147, 1);
-webkit-mask-image: var(--icon--use-in);
mask-image: var(--icon--use-in);
}
.admonition.returned-in > ul:hover, .admonition.available-in > ul:hover, .admonition.use-in > ul:hover, .admonition.shortcuts > ul:hover {
cursor: move;
}
.admonition.returned-in > ul, .admonition.available-in > ul, .admonition.use-in > ul, .admonition.shortcuts > ul {
max-height: 200px;
overflow-y: scroll;
}

View file

@ -0,0 +1,3 @@
.article-container h1 {
overflow-wrap: anywhere;
}

View file

@ -0,0 +1,11 @@
.sidebar-sticky .sidebar-brand {
flex-direction: row;
}
.sidebar-sticky .sidebar-brand .sidebar-logo-container {
align-self: center;
}
.sidebar-sticky .sidebar-brand .sidebar-brand-text {
align-self: center;
}

View file

@ -1,20 +1,12 @@
import inspect
import os
import re
import subprocess
import sys
from enum import Enum
from pathlib import Path
from typing import List, Tuple
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
from docutils.nodes import Element
from sphinx.application import Sphinx
from sphinx.domains.python import PyXRefRole
from sphinx.environment import BuildEnvironment
from sphinx.util import logging
sys.path.insert(0, os.path.abspath("../.."))
@ -34,7 +26,7 @@ version = "20.0" # telegram.__version__[:3]
release = "20.0" # telegram.__version__
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "5.1.1"
needs_sphinx = "6.1.3"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -46,6 +38,7 @@ extensions = [
"sphinx.ext.linkcode",
"sphinx.ext.extlinks",
"sphinx_paramlinks",
"sphinx_copybutton",
"sphinxcontrib.mermaid",
"sphinx_search.extension",
]
@ -220,7 +213,13 @@ html_favicon = "ptb-logo_1024.ico"
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_css_files = ["style_external_link.css", "style_mermaid_diagrams.css"]
html_css_files = [
"style_external_link.css",
"style_mermaid_diagrams.css",
"style_sidebar_brand.css",
"style_general.css",
"style_admonitions.css",
]
html_permalinks_icon = "" # Furo's default permalink icon is `#` which doesn't look great imo.
@ -284,326 +283,16 @@ texinfo_documents = [
# -- script stuff --------------------------------------------------------
# get the sphinx(!) logger
# Makes sure logs render in red and also plays nicely with e.g. the `nitpicky` option.
sphinx_logger = logging.getLogger(__name__)
# Due to Sphinx behaviour, these imports only work when imported here, not at top of module.
CONSTANTS_ROLE = "tg-const"
import telegram # We need this so that the `eval` below works
class TGConstXRefRole(PyXRefRole):
"""This is a bit of Sphinx magic. We add a new role type called tg-const that allows us to
reference values from the `telegram.constants.module` while using the actual value as title
of the link.
Example:
:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` renders as `4096` but links to the
constant.
"""
def process_link(
self,
env: BuildEnvironment,
refnode: Element,
has_explicit_title: bool,
title: str,
target: str,
) -> Tuple[str, str]:
title, target = super().process_link(env, refnode, has_explicit_title, title, target)
try:
# We use `eval` to get the value of the expression. Maybe there are better ways to
# do this via importlib or so, but it does the job for now
value = eval(target)
# Maybe we need a better check if the target is actually from tg.constants
# for now checking if it's an Enum suffices since those are used nowhere else in PTB
if isinstance(value, Enum):
# Special casing for file size limits
if isinstance(value, telegram.constants.FileSizeLimit):
return f"{int(value.value / 1e6)} MB", target
return repr(value.value), target
# Just for (Bot API) versions number auto add in constants:
if isinstance(value, str) and target in (
"telegram.constants.BOT_API_VERSION",
"telegram.__version__",
):
return value, target
if isinstance(value, tuple) and target in (
"telegram.constants.BOT_API_VERSION_INFO",
"telegram.__version_info__",
):
return repr(value), target
sphinx_logger.warning(
f"%s:%d: WARNING: Did not convert reference %s. :{CONSTANTS_ROLE}: is not supposed"
" to be used with this type of target.",
refnode.source,
refnode.line,
refnode.rawsource,
)
return title, target
except Exception as exc:
sphinx_logger.exception(
"%s:%d: WARNING: Did not convert reference %s due to an exception.",
refnode.source,
refnode.line,
refnode.rawsource,
exc_info=exc,
)
return title, target
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
if name == "filter" and obj.__module__ == "telegram.ext.filters":
if not included_in_obj:
return True # return True to exclude from docs.
# ------------------------------------------------------------------------------------------------
# This part is for getting the [source] links on the classes, methods etc link to the correct
# files & lines on github. Can be simplified once https://github.com/sphinx-doc/sphinx/issues/1556
# is closed
line_numbers = {}
file_root = Path(inspect.getsourcefile(telegram)).parent.parent.resolve()
import telegram.ext # Needed for checking if an object is a BaseFilter
keyword_args = [
":keyword _sphinx_paramlinks_telegram.Bot.{method}.read_timeout: Value to pass to :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to {read_timeout}.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.read_timeout: {read_timeout_type}, optional",
":keyword _sphinx_paramlinks_telegram.Bot.{method}.write_timeout: Value to pass to :paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to {write_timeout}.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.write_timeout: :obj:`float` | :obj:`None`, optional",
":keyword _sphinx_paramlinks_telegram.Bot.{method}.connect_timeout: Value to pass to :paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.connect_timeout: :obj:`float` | :obj:`None`, optional",
":keyword _sphinx_paramlinks_telegram.Bot.{method}.pool_timeout: Value to pass to :paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.pool_timeout: :obj:`float` | :obj:`None`, optional",
":keyword _sphinx_paramlinks_telegram.Bot.{method}.api_kwargs: Arbitrary keyword arguments to be passed to the Telegram API.",
":kwtype _sphinx_paramlinks_telegram.Bot.{method}.api_kwargs: :obj:`dict`, optional",
"",
]
write_timeout_sub = [":attr:`~telegram.request.BaseRequest.DEFAULT_NONE`", "``20``"]
read_timeout_sub = [
":attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.",
"``2``. :paramref:`timeout` will be added to this value",
]
read_timeout_type = [":obj:`float` | :obj:`None`", ":obj:`float`"]
def find_insert_pos(lines: List[str]) -> int:
"""Finds the correct position to insert the keyword arguments and returns the index."""
for idx, value in reversed(list(enumerate(lines))): # reversed since :returns: is at the end
if value.startswith(":returns:"):
return idx
else:
return False
def is_write_timeout_20(obj: object) -> int:
"""inspects the default value of write_timeout parameter of the bot method."""
sig = inspect.signature(obj)
return 1 if (sig.parameters["write_timeout"].default == 20) else 0
def check_timeout_and_api_kwargs_presence(obj: object) -> int:
"""Checks if the method has timeout and api_kwargs keyword only parameters."""
sig = inspect.signature(obj)
params_to_check = (
"read_timeout",
"write_timeout",
"connect_timeout",
"pool_timeout",
"api_kwargs",
)
return all(
param in sig.parameters and sig.parameters[param].kind == inspect.Parameter.KEYWORD_ONLY
for param in params_to_check
)
def autodoc_process_docstring(
app: Sphinx, what, name: str, obj: object, options, lines: List[str]
):
"""We do two things:
1) Use this method to automatically insert the Keyword Args for the Bot methods.
2) 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 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(lines)
if not insert_index:
raise ValueError(
f"Couldn't find the correct position to insert the keyword args for {obj}."
)
long_write_timeout = is_write_timeout_20(obj)
get_updates_sub = 1 if (method_name == "get_updates") else 0
# The below can be done in 1 line with itertools.chain, but this must be modified in-place
for i in range(insert_index, insert_index + len(keyword_args)):
lines.insert(
i,
keyword_args[i - insert_index].format(
method=method_name,
write_timeout=write_timeout_sub[long_write_timeout],
read_timeout=read_timeout_sub[get_updates_sub],
read_timeout_type=read_timeout_type[get_updates_sub],
),
)
# 2) 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__
try:
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)
except Exception:
pass
# 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)
def _git_branch() -> str:
"""Get's the current git sha if available or fall back to `master`"""
try:
output = subprocess.check_output( # skipcq: BAN-B607
["git", "describe", "--tags", "--always"], stderr=subprocess.STDOUT
)
return output.decode().strip()
except Exception as exc:
sphinx_logger.exception(
"Failed to get a description of the current commit. Falling back to `master`.",
exc_info=exc,
)
return "master"
git_branch = _git_branch()
base_url = "https://github.com/python-telegram-bot/python-telegram-bot/blob/"
def linkcode_resolve(_, info):
"""See www.sphinx-doc.org/en/master/usage/extensions/linkcode.html"""
combined = ".".join((info["module"], info["fullname"]))
# special casing for ExtBot which is due to the special structure of extbot.rst
combined = combined.replace("ExtBot.ExtBot", "ExtBot")
line_info = line_numbers.get(combined)
if not line_info:
# Try the __init__
line_info = line_numbers.get(f"{combined.rsplit('.', 1)[0]}.__init__")
if not line_info:
# Try the class
line_info = line_numbers.get(f"{combined.rsplit('.', 1)[0]}")
if not line_info:
# Try the module
line_info = line_numbers.get(info["module"])
if not line_info:
return
file, start_line, end_line = line_info
return f"{base_url}{git_branch}/{file}#L{start_line}-L{end_line}"
# End of logic for the [source] links
# ------------------------------------------------------------------------------------------------
# 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",
}
def autodoc_process_bases(app, name, obj, option, bases: list):
"""Here we fine tune how the base class's classes are displayed."""
for idx, base in enumerate(bases):
# let's use a string representation of the object
base = str(base)
# 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
match = re.search(pattern=r"(telegram(\.ext|))\.[_\w\.]+", string=base)
if not match or "_utils" in base:
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}`"
# Not used but must be imported for the linkcode extension to find it
from docs.auxil.link_code import linkcode_resolve
from docs.auxil.sphinx_hooks import (
autodoc_process_bases,
autodoc_process_docstring,
autodoc_skip_member,
)
from docs.auxil.tg_const_role import CONSTANTS_ROLE, TGConstXRefRole
def setup(app: Sphinx):

View file

@ -1,5 +1,5 @@
telegram.Animation
==================
Animation
=========
.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject

View file

@ -1,5 +1,5 @@
telegram.Audio
==============
Audio
=====
.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject

View file

@ -1,5 +1,5 @@
telegram.Bot
============
Bot
===
.. autoclass:: telegram.Bot
:members:

View file

@ -1,5 +1,5 @@
telegram.BotCommand
===================
BotCommand
==========
.. autoclass:: telegram.BotCommand
:members:

View file

@ -1,5 +1,5 @@
telegram.BotCommandScope
========================
BotCommandScope
===============
.. autoclass:: telegram.BotCommandScope
:members:

View file

@ -1,5 +1,5 @@
telegram.BotCommandScopeAllChatAdministrators
=============================================
BotCommandScopeAllChatAdministrators
====================================
.. autoclass:: telegram.BotCommandScopeAllChatAdministrators
:members:

View file

@ -1,5 +1,5 @@
telegram.BotCommandScopeAllGroupChats
=======================================
BotCommandScopeAllGroupChats
============================
.. autoclass:: telegram.BotCommandScopeAllGroupChats
:members:

View file

@ -1,5 +1,5 @@
telegram.BotCommandScopeAllPrivateChats
=======================================
BotCommandScopeAllPrivateChats
==============================
.. autoclass:: telegram.BotCommandScopeAllPrivateChats
:members:

View file

@ -1,5 +1,5 @@
telegram.BotCommandScopeChat
============================
BotCommandScopeChat
===================
.. autoclass:: telegram.BotCommandScopeChat
:members:

View file

@ -1,5 +1,5 @@
telegram.BotCommandScopeChatAdministrators
==========================================
BotCommandScopeChatAdministrators
=================================
.. autoclass:: telegram.BotCommandScopeChatAdministrators
:members:

View file

@ -1,5 +1,5 @@
telegram.BotCommandScopeChatMember
==================================
BotCommandScopeChatMember
=========================
.. autoclass:: telegram.BotCommandScopeChatMember
:members:

View file

@ -1,5 +1,5 @@
telegram.BotCommandScopeDefault
===============================
BotCommandScopeDefault
======================
.. autoclass:: telegram.BotCommandScopeDefault
:members:

View file

@ -1,5 +1,5 @@
telegram.Callbackgame
=====================
Callbackgame
============
.. autoclass:: telegram.CallbackGame
:members:

View file

@ -1,5 +1,5 @@
telegram.CallbackQuery
======================
CallbackQuery
=============
.. autoclass:: telegram.CallbackQuery
:members:

View file

@ -1,5 +1,5 @@
telegram.Chat
=============
Chat
====
.. autoclass:: telegram.Chat
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatAdministratorRights
================================
ChatAdministratorRights
=======================
.. versionadded:: 20.0

View file

@ -1,5 +1,5 @@
telegram.ChatInviteLink
=======================
ChatInviteLink
==============
.. autoclass:: telegram.ChatInviteLink
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatJoinRequest
========================
ChatJoinRequest
===============
.. autoclass:: telegram.ChatJoinRequest
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatLocation
=====================
ChatLocation
============
.. autoclass:: telegram.ChatLocation
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatMember
===================
ChatMember
==========
.. autoclass:: telegram.ChatMember
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatMemberAdministrator
================================
ChatMemberAdministrator
=======================
.. autoclass:: telegram.ChatMemberAdministrator
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatMemberBanned
=========================
ChatMemberBanned
================
.. autoclass:: telegram.ChatMemberBanned
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatMemberLeft
=======================
ChatMemberLeft
==============
.. autoclass:: telegram.ChatMemberLeft
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatMemberMember
=========================
ChatMemberMember
================
.. autoclass:: telegram.ChatMemberMember
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatMemberOwner
========================
ChatMemberOwner
===============
.. autoclass:: telegram.ChatMemberOwner
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatMemberRestricted
=============================
ChatMemberRestricted
====================
.. autoclass:: telegram.ChatMemberRestricted
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatMemberUpdated
==========================
ChatMemberUpdated
=================
.. autoclass:: telegram.ChatMemberUpdated
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatPermissions
========================
ChatPermissions
===============
.. autoclass:: telegram.ChatPermissions
:members:

View file

@ -1,5 +1,5 @@
telegram.ChatPhoto
==================
ChatPhoto
=========
.. autoclass:: telegram.ChatPhoto
:members:

View file

@ -1,5 +1,5 @@
telegram.ChosenInlineResult
===========================
ChosenInlineResult
==================
.. autoclass:: telegram.ChosenInlineResult
:members:

View file

@ -1,5 +1,5 @@
telegram.Contact
================
Contact
=======
.. autoclass:: telegram.Contact
:members:

View file

@ -1,5 +1,5 @@
telegram.Credentials
====================
Credentials
===========
.. autoclass:: telegram.Credentials
:members:

View file

@ -1,5 +1,5 @@
telegram.DataCredentials
========================
DataCredentials
===============
.. autoclass:: telegram.DataCredentials
:members:

View file

@ -1,5 +1,5 @@
telegram.Dice
=============
Dice
====
.. autoclass:: telegram.Dice
:members:

View file

@ -1,5 +1,5 @@
telegram.Document
=================
Document
========
.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject
.. autoclass:: telegram.Document

View file

@ -1,5 +1,5 @@
telegram.EncryptedCredentials
=============================
EncryptedCredentials
====================
.. autoclass:: telegram.EncryptedCredentials
:members:

View file

@ -1,5 +1,5 @@
telegram.EncryptedPassportElement
=================================
EncryptedPassportElement
========================
.. autoclass:: telegram.EncryptedPassportElement
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.AIORateLimiter
============================
AIORateLimiter
==============
.. autoclass:: telegram.ext.AIORateLimiter
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.Application
========================
Application
===========
.. autoclass:: telegram.ext.Application
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.ApplicationBuilder
===============================
ApplicationBuilder
==================
.. autoclass:: telegram.ext.ApplicationBuilder
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.ApplicationHandlerStop
===================================
ApplicationHandlerStop
======================
.. autoclass:: telegram.ext.ApplicationHandlerStop
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.BaseHandler
========================
BaseHandler
===========
.. autoclass:: telegram.ext.BaseHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.BasePersistence
============================
BasePersistence
===============
.. autoclass:: telegram.ext.BasePersistence
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.BaseRateLimiter
============================
BaseRateLimiter
===============
.. autoclass:: telegram.ext.BaseRateLimiter
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.CallbackContext
============================
CallbackContext
===============
.. autoclass:: telegram.ext.CallbackContext
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.CallbackDataCache
==============================
CallbackDataCache
=================
.. autoclass:: telegram.ext.CallbackDataCache
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.CallbackQueryHandler
=================================
CallbackQueryHandler
====================
.. autoclass:: telegram.ext.CallbackQueryHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.ChatJoinRequestHandler
===================================
ChatJoinRequestHandler
======================
.. autoclass:: telegram.ext.ChatJoinRequestHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.ChatMemberHandler
==============================
ChatMemberHandler
=================
.. autoclass:: telegram.ext.ChatMemberHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.ChosenInlineResultHandler
======================================
ChosenInlineResultHandler
=========================
.. autoclass:: telegram.ext.ChosenInlineResultHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.CommandHandler
===========================
CommandHandler
==============
.. autoclass:: telegram.ext.CommandHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.ContextTypes
=========================
ContextTypes
============
.. autoclass:: telegram.ext.ContextTypes
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.ConversationHandler
================================
ConversationHandler
===================
.. autoclass:: telegram.ext.ConversationHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.Defaults
=====================
Defaults
========
.. autoclass:: telegram.ext.Defaults
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.DictPersistence
============================
DictPersistence
===============
.. autoclass:: telegram.ext.DictPersistence
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.ExtBot
===================
ExtBot
======
.. autoclass:: telegram.ext.ExtBot
:show-inheritance:

View file

@ -1,5 +1,5 @@
telegram.ext.filters Module
===========================
filters Module
==============
.. :bysource: since e.g filters.CHAT is much above filters.Chat() in the docs when it shouldn't.
The classes in `filters.py` are sorted alphabetically such that :bysource: still is readable

View file

@ -1,5 +1,5 @@
telegram.ext.InlineQueryHandler
===============================
InlineQueryHandler
==================
.. autoclass:: telegram.ext.InlineQueryHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.InvalidCallbackData
================================
InvalidCallbackData
===================
.. autoclass:: telegram.ext.InvalidCallbackData
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.Job
=====================
Job
===
.. autoclass:: telegram.ext.Job
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.JobQueue
=====================
JobQueue
========
.. autoclass:: telegram.ext.JobQueue
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.MessageHandler
===========================
MessageHandler
==============
.. autoclass:: telegram.ext.MessageHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.PersistenceInput
=============================
PersistenceInput
================
.. autoclass:: telegram.ext.PersistenceInput
:show-inheritance:

View file

@ -1,5 +1,5 @@
telegram.ext.PicklePersistence
==============================
PicklePersistence
=================
.. autoclass:: telegram.ext.PicklePersistence
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.PollAnswerHandler
==============================
PollAnswerHandler
=================
.. autoclass:: telegram.ext.PollAnswerHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.PollHandler
========================
PollHandler
===========
.. autoclass:: telegram.ext.PollHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.PreCheckoutQueryHandler
====================================
PreCheckoutQueryHandler
=======================
.. autoclass:: telegram.ext.PreCheckoutQueryHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.PrefixHandler
===========================
PrefixHandler
=============
.. autoclass:: telegram.ext.PrefixHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.ShippingQueryHandler
=================================
ShippingQueryHandler
====================
.. autoclass:: telegram.ext.ShippingQueryHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.StringCommandHandler
=================================
StringCommandHandler
====================
.. autoclass:: telegram.ext.StringCommandHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.StringRegexHandler
===============================
StringRegexHandler
==================
.. autoclass:: telegram.ext.StringRegexHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.TypeHandler
========================
TypeHandler
===========
.. autoclass:: telegram.ext.TypeHandler
:members:

View file

@ -1,5 +1,5 @@
telegram.ext.Updater
====================
Updater
=======
.. autoclass:: telegram.ext.Updater
:members:

View file

@ -1,5 +1,5 @@
telegram.File
=============
File
====
.. autoclass:: telegram.File
:members:

View file

@ -1,5 +1,5 @@
telegram.FileCredentials
========================
FileCredentials
===============
.. autoclass:: telegram.FileCredentials
:members:

View file

@ -1,5 +1,5 @@
telegram.ForceReply
===================
ForceReply
==========
.. autoclass:: telegram.ForceReply
:members:

View file

@ -1,5 +1,5 @@
telegram.ForumTopic
===================
ForumTopic
==========
.. autoclass:: telegram.ForumTopic
:members:

View file

@ -1,5 +1,5 @@
telegram.ForumTopicClosed
=========================
ForumTopicClosed
================
.. autoclass:: telegram.ForumTopicClosed
:members:

View file

@ -1,5 +1,5 @@
telegram.ForumTopicCreated
==========================
ForumTopicCreated
=================
.. autoclass:: telegram.ForumTopicCreated
:members:

View file

@ -1,5 +1,5 @@
telegram.ForumTopicEdited
=========================
ForumTopicEdited
================
.. autoclass:: telegram.ForumTopicEdited
:members:

View file

@ -1,5 +1,5 @@
telegram.ForumTopicReopened
===========================
ForumTopicReopened
==================
.. autoclass:: telegram.ForumTopicReopened
:members:

View file

@ -1,5 +1,5 @@
telegram.Game
=============
Game
====
.. autoclass:: telegram.Game
:members:

View file

@ -1,5 +1,5 @@
telegram.GameHighScore
======================
GameHighScore
=============
.. autoclass:: telegram.GameHighScore
:members:

View file

@ -1,5 +1,5 @@
telegram.GeneralForumTopicHidden
================================
GeneralForumTopicHidden
=======================
.. autoclass:: telegram.GeneralForumTopicHidden
:members:

View file

@ -1,5 +1,5 @@
telegram.GeneralForumTopicUnhidden
==================================
GeneralForumTopicUnhidden
=========================
.. autoclass:: telegram.GeneralForumTopicUnhidden
:members:

Some files were not shown because too many files have changed in this diff Show more