Allow Input of Type Sticker for Several Methods (#4616)

This commit is contained in:
Bibo-Joshi 2024-12-29 20:16:46 +01:00 committed by GitHub
parent df20e49db1
commit a6cd9c5292
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 190 additions and 55 deletions

View file

@ -181,8 +181,8 @@ markers = [
"req", "req",
] ]
asyncio_mode = "auto" asyncio_mode = "auto"
log_format = "%(funcName)s - Line %(lineno)d - %(message)s" log_cli_format = "%(funcName)s - Line %(lineno)d - %(message)s"
# log_level = "DEBUG" # uncomment to see DEBUG logs # log_cli_level = "DEBUG" # uncomment to see DEBUG logs
# MYPY: # MYPY:
[tool.mypy] [tool.mypy]

View file

@ -6622,7 +6622,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def set_sticker_position_in_set( async def set_sticker_position_in_set(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
position: int, position: int,
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -6634,7 +6634,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"""Use this method to move a sticker in a set created by the bot to a specific position. """Use this method to move a sticker in a set created by the bot to a specific position.
Args: Args:
sticker (:obj:`str`): File identifier of the sticker. sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
position (:obj:`int`): New sticker position in the set, zero-based. position (:obj:`int`): New sticker position in the set, zero-based.
Returns: Returns:
@ -6644,7 +6648,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:class:`telegram.error.TelegramError` :class:`telegram.error.TelegramError`
""" """
data: JSONDict = {"sticker": sticker, "position": position} data: JSONDict = {
"sticker": sticker if isinstance(sticker, str) else sticker.file_id,
"position": position,
}
return await self._post( return await self._post(
"setStickerPositionInSet", "setStickerPositionInSet",
data, data,
@ -6749,7 +6756,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def delete_sticker_from_set( async def delete_sticker_from_set(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -6760,7 +6767,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"""Use this method to delete a sticker from a set created by the bot. """Use this method to delete a sticker from a set created by the bot.
Args: Args:
sticker (:obj:`str`): File identifier of the sticker. sticker (:obj:`str` | :class:`telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
Returns: Returns:
:obj:`bool`: On success, :obj:`True` is returned. :obj:`bool`: On success, :obj:`True` is returned.
@ -6769,7 +6780,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:class:`telegram.error.TelegramError` :class:`telegram.error.TelegramError`
""" """
data: JSONDict = {"sticker": sticker} data: JSONDict = {"sticker": sticker if isinstance(sticker, str) else sticker.file_id}
return await self._post( return await self._post(
"deleteStickerFromSet", "deleteStickerFromSet",
data, data,
@ -6937,7 +6948,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def set_sticker_emoji_list( async def set_sticker_emoji_list(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
emoji_list: Sequence[str], emoji_list: Sequence[str],
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -6953,7 +6964,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 20.2 .. versionadded:: 20.2
Args: Args:
sticker (:obj:`str`): File identifier of the sticker. sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
emoji_list (Sequence[:obj:`str`]): A sequence of emoji_list (Sequence[:obj:`str`]): A sequence of
:tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI`- :tg-const:`telegram.constants.StickerLimit.MIN_STICKER_EMOJI`-
:tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with :tg-const:`telegram.constants.StickerLimit.MAX_STICKER_EMOJI` emoji associated with
@ -6965,7 +6980,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Raises: Raises:
:class:`telegram.error.TelegramError` :class:`telegram.error.TelegramError`
""" """
data: JSONDict = {"sticker": sticker, "emoji_list": emoji_list} data: JSONDict = {
"sticker": sticker if isinstance(sticker, str) else sticker.file_id,
"emoji_list": emoji_list,
}
return await self._post( return await self._post(
"setStickerEmojiList", "setStickerEmojiList",
data, data,
@ -6978,7 +6996,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def set_sticker_keywords( async def set_sticker_keywords(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
keywords: Optional[Sequence[str]] = None, keywords: Optional[Sequence[str]] = None,
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -6994,7 +7012,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 20.2 .. versionadded:: 20.2
Args: Args:
sticker (:obj:`str`): File identifier of the sticker. sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
keywords (Sequence[:obj:`str`]): A sequence of keywords (Sequence[:obj:`str`]): A sequence of
0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords 0-:tg-const:`telegram.constants.StickerLimit.MAX_SEARCH_KEYWORDS` search keywords
for the sticker with total length up to for the sticker with total length up to
@ -7006,7 +7028,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Raises: Raises:
:class:`telegram.error.TelegramError` :class:`telegram.error.TelegramError`
""" """
data: JSONDict = {"sticker": sticker, "keywords": keywords} data: JSONDict = {
"sticker": sticker if isinstance(sticker, str) else sticker.file_id,
"keywords": keywords,
}
return await self._post( return await self._post(
"setStickerKeywords", "setStickerKeywords",
data, data,
@ -7019,7 +7044,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def set_sticker_mask_position( async def set_sticker_mask_position(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
mask_position: Optional[MaskPosition] = None, mask_position: Optional[MaskPosition] = None,
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -7035,7 +7060,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 20.2 .. versionadded:: 20.2
Args: Args:
sticker (:obj:`str`): File identifier of the sticker. sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the sticker or
the sticker object.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
mask_position (:class:`telegram.MaskPosition`, optional): A object with the position mask_position (:class:`telegram.MaskPosition`, optional): A object with the position
where the mask should be placed on faces. Omit the parameter to remove the mask where the mask should be placed on faces. Omit the parameter to remove the mask
position. position.
@ -7046,7 +7075,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Raises: Raises:
:class:`telegram.error.TelegramError` :class:`telegram.error.TelegramError`
""" """
data: JSONDict = {"sticker": sticker, "mask_position": mask_position} data: JSONDict = {
"sticker": sticker if isinstance(sticker, str) else sticker.file_id,
"mask_position": mask_position,
}
return await self._post( return await self._post(
"setStickerMaskPosition", "setStickerMaskPosition",
data, data,
@ -9248,7 +9280,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
self, self,
user_id: int, user_id: int,
name: str, name: str,
old_sticker: str, old_sticker: Union[str, "Sticker"],
sticker: "InputSticker", sticker: "InputSticker",
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -9266,7 +9298,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Args: Args:
user_id (:obj:`int`): User identifier of the sticker set owner. user_id (:obj:`int`): User identifier of the sticker set owner.
name (:obj:`str`): Sticker set name. name (:obj:`str`): Sticker set name.
old_sticker (:obj:`str`): File identifier of the replaced sticker. old_sticker (:obj:`str` | :class:`~telegram.Sticker`): File identifier of the replaced
sticker or the sticker object itself.
.. versionchanged:: NEXT.VERSION
Accepts also :class:`telegram.Sticker` instances.
sticker (:class:`telegram.InputSticker`): An object with information about the added sticker (:class:`telegram.InputSticker`): An object with information about the added
sticker. If exactly the same sticker had already been added to the set, then the sticker. If exactly the same sticker had already been added to the set, then the
set remains unchanged. set remains unchanged.
@ -9280,7 +9316,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
data: JSONDict = { data: JSONDict = {
"user_id": user_id, "user_id": user_id,
"name": name, "name": name,
"old_sticker": old_sticker, "old_sticker": old_sticker if isinstance(old_sticker, str) else old_sticker.file_id,
"sticker": sticker, "sticker": sticker,
} }

View file

@ -1426,7 +1426,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def delete_sticker_from_set( async def delete_sticker_from_set(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -3660,7 +3660,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def set_sticker_position_in_set( async def set_sticker_position_in_set(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
position: int, position: int,
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -4114,7 +4114,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def set_sticker_emoji_list( async def set_sticker_emoji_list(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
emoji_list: Sequence[str], emoji_list: Sequence[str],
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -4136,7 +4136,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def set_sticker_keywords( async def set_sticker_keywords(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
keywords: Optional[Sequence[str]] = None, keywords: Optional[Sequence[str]] = None,
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -4158,7 +4158,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def set_sticker_mask_position( async def set_sticker_mask_position(
self, self,
sticker: str, sticker: Union[str, "Sticker"],
mask_position: Optional[MaskPosition] = None, mask_position: Optional[MaskPosition] = None,
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
@ -4250,7 +4250,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self, self,
user_id: int, user_id: int,
name: str, name: str,
old_sticker: str, old_sticker: Union[str, "Sticker"],
sticker: "InputSticker", sticker: "InputSticker",
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,

View file

@ -699,6 +699,54 @@ class TestStickerSetWithoutRequest(StickerSetTestBase):
monkeypatch.setattr(sticker.get_bot(), "get_file", make_assertion) monkeypatch.setattr(sticker.get_bot(), "get_file", make_assertion)
assert await sticker.get_file() assert await sticker.get_file()
async def test_delete_sticker_from_set_sticker_input(self, offline_bot, sticker, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.delete_sticker_from_set(sticker)
async def test_replace_sticker_in_set_sticker_input(self, offline_bot, sticker, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["old_sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.replace_sticker_in_set(
user_id=1, name="name", sticker="sticker", old_sticker=sticker
)
async def test_set_sticker_emoji_list_sticker_input(self, offline_bot, sticker, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.set_sticker_emoji_list(sticker, ["emoji"])
async def test_set_sticker_mask_position_sticker_input(
self, offline_bot, sticker, monkeypatch
):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.set_sticker_mask_position(sticker, MaskPosition("eyes", 1, 2, 3))
async def test_set_sticker_position_in_set_sticker_input(
self, offline_bot, sticker, monkeypatch
):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.set_sticker_position_in_set(sticker, 1)
async def test_set_sticker_keywords_sticker_input(self, offline_bot, sticker, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["sticker"] == sticker.file_id
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.set_sticker_keywords(sticker, ["keyword"])
@pytest.mark.xdist_group("stickerset") @pytest.mark.xdist_group("stickerset")
class TestStickerSetWithRequest: class TestStickerSetWithRequest:

View file

@ -37,6 +37,7 @@ from telegram import (
InputTextMessageContent, InputTextMessageContent,
LinkPreviewOptions, LinkPreviewOptions,
ReplyParameters, ReplyParameters,
Sticker,
TelegramObject, TelegramObject,
) )
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
@ -317,6 +318,16 @@ def build_kwargs(
kws["error_message"] = "error" kws["error_message"] = "error"
elif name == "options": elif name == "options":
kws[name] = ["option1", "option2"] kws[name] = ["option1", "option2"]
elif name in ("sticker", "old_sticker"):
kws[name] = Sticker(
file_id="file_id",
file_unique_id="file_unique_id",
width=1,
height=1,
is_animated=False,
is_video=False,
type="regular",
)
else: else:
kws[name] = True kws[name] = True

View file

@ -38,6 +38,7 @@ from tests.test_official.helpers import (
_get_params_base, _get_params_base,
_unionizer, _unionizer,
cached_type_hints, cached_type_hints,
extract_mappings,
resolve_forward_refs_in_type, resolve_forward_refs_in_type,
wrap_with_none, wrap_with_none,
) )
@ -144,7 +145,7 @@ def check_param_type(
) )
# CHECKING: # CHECKING:
# Each branch manipulates the `mapped_type` (except for 4) ) to match the `ptb_annotation`. # Each branch manipulates the `mapped_type` (except for 5) ) to match the `ptb_annotation`.
# 1) HANDLING ARRAY TYPES: # 1) HANDLING ARRAY TYPES:
# Now let's do the checking, starting with "Array of ..." types. # Now let's do the checking, starting with "Array of ..." types.
@ -174,9 +175,11 @@ def check_param_type(
# 2) HANDLING OTHER TYPES: # 2) HANDLING OTHER TYPES:
# Special case for send_* methods where we accept more types than the official API: # Special case for send_* methods where we accept more types than the official API:
elif ptb_param.name in PTCE.ADDITIONAL_TYPES and obj.__name__.startswith("send"): elif additional_types := extract_mappings(PTCE.ADDITIONAL_TYPES, obj, ptb_param.name):
log("Checking that `%s` has an additional argument!\n", ptb_param.name) log("Checking that `%s` accepts additional types for some parameters!\n", obj.__name__)
mapped_type = mapped_type | PTCE.ADDITIONAL_TYPES[ptb_param.name] for at in additional_types:
log("Checking that `%s` is an additional type for `%s`!\n", at, ptb_param.name)
mapped_type = mapped_type | at
# 3) HANDLING DATETIMES: # 3) HANDLING DATETIMES:
elif ( elif (
@ -205,11 +208,10 @@ def check_param_type(
# 5) COMPLEX TYPES: # 5) COMPLEX TYPES:
# Some types are too complicated, so we replace our annotation with a simpler type: # Some types are too complicated, so we replace our annotation with a simpler type:
elif any(ptb_param.name in key for key in PTCE.COMPLEX_TYPES): elif overrides := extract_mappings(PTCE.COMPLEX_TYPES, obj, ptb_param.name):
exception_type = overrides[0]
log("Converting `%s` to a simpler type!\n", ptb_param.name) log("Converting `%s` to a simpler type!\n", ptb_param.name)
for (param_name, is_expected_class), exception_type in PTCE.COMPLEX_TYPES.items(): ptb_annotation = wrap_with_none(tg_parameter, exception_type, obj)
if ptb_param.name == param_name and is_class is is_expected_class:
ptb_annotation = wrap_with_none(tg_parameter, exception_type, obj)
# 6) HANDLING DEFAULTS PARAMETERS: # 6) HANDLING DEFAULTS PARAMETERS:
# Classes whose parameters are all ODVInput should be converted and checked. # Classes whose parameters are all ODVInput should be converted and checked.

View file

@ -35,16 +35,25 @@ GLOBALLY_IGNORED_PARAMETERS = {
class ParamTypeCheckingExceptions: class ParamTypeCheckingExceptions:
# Types for certain parameters accepted by PTB but not in the official API # Types for certain parameters accepted by PTB but not in the official API
# structure: method/class_name/regex: {param_name/regex: type}
ADDITIONAL_TYPES = { ADDITIONAL_TYPES = {
"photo": PhotoSize, r"send_\w*": {
"video": Video, "photo$": PhotoSize,
"video_note": VideoNote, "video$": Video,
"audio": Audio, "video_note": VideoNote,
"document": Document, "audio": Audio,
"animation": Animation, "document": Document,
"voice": Voice, "animation": Animation,
"sticker": Sticker, "voice": Voice,
"gift_id": Gift, "sticker": Sticker,
"gift_id": Gift,
},
"(delete|set)_sticker.*": {
"sticker$": Sticker,
},
"replace_sticker_in_set": {
"old_sticker$": Sticker,
},
} }
# TODO: Look into merging this with COMPLEX_TYPES # TODO: Look into merging this with COMPLEX_TYPES
@ -61,19 +70,29 @@ class ParamTypeCheckingExceptions:
} }
# Special cases for other parameters that accept more types than the official API, and are # Special cases for other parameters that accept more types than the official API, and are
# too complex to compare/predict with official API: # too complex to compare/predict with official API
# structure: class/method_name: {param_name: reduced form of annotation}
COMPLEX_TYPES = ( COMPLEX_TYPES = (
{ # (param_name, is_class (i.e appears in a class?)): reduced form of annotation { # (param_name, is_class (i.e appears in a class?)): reduced form of annotation
("correct_option_id", False): int, # actual: Literal "send_poll": {"correct_option_id": int}, # actual: Literal
("file_id", False): str, # actual: Union[str, objs_with_file_id_attr] "get_file": {
("invite_link", False): str, # actual: Union[str, ChatInviteLink] "file_id": str, # actual: Union[str, objs_with_file_id_attr]
("provider_data", False): str, # actual: Union[str, obj] },
("callback_data", True): str, # actual: Union[str, obj] r"\w+invite_link": {
("media", True): str, # actual: Union[str, InputMedia*, FileInput] "invite_link": str, # actual: Union[str, ChatInviteLink]
( },
"data", "send_invoice|create_invoice_link": {
True, "provider_data": str, # actual: Union[str, obj]
): str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress] },
"InlineKeyboardButton": {
"callback_data": str, # actual: Union[str, obj]
},
"Input(Paid)?Media.*": {
"media": str, # actual: Union[str, InputMedia*, FileInput]
},
"EncryptedPassportElement": {
"data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress]
},
} }
) )

View file

@ -21,7 +21,7 @@
import functools import functools
import re import re
from collections.abc import Sequence from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, _eval_type, get_type_hints from typing import TYPE_CHECKING, Any, Optional, TypeVar, _eval_type, get_type_hints
from bs4 import PageElement, Tag from bs4 import PageElement, Tag
@ -110,3 +110,22 @@ def cached_type_hints(obj: Any, is_class: bool) -> dict[str, Any]:
def resolve_forward_refs_in_type(obj: type) -> type: def resolve_forward_refs_in_type(obj: type) -> type:
"""Resolves forward references in a type hint.""" """Resolves forward references in a type hint."""
return _eval_type(obj, localns=tg_objects, globalns=None) return _eval_type(obj, localns=tg_objects, globalns=None)
T = TypeVar("T")
def extract_mappings(
exceptions: dict[str, dict[str, T]], obj: object, param_name: str
) -> Optional[list[T]]:
mappings = (
mapping for pattern, mapping in exceptions.items() if (re.match(pattern, obj.__name__))
)
out = [
value
for mapping in mappings
for key, value in mapping.items()
if re.match(key, param_name)
]
return None or out