From f5bdffd4cba0d659f609918e58194dc64c9deb28 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:58:13 +0300 Subject: [PATCH 01/31] Add `typing_extensions` --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 56764b83294..e06d52ee355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,8 @@ classifiers = [ ] dependencies = [ "httpx >=0.27,<0.29", - "httpcore >=1.0.9; python_version >= '3.14'" # httpx doesn't pin this as of 0.28.1 + "httpcore >=1.0.9; python_version >= '3.14'", # httpx doesn't pin this as of 0.28.1 + "typing-extensions~=4.16", ] [project.urls] @@ -169,7 +170,7 @@ line-length = 99 show-fixes = true [tool.ruff.lint] -typing-extensions = false +typing-extensions = true ignore = ["PLR2004", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PERF203", "ASYNC240", "PLR0917"] select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE", "G", "ISC", "PT", "ASYNC", "TCH", "SLOT", "PERF", "PYI", "FLY", "AIR", "RUF022", From d6c35bd4c8e5912628bfcc2d0518ecb67d759e79 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:35:06 +0300 Subject: [PATCH 02/31] Set ruff `typing-extensions=false` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e06d52ee355..2f155035fba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -170,7 +170,7 @@ line-length = 99 show-fixes = true [tool.ruff.lint] -typing-extensions = true +typing-extensions = false ignore = ["PLR2004", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PERF203", "ASYNC240", "PLR0917"] select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE", "G", "ISC", "PT", "ASYNC", "TCH", "SLOT", "PERF", "PYI", "FLY", "AIR", "RUF022", From ae4210ca3f87bddb259cd532f4b4bd6f75b54d47 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:09:26 +0300 Subject: [PATCH 03/31] Add `tg{dataclass,field}` helpers --- src/telegram/_utils/dataclass.py | 80 ++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/telegram/_utils/dataclass.py diff --git a/src/telegram/_utils/dataclass.py b/src/telegram/_utils/dataclass.py new file mode 100644 index 00000000000..6558f57b9bd --- /dev/null +++ b/src/telegram/_utils/dataclass.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# Leandro Toledo de Souza +# +# 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/]. +"""Helpers for Implementing Telegram Objects as dataclasses. + +Warning: + Contents of this module are intended to be used internally by the library and *not* by the + user. Changes to this module are not considered breaking changes and may not be documented in + the changelog. +""" + +from collections.abc import Callable +from dataclasses import MISSING, dataclass, field +from typing import Any, TypeVar + +from typing_extensions import dataclass_transform + +_T = TypeVar("_T") +CONVERTER_KEY = object() + + +def tg_field( + *, + default: Any = MISSING, + default_factory: Any = MISSING, + init: bool = True, + compare: bool = False, + kw_only: Any = MISSING, + converter: Callable[[Any], Any] | None = None, +) -> Any: + field_metadata = None + + if converter is not None: + field_metadata = {} + field_metadata[CONVERTER_KEY] = converter + + return field( # pylint: disable=invalid-field-call + repr=False, + compare=compare, + init=init, + default=default, + default_factory=default_factory, + metadata=field_metadata, + kw_only=kw_only, + ) + + +@dataclass_transform( + eq_default=True, + order_default=False, + kw_only_default=False, + frozen_default=True, + field_specifiers=(tg_field,), +) +def tg_dataclass( + *, + eq: bool = True, +) -> Callable[[type[_T]], type[_T]]: + return dataclass( + frozen=True, + slots=True, + repr=False, + match_args=False, + eq=eq, + ) From e20a0748bb890665f00a9f07238b398846b1586f Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:39:42 +0300 Subject: [PATCH 04/31] Do ground work in `TelegramObject` --- src/telegram/_telegramobject.py | 196 +++++++++----------------------- src/telegram/_utils/de_json.py | 4 +- 2 files changed, 56 insertions(+), 144 deletions(-) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 379e5a1e792..9c88882f670 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -19,16 +19,17 @@ """Base class for Telegram Objects.""" import contextlib +import dataclasses import datetime as dtm import inspect import json -from collections.abc import Iterator, Mapping, Sequence, Sized -from contextlib import contextmanager +from collections.abc import Iterator, Mapping, Sized from copy import deepcopy from itertools import chain from types import MappingProxyType from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast, get_args, get_origin +from telegram._utils.dataclass import CONVERTER_KEY, tg_dataclass, tg_field from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp, to_timestamp from telegram._utils.de_json import ( build_sequence_transformer, @@ -38,16 +39,15 @@ ) from telegram._utils.defaultvalue import DefaultValue from telegram._utils.types import JSONDict -from telegram._utils.warnings import warn if TYPE_CHECKING: from telegram import Bot Tele_co = TypeVar("Tele_co", bound="TelegramObject", covariant=True) -Tele = TypeVar("Tele", bound="TelegramObject") _DATETIME_FIELD = object() # Sentinel that marks datetime fields in the de_json plan. +@tg_dataclass(eq=False) class TelegramObject: """Base class for most Telegram objects. @@ -55,6 +55,10 @@ class TelegramObject: The :mod:`pickle` and :func:`~copy.deepcopy` behavior of objects of this type are defined by :meth:`__getstate__`, :meth:`__setstate__` and :meth:`__deepcopy__`. + .. versionchanged:: NEXT.VERSION + Objects loaded from pickled data are now always immutable, even if they were mutable + when pickled. + Tip: Objects of this type can be serialized via Python's :mod:`pickle` module and pickled objects from one version of PTB are usually loadable in future versions. However, we can @@ -85,8 +89,6 @@ class TelegramObject: """ - __slots__ = ("_bot", "_frozen", "_id_attrs", "api_kwargs") - # Names accepted by this class' __init__. Built alongside the transformation plan. __INIT_PARAMS: ClassVar[set[str]] = set() @@ -113,94 +115,33 @@ class TelegramObject: # The values are class name strings, e.g. "TransactionPartnerChat". __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = None - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - # Setting _frozen to `False` here means that classes without arguments still need to - # implement __init__. However, with `True` would mean increased usage of - # `with self._unfrozen()` in the `__init__` of subclasses and we have fewer empty - # classes than classes with arguments. - self._frozen: bool = False - self._id_attrs: tuple[object, ...] = () - self._bot: Bot | None = None - # We don't do anything with api_kwargs here - see docstring of _apply_api_kwargs - self.api_kwargs: Mapping[str, Any] = MappingProxyType(api_kwargs or {}) - - def __eq__(self, other: object) -> bool: - """Compares this object with :paramref:`other` in terms of equality. - If this object and :paramref:`other` are `not` objects of the same class, - this comparison will fall back to Python's default implementation of :meth:`object.__eq__`. - Otherwise, both objects may be compared in terms of equality, if the corresponding - subclass of :class:`TelegramObject` has defined a set of attributes to compare and - the objects are considered to be equal, if all of these attributes are equal. - If the subclass has not defined a set of attributes to compare, a warning will be issued. - - Tip: - If instances of a class in the :mod:`telegram` module are comparable in terms of - equality, the documentation of the class will state the attributes that will be used - for this comparison. - - Args: - other (:obj:`object`): The object to compare with. - - Returns: - :obj:`bool` - - """ - if isinstance(other, self.__class__): - if not self._id_attrs: - warn( - f"Objects of type {self.__class__.__name__} can not be meaningfully tested for" - " equivalence.", - stacklevel=2, - ) - if not other._id_attrs: - warn( - f"Objects of type {other.__class__.__name__} can not be meaningfully tested" - " for equivalence.", - stacklevel=2, + @staticmethod + def _to_mapping_proxy( + value: JSONDict | None, + ) -> Mapping[str, Any]: + return MappingProxyType(value or {}) + + # see docstring of _apply_api_kwargs + api_kwargs: Mapping[str, Any] = tg_field( + kw_only=True, default=None, converter=_to_mapping_proxy + ) + _bot: "Bot | None" = tg_field(init=False, default=None) + + if TYPE_CHECKING: + # FIXME: not sure what to do about this + # mypy doesn't recognize converters currently + # https://github.com/python/mypy/issues/17547 + def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: ... # pylint: disable=unused-argument + + def __post_init__(self) -> None: + for dataclass_field in dataclasses.fields(self): + converter = dataclass_field.metadata.get(CONVERTER_KEY) + if converter is not None: + object.__setattr__( + self, + dataclass_field.name, + converter(getattr(self, dataclass_field.name)), ) - return self._id_attrs == other._id_attrs - return super().__eq__(other) - - def __hash__(self) -> int: - """Builds a hash value for this object such that the hash of two objects is equal if and - only if the objects are equal in terms of :meth:`__eq__`. - - Returns: - :obj:`int` - """ - if self._id_attrs: - return hash((self.__class__, self._id_attrs)) - return super().__hash__() - - def __setattr__(self, key: str, value: object) -> None: - """Overrides :meth:`object.__setattr__` to prevent the overriding of attributes. - - Raises: - :exc:`AttributeError` - """ - # protected attributes can always be set for convenient internal use - if key[0] == "_" or not self._frozen: - super().__setattr__(key, value) - return - - raise AttributeError( - f"Attribute `{key}` of class `{self.__class__.__name__}` can't be set!" - ) - - def __delattr__(self, key: str) -> None: - """Overrides :meth:`object.__delattr__` to prevent the deletion of attributes. - - Raises: - :exc:`AttributeError` - """ - # protected attributes can always be set for convenient internal use - if key[0] == "_" or not self._frozen: - super().__delattr__(key) - return - - raise AttributeError( - f"Attribute `{key}` of class `{self.__class__.__name__}` can't be deleted!" - ) def __repr__(self) -> str: """Gives a string representation of this object in the form @@ -309,27 +250,23 @@ def __setstate__(self, state: dict[str, object]) -> None: Args: state (:obj:`dict`): The data to set as attributes of this object. """ - self._unfreeze() - # Make sure that we have a `_bot` attribute. This is necessary, since __getstate__ omits # this as Bots are not pickable. - self._bot = None + self.set_bot(None) # get api_kwargs first because we may need to add entries to it (see try-except below) api_kwargs = cast("dict[str, object]", state.pop("api_kwargs", {})) - # get _frozen before the loop to avoid setting it to True in the loop - frozen = state.pop("_frozen", False) for key, val in state.items(): try: - setattr(self, key, val) + object.__setattr__(self, key, val) except AttributeError: # So an attribute was deprecated and removed from the class. Let's handle this: # 1) Is the attribute now a property with no setter? Let's check that: if isinstance(getattr(self.__class__, key, None), property): # It is, so let's try to set the "private attribute" instead try: - setattr(self, f"_{key}", val) + object.__setattr__(self, f"_{key}", val) # If this fails as well, guess we've completely removed it. Let's add it to # api_kwargs as fallback except AttributeError: @@ -344,13 +281,7 @@ def __setstate__(self, state: dict[str, object]) -> None: # and then set the rest as MappingProxyType attribute. Converting to MappingProxyType # is necessary, since __getstate__ converts it to a dict as MPT is not pickable. self._apply_api_kwargs(api_kwargs) - self.api_kwargs = MappingProxyType(api_kwargs) - - # Apply freezing if necessary - # we .get(…) the setting for backwards compatibility with objects that were pickled - # before the freeze feature was introduced - if frozen: - self._freeze() + object.__setattr__(self, "api_kwargs", MappingProxyType(api_kwargs)) def __deepcopy__(self: Tele_co, memodict: dict[int, object]) -> Tele_co: """ @@ -373,31 +304,24 @@ def __deepcopy__(self: Tele_co, memodict: dict[int, object]) -> Tele_co: result = cls.__new__(cls) # create a new instance memodict[id(self)] = result # save the id of the object in the dict - result._frozen = False # unfreeze the new object for setting the attributes - # now we set the attributes in the deepcopied object for k in self._get_attrs_names(include_private=True): - if k == "_frozen": - # Setting the frozen status to True would prevent the attributes from being set - continue if k == "api_kwargs": # Need to copy api_kwargs manually, since it's a MappingProxyType is not # pickable and deepcopy uses the pickle interface - setattr(result, k, MappingProxyType(deepcopy(dict(self.api_kwargs), memodict))) + object.__setattr__( + result, k, MappingProxyType(deepcopy(dict(self.api_kwargs), memodict)) + ) continue try: - setattr(result, k, deepcopy(getattr(self, k), memodict)) + object.__setattr__(result, k, deepcopy(getattr(self, k), memodict)) except AttributeError: # Skip missing attributes. This can happen if the object was loaded from a pickle # file that was created with an older version of the library, where the class # did not have the attribute yet. continue - # Apply freezing if necessary - if self._frozen: - result._freeze() - result.set_bot(bot) # Assign the bots back self.set_bot(bot) return result @@ -455,13 +379,15 @@ def _build_plan(cls) -> dict[str, Any]: origin = get_origin(inner) if param.default is inspect.Parameter.empty: - compatibility_defaults[name] = () if origin is Sequence else None + # We check for `is tuple` instead of `Sequence` + # because the inspected annotation returns the type declared in the field + compatibility_defaults[name] = () if origin is tuple else None if inner is dtm.datetime: plan[name] = _DATETIME_FIELD elif isinstance(inner, type) and issubclass(inner, TelegramObject): plan[name] = inner - elif origin is Sequence: + elif origin is tuple: args = get_args(inner) if not args: continue @@ -604,24 +530,6 @@ def de_list( """ return tuple(cls.de_json(d, bot) for d in data) - @contextmanager - def _unfrozen(self: Tele) -> Iterator[Tele]: - """Context manager to temporarily unfreeze the object. For internal use only. - - Note: - with to._unfrozen() as other_to: - assert to is other_to - """ - self._unfreeze() - yield self - self._freeze() - - def _freeze(self) -> None: - self._frozen = True - - def _unfreeze(self) -> None: - self._frozen = False - def _apply_api_kwargs(self, api_kwargs: JSONDict) -> None: """Loops through the api kwargs and for every key that exists as attribute of the object (and is None), it moves the value from `api_kwargs` to the attribute. @@ -643,9 +551,9 @@ def _apply_api_kwargs(self, api_kwargs: JSONDict) -> None: if isinstance(getattr(self.__class__, key, None), property): # if setattr fails, we'll just leave the value in api_kwargs: with contextlib.suppress(AttributeError): - setattr(self, f"_{key}", api_kwargs.pop(key)) + object.__setattr__(self, f"_{key}", api_kwargs.pop(key)) elif getattr(self, key, True) is None: - setattr(self, key, api_kwargs.pop(key)) + object.__setattr__(self, key, api_kwargs.pop(key)) def _is_deprecated_attr(self, attr: str) -> bool: """Checks whether `attr` is in the list of deprecated time period attributes.""" @@ -667,7 +575,11 @@ def _get_attrs_names(self, include_private: bool) -> Iterator[str]: # We want to get all attributes for the class, using self.__slots__ only includes the # attributes used by that class itself, and not its superclass(es). Hence, we get its MRO # and then get their attributes. The `[:-1]` slice excludes the `object` class - all_slots = (s for c in self.__class__.__mro__[:-1] for s in c.__slots__) # type: ignore + all_slots = ( + slot + for cls in self.__class__.__mro__[:-1] + for slot in cls.__dict__.get("__slots__", ()) + ) # chain the class's slots with the user defined subclass __dict__ (class has no slots) all_attrs = ( chain(all_slots, self.__dict__.keys()) if hasattr(self, "__dict__") else all_slots @@ -838,7 +750,7 @@ def set_bot(self, bot: "Bot | None") -> None: Arguments: bot (:class:`telegram.Bot` | :obj:`None`): The bot instance. """ - self._bot = bot + object.__setattr__(self, "_bot", bot) # We use str keys to avoid importing which causes circular dependencies diff --git a/src/telegram/_utils/de_json.py b/src/telegram/_utils/de_json.py index e412f856fb6..41f0d3c1418 100644 --- a/src/telegram/_utils/de_json.py +++ b/src/telegram/_utils/de_json.py @@ -25,7 +25,7 @@ """ import importlib -from collections.abc import Callable, Sequence +from collections.abc import Callable from functools import lru_cache from types import UnionType from typing import TYPE_CHECKING, TypeAlias, Union, cast, get_args, get_origin @@ -91,7 +91,7 @@ def build_sequence_transformer( ) item_origin = get_origin(resolved_item_annotation) - if item_origin is Sequence: + if item_origin is tuple: nested_item_annotations = get_args(resolved_item_annotation) if not nested_item_annotations: return None From b4bbfa36bee352722a1a6b24cba12b652c8f70e4 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sat, 22 Aug 2026 19:53:24 +0300 Subject: [PATCH 05/31] Add runtime support for specifier parameter `alias` --- src/telegram/_telegramobject.py | 2 +- src/telegram/_utils/dataclass.py | 96 +++++++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 9c88882f670..5822026e2da 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -362,7 +362,7 @@ def _build_plan(cls) -> dict[str, Any]: plan: dict[str, Any] = {} compatibility_defaults: dict[str, object] = {} - globalns: dict[str, object] = getattr(init_fn, "__globals__", {}) + globalns: dict[str, object] = getattr(inspect.unwrap(init_fn), "__globals__", {}) tg_ns = get_telegram_namespace() sig = inspect.signature(init_fn) cls.__INIT_PARAMS = set(sig.parameters) - {"self"} diff --git a/src/telegram/_utils/dataclass.py b/src/telegram/_utils/dataclass.py index 6558f57b9bd..7454f3a1053 100644 --- a/src/telegram/_utils/dataclass.py +++ b/src/telegram/_utils/dataclass.py @@ -24,6 +24,9 @@ the changelog. """ +import dataclasses +import functools +import inspect from collections.abc import Callable from dataclasses import MISSING, dataclass, field from typing import Any, TypeVar @@ -32,6 +35,72 @@ _T = TypeVar("_T") CONVERTER_KEY = object() +ALIAS_KEY = object() + + +def _apply_aliases(cls: type[_T]) -> type[_T]: + """Provides runtime support for the `alias` parameter of a field specifier (e.g tg_field). + + Args: + cls (:obj:`type`): A class transformed with `dataclasses.dataclass` + + The function does two things to the provided class generated `__init__`: + + 1) Wraps the generated __init__ with a generic one (*args: object, **kwargs: object) that + rejects having the unalised field_name in kwargs. + + 2) Updates the signature of the wrapper __init__ to replace parameter names from raw + to aliased. + + Note: + The aliased name is assumed to exist under the field metadata with + the sentinel key `ALIAS_KEY` + + Returns: + :obj:`type`: + `cls` *Modified In Place* + """ + aliases = { + dataclass_field.name: alias + for dataclass_field in dataclasses.fields(cls) + if dataclass_field.init and (alias := dataclass_field.metadata.get(ALIAS_KEY)) is not None + } + + if not aliases: + return cls + + generated_init = cls.__init__ + generated_signature = inspect.signature(generated_init) + + @functools.wraps(generated_init) + def aliased_init(self: object, *args: object, **kwargs: object) -> None: + for field_name, alias in aliases.items(): + # 1.1) Reject kwargs keys using the raw (unaliased) field name + if field_name in kwargs: + raise TypeError( + f"{cls.__name__}() got an unexpected keyword argument {field_name!r}" + ) + + # 1.2) Swap the kwargs key from alias to field_name to satisfy runtime signature + # of generated_init + if alias in kwargs: + kwargs[field_name] = kwargs.pop(alias) + + generated_init(self, *args, **kwargs) + + # 2) Now and since inspect.signature(generated_init/aliased_init) would still return parameters + # with raw field_names instead of aliased ones, we update those parameters in the signature + # This is neccessary because TO._build_plan expect field names to match what + # Telegram API returns + parameters = [ + parameter.replace(name=aliases.get(parameter.name) or parameter.name) + for parameter in generated_signature.parameters.values() + ] + signature = generated_signature.replace(parameters=parameters) + + aliased_init.__signature__ = signature + cls.__init__ = aliased_init + return cls def tg_field( @@ -42,13 +111,19 @@ def tg_field( compare: bool = False, kw_only: Any = MISSING, converter: Callable[[Any], Any] | None = None, + alias: str | None = None, ) -> Any: field_metadata = None - if converter is not None: + if any((converter, alias)): field_metadata = {} + + if converter is not None: field_metadata[CONVERTER_KEY] = converter + if alias is not None: + field_metadata[ALIAS_KEY] = alias + return field( # pylint: disable=invalid-field-call repr=False, compare=compare, @@ -71,10 +146,15 @@ def tg_dataclass( *, eq: bool = True, ) -> Callable[[type[_T]], type[_T]]: - return dataclass( - frozen=True, - slots=True, - repr=False, - match_args=False, - eq=eq, - ) + def decorate(cls: type[_T]) -> type[_T]: + transformed_cls = dataclass( + cls, + frozen=True, + slots=True, + repr=False, + match_args=False, + eq=eq, + ) + return _apply_aliases(transformed_cls) + + return decorate From 0026d930d4aff6dc0a67f7db2197d05dc46e2cbe Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:40:32 +0300 Subject: [PATCH 06/31] Some typing improvements to `_utils/dataclass.py` --- src/telegram/_utils/dataclass.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/telegram/_utils/dataclass.py b/src/telegram/_utils/dataclass.py index 7454f3a1053..188bc27be46 100644 --- a/src/telegram/_utils/dataclass.py +++ b/src/telegram/_utils/dataclass.py @@ -28,7 +28,7 @@ import functools import inspect from collections.abc import Callable -from dataclasses import MISSING, dataclass, field +from dataclasses import MISSING, dataclass, field, is_dataclass from typing import Any, TypeVar from typing_extensions import dataclass_transform @@ -59,7 +59,13 @@ def _apply_aliases(cls: type[_T]) -> type[_T]: Returns: :obj:`type`: `cls` *Modified In Place* + + Raises: + TypeError: If :paramref:`cls` is not a dataclass. """ + if not is_dataclass(cls): + raise TypeError(f"{cls!r} is not a dataclass") + aliases = { dataclass_field.name: alias for dataclass_field in dataclasses.fields(cls) @@ -98,8 +104,8 @@ def aliased_init(self: object, *args: object, **kwargs: object) -> None: ] signature = generated_signature.replace(parameters=parameters) - aliased_init.__signature__ = signature - cls.__init__ = aliased_init + aliased_init.__signature__ = signature # type: ignore[attr-defined] + cls.__init__ = aliased_init # type: ignore[method-assign] return cls @@ -113,10 +119,7 @@ def tg_field( converter: Callable[[Any], Any] | None = None, alias: str | None = None, ) -> Any: - field_metadata = None - - if any((converter, alias)): - field_metadata = {} + field_metadata: dict[Any, Any] = {} if converter is not None: field_metadata[CONVERTER_KEY] = converter @@ -130,7 +133,7 @@ def tg_field( init=init, default=default, default_factory=default_factory, - metadata=field_metadata, + metadata=field_metadata or None, kw_only=kw_only, ) From 12abacfcf5d5cd923c61f5e04f53ef1c7e18732c Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:44:27 +0300 Subject: [PATCH 07/31] Migrate classes direclty under `telegram/` --- src/telegram/_birthdate.py | 32 +- src/telegram/_botaccesssettings.py | 22 +- src/telegram/_botcommand.py | 26 +- src/telegram/_botcommandscope.py | 105 ++-- src/telegram/_botdescription.py | 24 +- src/telegram/_botname.py | 17 +- src/telegram/_business.py | 288 ++------- src/telegram/_callbackquery.py | 52 +- src/telegram/_chat.py | 89 ++- src/telegram/_chatadministratorrights.py | 116 +--- src/telegram/_chatbackground.py | 234 ++------ src/telegram/_chatboost.py | 169 ++---- src/telegram/_chatfullinfo.py | 234 +++----- src/telegram/_chatinvitelink.py | 74 +-- src/telegram/_chatjoinrequest.py | 36 +- src/telegram/_chatlocation.py | 27 +- src/telegram/_chatmember.py | 302 +++------- src/telegram/_chatmemberupdated.py | 60 +- src/telegram/_chatowner.py | 34 +- src/telegram/_chatpermissions.py | 109 +--- src/telegram/_checklists.py | 124 +--- src/telegram/_choseninlineresult.py | 36 +- src/telegram/_copytextbutton.py | 13 +- src/telegram/_dice.py | 45 +- src/telegram/_directmessagepricechanged.py | 21 +- src/telegram/_directmessagestopic.py | 21 +- src/telegram/_forcereply.py | 29 +- src/telegram/_forumtopic.py | 112 +--- src/telegram/_gifts.py | 217 ++----- src/telegram/_giveaway.py | 169 ++---- src/telegram/_inputchecklist.py | 73 +-- src/telegram/_keyboardbutton.py | 72 +-- src/telegram/_keyboardbuttonpolltype.py | 20 +- src/telegram/_keyboardbuttonrequest.py | 144 ++--- src/telegram/_linkpreviewoptions.py | 46 +- src/telegram/_loginurl.py | 31 +- src/telegram/_managedbot.py | 41 +- src/telegram/_menubutton.py | 57 +- src/telegram/_message.py | 555 +++++------------- .../_messageautodeletetimerchanged.py | 20 +- src/telegram/_messageentity.py | 104 ++-- src/telegram/_messageid.py | 13 +- src/telegram/_messageorigin.py | 122 ++-- src/telegram/_messagereactionupdated.py | 85 +-- src/telegram/_ownedgift.py | 178 ++---- src/telegram/_paidmedia.py | 141 ++--- src/telegram/_paidmessagepricechanged.py | 17 +- src/telegram/_poll.py | 381 ++++-------- src/telegram/_preparedkeyboardbutton.py | 18 +- src/telegram/_proximityalerttriggered.py | 24 +- src/telegram/_reaction.py | 100 +--- src/telegram/_reply.py | 218 ++----- src/telegram/_replykeyboardmarkup.py | 62 +- src/telegram/_replykeyboardremove.py | 15 +- src/telegram/_sentguestmessage.py | 20 +- src/telegram/_sentwebappmessage.py | 17 +- src/telegram/_shared.py | 83 +-- src/telegram/_story.py | 24 +- src/telegram/_storyarea.py | 232 ++------ src/telegram/_suggestedpost.py | 202 ++----- src/telegram/_switchinlinequerychosenchat.py | 44 +- src/telegram/_telegramobject.py | 7 +- src/telegram/_uniquegift.py | 305 +++------- src/telegram/_update.py | 200 +++---- src/telegram/_user.py | 87 +-- src/telegram/_userprofileaudios.py | 25 +- src/telegram/_userprofilephotos.py | 27 +- src/telegram/_userrating.py | 29 +- src/telegram/_utils/usernames.py | 42 +- src/telegram/_videochat.py | 57 +- src/telegram/_webappdata.py | 16 +- src/telegram/_webappinfo.py | 15 +- src/telegram/_webhookinfo.py | 68 +-- src/telegram/_writeaccessallowed.py | 24 +- 74 files changed, 1784 insertions(+), 5114 deletions(-) diff --git a/src/telegram/_birthdate.py b/src/telegram/_birthdate.py index 73e6c3c2d32..ca63b442bf5 100644 --- a/src/telegram/_birthdate.py +++ b/src/telegram/_birthdate.py @@ -21,9 +21,10 @@ import datetime as dtm from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class Birthdate(TelegramObject): """ This object describes the birthdate of a user. @@ -45,30 +46,11 @@ class Birthdate(TelegramObject): """ - __slots__ = ("day", "month", "year") - - def __init__( - self, - day: int, - month: int, - year: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Required - self.day: int = day - self.month: int = month - # Optional - self.year: int | None = year - - self._id_attrs = ( - self.day, - self.month, - ) - - self._freeze() + # Required + day: int = tg_field(compare=True) + month: int = tg_field(compare=True) + # Optional + year: int | None = tg_field(default=None) def to_date(self, year: int | None = None) -> dtm.date: """Return the birthdate as a date object. diff --git a/src/telegram/_botaccesssettings.py b/src/telegram/_botaccesssettings.py index 0836c07b8da..6071c2eae40 100644 --- a/src/telegram/_botaccesssettings.py +++ b/src/telegram/_botaccesssettings.py @@ -18,14 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Bot Access Settings.""" -from collections.abc import Sequence - from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class BotAccessSettings(TelegramObject): """ This object describes the access settings of a bot. @@ -48,18 +47,5 @@ class BotAccessSettings(TelegramObject): have access to the bot if the access is restricted. """ - __slots__ = ("added_users", "is_access_restricted") - - def __init__( - self, - is_access_restricted: bool, - added_users: Sequence[User] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.is_access_restricted: bool = is_access_restricted - self.added_users: tuple[User, ...] = parse_sequence_arg(added_users) - - self._id_attrs = (self.is_access_restricted, self.added_users) - self._freeze() + is_access_restricted: bool = tg_field(compare=True) + added_users: tuple[User, ...] = tg_field(compare=True, converter=parse_sequence_arg) diff --git a/src/telegram/_botcommand.py b/src/telegram/_botcommand.py index 6b697128427..75aa9737dc3 100644 --- a/src/telegram/_botcommand.py +++ b/src/telegram/_botcommand.py @@ -18,13 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Bot Command.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class BotCommand(TelegramObject): """ This object represents a bot command. @@ -50,33 +51,26 @@ class BotCommand(TelegramObject): """ - __slots__ = ("command", "description") + command: str = tg_field(compare=True) + description: str = tg_field(compare=True) - def __init__(self, command: str, description: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.command: str = command - self.description: str = description - - self._id_attrs = (self.command, self.description) - - self._freeze() - - MIN_COMMAND: Final[int] = constants.BotCommandLimit.MIN_COMMAND + # TODO: https://docs.python.org/3.13/library/typing.html#typing.ClassVar + MIN_COMMAND: ClassVar[int] = constants.BotCommandLimit.MIN_COMMAND """:const:`telegram.constants.BotCommandLimit.MIN_COMMAND` .. versionadded:: 20.0 """ - MAX_COMMAND: Final[int] = constants.BotCommandLimit.MAX_COMMAND + MAX_COMMAND: ClassVar[int] = constants.BotCommandLimit.MAX_COMMAND """:const:`telegram.constants.BotCommandLimit.MAX_COMMAND` .. versionadded:: 20.0 """ - MIN_DESCRIPTION: Final[int] = constants.BotCommandLimit.MIN_DESCRIPTION + MIN_DESCRIPTION: ClassVar[int] = constants.BotCommandLimit.MIN_DESCRIPTION """:const:`telegram.constants.BotCommandLimit.MIN_DESCRIPTION` .. versionadded:: 20.0 """ - MAX_DESCRIPTION: Final[int] = constants.BotCommandLimit.MAX_DESCRIPTION + MAX_DESCRIPTION: ClassVar[int] = constants.BotCommandLimit.MAX_DESCRIPTION """:const:`telegram.constants.BotCommandLimit.MAX_DESCRIPTION` .. versionadded:: 20.0 diff --git a/src/telegram/_botcommandscope.py b/src/telegram/_botcommandscope.py index 44aa70389d5..d0c9a81d4e7 100644 --- a/src/telegram/_botcommandscope.py +++ b/src/telegram/_botcommandscope.py @@ -16,17 +16,17 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=redefined-builtin """This module contains objects representing Telegram bot command scopes.""" -from typing import ClassVar, Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class BotCommandScope(TelegramObject): """Base class for objects that represent the scope to which bot commands are applied. Currently, the following 7 scopes are supported: @@ -57,8 +57,6 @@ class BotCommandScope(TelegramObject): type (:obj:`str`): Scope type. """ - __slots__ = ("type",) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "type", { @@ -72,29 +70,34 @@ class BotCommandScope(TelegramObject): }, ) - DEFAULT: Final[str] = constants.BotCommandScopeType.DEFAULT + # TODO: https://docs.python.org/3.13/library/typing.html#typing.ClassVar + DEFAULT: ClassVar[str] = constants.BotCommandScopeType.DEFAULT """:const:`telegram.constants.BotCommandScopeType.DEFAULT`""" - ALL_PRIVATE_CHATS: Final[str] = constants.BotCommandScopeType.ALL_PRIVATE_CHATS + ALL_PRIVATE_CHATS: ClassVar[str] = constants.BotCommandScopeType.ALL_PRIVATE_CHATS """:const:`telegram.constants.BotCommandScopeType.ALL_PRIVATE_CHATS`""" - ALL_GROUP_CHATS: Final[str] = constants.BotCommandScopeType.ALL_GROUP_CHATS + ALL_GROUP_CHATS: ClassVar[str] = constants.BotCommandScopeType.ALL_GROUP_CHATS """:const:`telegram.constants.BotCommandScopeType.ALL_GROUP_CHATS`""" - ALL_CHAT_ADMINISTRATORS: Final[str] = constants.BotCommandScopeType.ALL_CHAT_ADMINISTRATORS + ALL_CHAT_ADMINISTRATORS: ClassVar[str] = constants.BotCommandScopeType.ALL_CHAT_ADMINISTRATORS """:const:`telegram.constants.BotCommandScopeType.ALL_CHAT_ADMINISTRATORS`""" - CHAT: Final[str] = constants.BotCommandScopeType.CHAT + CHAT: ClassVar[str] = constants.BotCommandScopeType.CHAT """:const:`telegram.constants.BotCommandScopeType.CHAT`""" - CHAT_ADMINISTRATORS: Final[str] = constants.BotCommandScopeType.CHAT_ADMINISTRATORS + CHAT_ADMINISTRATORS: ClassVar[str] = constants.BotCommandScopeType.CHAT_ADMINISTRATORS """:const:`telegram.constants.BotCommandScopeType.CHAT_ADMINISTRATORS`""" - CHAT_MEMBER: Final[str] = constants.BotCommandScopeType.CHAT_MEMBER + CHAT_MEMBER: ClassVar[str] = constants.BotCommandScopeType.CHAT_MEMBER """:const:`telegram.constants.BotCommandScopeType.CHAT_MEMBER`""" - def __init__(self, type: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.BotCommandScopeType, type, type) - self._id_attrs = (self.type,) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.BotCommandScopeType, value, value) + + @staticmethod + def _chat_id_converter(value: str | int) -> str | int: + return value if isinstance(value, str) and value.startswith("@") else int(value) - self._freeze() + type: str = tg_field(compare=True, converter=_type_converter) +@tg_dataclass() class BotCommandScopeDefault(BotCommandScope): """Represents the default scope of bot commands. Default commands are used if no commands with a `narrower scope`_ are specified for the user. @@ -106,13 +109,11 @@ class BotCommandScopeDefault(BotCommandScope): type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.DEFAULT`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None): - super().__init__(type=BotCommandScope.DEFAULT, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=BotCommandScope.DEFAULT) +@tg_dataclass() class BotCommandScopeAllPrivateChats(BotCommandScope): """Represents the scope of bot commands, covering all private chats. @@ -122,13 +123,11 @@ class BotCommandScopeAllPrivateChats(BotCommandScope): type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.ALL_PRIVATE_CHATS`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None): - super().__init__(type=BotCommandScope.ALL_PRIVATE_CHATS, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=BotCommandScope.ALL_PRIVATE_CHATS) +@tg_dataclass() class BotCommandScopeAllGroupChats(BotCommandScope): """Represents the scope of bot commands, covering all group and supergroup chats. @@ -137,13 +136,10 @@ class BotCommandScopeAllGroupChats(BotCommandScope): type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.ALL_GROUP_CHATS`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None): - super().__init__(type=BotCommandScope.ALL_GROUP_CHATS, api_kwargs=api_kwargs) - self._freeze() + type: str = tg_field(init=False, default=BotCommandScope.ALL_GROUP_CHATS) +@tg_dataclass() class BotCommandScopeAllChatAdministrators(BotCommandScope): """Represents the scope of bot commands, covering all group and supergroup chat administrators. @@ -152,13 +148,11 @@ class BotCommandScopeAllChatAdministrators(BotCommandScope): type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.ALL_CHAT_ADMINISTRATORS`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None): - super().__init__(type=BotCommandScope.ALL_CHAT_ADMINISTRATORS, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=BotCommandScope.ALL_CHAT_ADMINISTRATORS) +@tg_dataclass() class BotCommandScopeChat(BotCommandScope): """Represents the scope of bot commands, covering a specific chat. @@ -175,17 +169,13 @@ class BotCommandScopeChat(BotCommandScope): chat_id (:obj:`str` | :obj:`int`): |chat_id_group| """ - __slots__ = ("chat_id",) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=BotCommandScope.CHAT) - def __init__(self, chat_id: str | int, *, api_kwargs: JSONDict | None = None): - super().__init__(type=BotCommandScope.CHAT, api_kwargs=api_kwargs) - with self._unfrozen(): - self.chat_id: str | int = ( - chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) - ) - self._id_attrs = (self.type, self.chat_id) + chat_id: str | int = tg_field(compare=True, converter=BotCommandScope._chat_id_converter) +@tg_dataclass() class BotCommandScopeChatAdministrators(BotCommandScope): """Represents the scope of bot commands, covering all administrators of a specific group or supergroup chat. @@ -202,17 +192,13 @@ class BotCommandScopeChatAdministrators(BotCommandScope): chat_id (:obj:`str` | :obj:`int`): |chat_id_group| """ - __slots__ = ("chat_id",) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=BotCommandScope.CHAT_ADMINISTRATORS) - def __init__(self, chat_id: str | int, *, api_kwargs: JSONDict | None = None): - super().__init__(type=BotCommandScope.CHAT_ADMINISTRATORS, api_kwargs=api_kwargs) - with self._unfrozen(): - self.chat_id: str | int = ( - chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) - ) - self._id_attrs = (self.type, self.chat_id) + chat_id: str | int = tg_field(compare=True, converter=BotCommandScope._chat_id_converter) +@tg_dataclass() class BotCommandScopeChatMember(BotCommandScope): """Represents the scope of bot commands, covering a specific member of a group or supergroup chat. @@ -232,13 +218,8 @@ class BotCommandScopeChatMember(BotCommandScope): user_id (:obj:`int`): Unique identifier of the target user. """ - __slots__ = ("chat_id", "user_id") + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=BotCommandScope.CHAT_MEMBER) - def __init__(self, chat_id: str | int, user_id: int, *, api_kwargs: JSONDict | None = None): - super().__init__(type=BotCommandScope.CHAT_MEMBER, api_kwargs=api_kwargs) - with self._unfrozen(): - self.chat_id: str | int = ( - chat_id if isinstance(chat_id, str) and chat_id.startswith("@") else int(chat_id) - ) - self.user_id: int = user_id - self._id_attrs = (self.type, self.chat_id, self.user_id) + chat_id: str | int = tg_field(compare=True, converter=BotCommandScope._chat_id_converter) + user_id: int = tg_field(compare=True) diff --git a/src/telegram/_botdescription.py b/src/telegram/_botdescription.py index e7fe7211cb3..b85cbd64990 100644 --- a/src/telegram/_botdescription.py +++ b/src/telegram/_botdescription.py @@ -19,9 +19,10 @@ """This module contains two objects that represent a Telegram bots (short) description.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class BotDescription(TelegramObject): """This object represents the bot's description. @@ -38,17 +39,10 @@ class BotDescription(TelegramObject): """ - __slots__ = ("description",) - - def __init__(self, description: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.description: str = description - - self._id_attrs = (self.description,) - - self._freeze() + description: str = tg_field(compare=True) +@tg_dataclass() class BotShortDescription(TelegramObject): """This object represents the bot's short description. @@ -65,12 +59,4 @@ class BotShortDescription(TelegramObject): """ - __slots__ = ("short_description",) - - def __init__(self, short_description: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.short_description: str = short_description - - self._id_attrs = (self.short_description,) - - self._freeze() + short_description: str = tg_field(compare=True) diff --git a/src/telegram/_botname.py b/src/telegram/_botname.py index 42021df472e..8f4c6a75a24 100644 --- a/src/telegram/_botname.py +++ b/src/telegram/_botname.py @@ -18,13 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represent a Telegram bots name.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class BotName(TelegramObject): """This object represents the bot's name. @@ -41,15 +42,7 @@ class BotName(TelegramObject): """ - __slots__ = ("name",) + name: str = tg_field(compare=True) - def __init__(self, name: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.name: str = name - - self._id_attrs = (self.name,) - - self._freeze() - - MAX_LENGTH: Final[int] = constants.BotNameLimit.MAX_NAME_LENGTH + MAX_LENGTH: ClassVar[int] = constants.BotNameLimit.MAX_NAME_LENGTH """:const:`telegram.constants.BotNameLimit.MAX_NAME_LENGTH`""" diff --git a/src/telegram/_business.py b/src/telegram/_business.py index 58b619465be..6bd1bc3b02a 100644 --- a/src/telegram/_business.py +++ b/src/telegram/_business.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# pylint: disable=redefined-builtin # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2026 @@ -20,7 +19,6 @@ """This module contains the Telegram Business related classes.""" import datetime as dtm -from collections.abc import Sequence from typing import TYPE_CHECKING from zoneinfo import ZoneInfo @@ -30,16 +28,17 @@ from telegram._utils.argumentparsing import ( parse_sequence_arg, ) +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import ( get_zone_info, ) -from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram._files.location import Location from telegram._user import User +@tg_dataclass() class BusinessBotRights(TelegramObject): """ This object represents the rights of a business bot. @@ -112,78 +111,23 @@ class BusinessBotRights(TelegramObject): stories on behalf of the business account. """ - __slots__ = ( - "can_change_gift_settings", - "can_convert_gifts_to_stars", - "can_delete_all_messages", - "can_delete_sent_messages", - "can_edit_bio", - "can_edit_name", - "can_edit_profile_photo", - "can_edit_username", - "can_manage_stories", - "can_read_messages", - "can_reply", - "can_transfer_and_upgrade_gifts", - "can_transfer_stars", - "can_view_gifts_and_stars", - ) - - def __init__( - self, - can_reply: bool | None = None, - can_read_messages: bool | None = None, - can_delete_sent_messages: bool | None = None, - can_delete_all_messages: bool | None = None, - can_edit_name: bool | None = None, - can_edit_bio: bool | None = None, - can_edit_profile_photo: bool | None = None, - can_edit_username: bool | None = None, - can_change_gift_settings: bool | None = None, - can_view_gifts_and_stars: bool | None = None, - can_convert_gifts_to_stars: bool | None = None, - can_transfer_and_upgrade_gifts: bool | None = None, - can_transfer_stars: bool | None = None, - can_manage_stories: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.can_reply: bool | None = can_reply - self.can_read_messages: bool | None = can_read_messages - self.can_delete_sent_messages: bool | None = can_delete_sent_messages - self.can_delete_all_messages: bool | None = can_delete_all_messages - self.can_edit_name: bool | None = can_edit_name - self.can_edit_bio: bool | None = can_edit_bio - self.can_edit_profile_photo: bool | None = can_edit_profile_photo - self.can_edit_username: bool | None = can_edit_username - self.can_change_gift_settings: bool | None = can_change_gift_settings - self.can_view_gifts_and_stars: bool | None = can_view_gifts_and_stars - self.can_convert_gifts_to_stars: bool | None = can_convert_gifts_to_stars - self.can_transfer_and_upgrade_gifts: bool | None = can_transfer_and_upgrade_gifts - self.can_transfer_stars: bool | None = can_transfer_stars - self.can_manage_stories: bool | None = can_manage_stories - - self._id_attrs = ( - self.can_reply, - self.can_read_messages, - self.can_delete_sent_messages, - self.can_delete_all_messages, - self.can_edit_name, - self.can_edit_bio, - self.can_edit_profile_photo, - self.can_edit_username, - self.can_change_gift_settings, - self.can_view_gifts_and_stars, - self.can_convert_gifts_to_stars, - self.can_transfer_and_upgrade_gifts, - self.can_transfer_stars, - self.can_manage_stories, - ) - - self._freeze() - - + can_reply: bool | None = tg_field(compare=True, default=None) + can_read_messages: bool | None = tg_field(compare=True, default=None) + can_delete_sent_messages: bool | None = tg_field(compare=True, default=None) + can_delete_all_messages: bool | None = tg_field(compare=True, default=None) + can_edit_name: bool | None = tg_field(compare=True, default=None) + can_edit_bio: bool | None = tg_field(compare=True, default=None) + can_edit_profile_photo: bool | None = tg_field(compare=True, default=None) + can_edit_username: bool | None = tg_field(compare=True, default=None) + can_change_gift_settings: bool | None = tg_field(compare=True, default=None) + can_view_gifts_and_stars: bool | None = tg_field(compare=True, default=None) + can_convert_gifts_to_stars: bool | None = tg_field(compare=True, default=None) + can_transfer_and_upgrade_gifts: bool | None = tg_field(compare=True, default=None) + can_transfer_stars: bool | None = tg_field(compare=True, default=None) + can_manage_stories: bool | None = tg_field(compare=True, default=None) + + +@tg_dataclass() class BusinessConnection(TelegramObject): """ Describes the connection of the bot with a business account. @@ -222,46 +166,15 @@ class BusinessConnection(TelegramObject): .. versionadded:: 22.1 """ - __slots__ = ( - "date", - "id", - "is_enabled", - "rights", - "user", - "user_chat_id", - ) - - def __init__( - self, - id: str, - user: "User", - user_chat_id: int, - date: dtm.datetime, - is_enabled: bool, - rights: BusinessBotRights | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.id: str = id - self.user: User = user - self.user_chat_id: int = user_chat_id - self.date: dtm.datetime = date - self.is_enabled: bool = is_enabled - self.rights: BusinessBotRights | None = rights - - self._id_attrs = ( - self.id, - self.user, - self.user_chat_id, - self.date, - self.rights, - self.is_enabled, - ) - - self._freeze() + id: str = tg_field(compare=True) + user: "User" = tg_field(compare=True) + user_chat_id: int = tg_field(compare=True) + date: dtm.datetime = tg_field(compare=True) + is_enabled: bool = tg_field(compare=True) + rights: BusinessBotRights | None = tg_field(compare=True, default=None) +@tg_dataclass() class BusinessMessagesDeleted(TelegramObject): """ This object is received when messages are deleted from a connected business account. @@ -287,34 +200,12 @@ class BusinessMessagesDeleted(TelegramObject): chat of the business account. """ - __slots__ = ( - "business_connection_id", - "chat", - "message_ids", - ) - - def __init__( - self, - business_connection_id: str, - chat: Chat, - message_ids: Sequence[int], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.business_connection_id: str = business_connection_id - self.chat: Chat = chat - self.message_ids: tuple[int, ...] = parse_sequence_arg(message_ids) - - self._id_attrs = ( - self.business_connection_id, - self.chat, - self.message_ids, - ) - - self._freeze() + business_connection_id: str = tg_field(compare=True) + chat: Chat = tg_field(compare=True) + message_ids: tuple[int, ...] = tg_field(compare=True, converter=parse_sequence_arg) +@tg_dataclass() class BusinessIntro(TelegramObject): """ This object contains information about the start page settings of a Telegram Business account. @@ -336,30 +227,12 @@ class BusinessIntro(TelegramObject): sticker (:class:`telegram.Sticker`): Optional. Sticker of the business intro. """ - __slots__ = ( - "message", - "sticker", - "title", - ) - - def __init__( - self, - title: str | None = None, - message: str | None = None, - sticker: Sticker | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.title: str | None = title - self.message: str | None = message - self.sticker: Sticker | None = sticker - - self._id_attrs = (self.title, self.message, self.sticker) - - self._freeze() + title: str | None = tg_field(compare=True, default=None) + message: str | None = tg_field(compare=True, default=None) + sticker: Sticker | None = tg_field(compare=True, default=None) +@tg_dataclass() class BusinessLocation(TelegramObject): """ This object contains information about the location of a Telegram Business account. @@ -379,27 +252,11 @@ class BusinessLocation(TelegramObject): location (:class:`telegram.Location`): Optional. Location of the business. """ - __slots__ = ( - "address", - "location", - ) - - def __init__( - self, - address: str, - location: "Location | None" = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.address: str = address - self.location: Location | None = location - - self._id_attrs = (self.address,) - - self._freeze() + address: str = tg_field(compare=True) + location: "Location | None" = tg_field(default=None) +@tg_dataclass() class BusinessOpeningHoursInterval(TelegramObject): """ This object describes an interval of time during which a business is open. @@ -442,25 +299,11 @@ class BusinessOpeningHoursInterval(TelegramObject): during which the business is open; 0 - 8 * 24 * 60 """ - __slots__ = ("_closing_time", "_opening_time", "closing_minute", "opening_minute") + opening_minute: int = tg_field(compare=True) + closing_minute: int = tg_field(compare=True) - def __init__( - self, - opening_minute: int, - closing_minute: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.opening_minute: int = opening_minute - self.closing_minute: int = closing_minute - - self._opening_time: tuple[int, int, int] | None = None - self._closing_time: tuple[int, int, int] | None = None - - self._id_attrs = (self.opening_minute, self.closing_minute) - - self._freeze() + _opening_time: tuple[int, int, int] | None = tg_field(init=False, default=None) + _closing_time: tuple[int, int, int] | None = tg_field(init=False, default=None) def _parse_minute(self, minute: int) -> tuple[int, int, int]: return (minute // 1440, minute % 1440 // 60, minute % 1440 % 60) @@ -474,9 +317,11 @@ def opening_time(self) -> tuple[int, int, int]: Returns: tuple[:obj:`int`, :obj:`int`, :obj:`int`]: """ - if self._opening_time is None: - self._opening_time = self._parse_minute(self.opening_minute) - return self._opening_time + opening_time = self._opening_time + if opening_time is None: + opening_time = self._parse_minute(self.opening_minute) + object.__setattr__(self, "_opening_time", opening_time) + return opening_time @property def closing_time(self) -> tuple[int, int, int]: @@ -487,11 +332,14 @@ def closing_time(self) -> tuple[int, int, int]: Returns: tuple[:obj:`int`, :obj:`int`, :obj:`int`]: """ - if self._closing_time is None: - self._closing_time = self._parse_minute(self.closing_minute) - return self._closing_time + closing_time = self._closing_time + if closing_time is None: + closing_time = self._parse_minute(self.closing_minute) + object.__setattr__(self, "_closing_time", closing_time) + return closing_time +@tg_dataclass() class BusinessOpeningHours(TelegramObject): """ This object describes the opening hours of a business. @@ -515,32 +363,20 @@ class BusinessOpeningHours(TelegramObject): time intervals describing business opening hours. """ - __slots__ = ("_cached_zone_info", "opening_hours", "time_zone_name") - - def __init__( - self, - time_zone_name: str, - opening_hours: Sequence[BusinessOpeningHoursInterval], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.time_zone_name: str = time_zone_name - self.opening_hours: Sequence[BusinessOpeningHoursInterval] = parse_sequence_arg( - opening_hours - ) - - self._cached_zone_info: ZoneInfo | None = None - - self._id_attrs = (self.time_zone_name, self.opening_hours) + time_zone_name: str = tg_field(compare=True) + opening_hours: tuple[BusinessOpeningHoursInterval, ...] = tg_field( + compare=True, converter=parse_sequence_arg + ) - self._freeze() + _cached_zone_info: ZoneInfo | None = tg_field(init=False, default=None) @property def _zone_info(self) -> ZoneInfo: - if self._cached_zone_info is None: - self._cached_zone_info = get_zone_info(self.time_zone_name) - return self._cached_zone_info + cached_zone_info = self._cached_zone_info + if cached_zone_info is None: + cached_zone_info = get_zone_info(self.time_zone_name) + object.__setattr__(self, "_cached_zone_info", cached_zone_info) + return cached_zone_info def get_opening_hours_for_day( self, date: dtm.date, time_zone: dtm.tzinfo | str | None = None diff --git a/src/telegram/_callbackquery.py b/src/telegram/_callbackquery.py index 38ce19814c0..4ea039e673d 100644 --- a/src/telegram/_callbackquery.py +++ b/src/telegram/_callbackquery.py @@ -16,17 +16,17 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=redefined-builtin """This module contains an object that represents a Telegram CallbackQuery""" from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._inputchecklist import InputChecklist from telegram._message import MaybeInaccessibleMessage, Message from telegram._telegramobject import TelegramObject from telegram._user import User +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput, TimePeriod @@ -45,6 +45,7 @@ from telegram._utils.types import ReplyMarkup +@tg_dataclass() class CallbackQuery(TelegramObject): """ This object represents an incoming callback query from a callback button in an inline keyboard. @@ -114,42 +115,15 @@ class CallbackQuery(TelegramObject): """ - __slots__ = ( - "chat_instance", - "data", - "from_user", - "game_short_name", - "id", - "inline_message_id", - "message", - ) - - def __init__( - self, - id: str, - from_user: User, - chat_instance: str, - message: MaybeInaccessibleMessage | None = None, - data: str | None = None, - inline_message_id: str | None = None, - game_short_name: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.id: str = id - self.from_user: User = from_user - self.chat_instance: str = chat_instance - # Optionals - self.message: MaybeInaccessibleMessage | None = message - self.data: str | None = data - self.inline_message_id: str | None = inline_message_id - self.game_short_name: str | None = game_short_name - - self._id_attrs = (self.id,) - - self._freeze() + # Required + id: str = tg_field(compare=True) + from_user: User = tg_field() + chat_instance: str = tg_field() + # Optionals + message: MaybeInaccessibleMessage | None = tg_field(default=None) + data: str | None = tg_field(default=None) + inline_message_id: str | None = tg_field(default=None) + game_short_name: str | None = tg_field(default=None) async def answer( self, @@ -917,7 +891,7 @@ async def copy_message( message_effect_id=message_effect_id, ) - MAX_ANSWER_TEXT_LENGTH: Final[int] = ( + MAX_ANSWER_TEXT_LENGTH: ClassVar[int] = ( constants.CallbackQueryLimit.ANSWER_CALLBACK_QUERY_TEXT_LENGTH ) """ diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index 9c3ac7b120a..b57ef0b965e 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -22,7 +22,7 @@ import datetime as dtm from collections.abc import Sequence from html import escape -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._chatpermissions import ChatPermissions @@ -31,6 +31,7 @@ from telegram._reaction import ReactionType from telegram._telegramobject import TelegramObject from telegram._utils import enum +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ( CorrectOptionIds, @@ -85,64 +86,52 @@ from telegram._utils.types import ReplyMarkup +@tg_dataclass() class _ChatBase(TelegramObject): """Base class for :class:`telegram.Chat` and :class:`telegram.ChatFullInfo`. .. versionadded:: 21.3 """ - __slots__ = ( - "first_name", - "id", - "is_direct_messages", - "is_forum", - "last_name", - "title", - "type", - "username", - ) - - def __init__( - self, - id: int, - type: str, - title: str | None = None, - username: str | None = None, - first_name: str | None = None, - last_name: str | None = None, - is_forum: bool | None = None, - is_direct_messages: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.id: int = id - self.type: str = enum.get_member(constants.ChatType, type, type) - # Optionals - self.title: str | None = title - self.username: str | None = username - self.first_name: str | None = first_name - self.last_name: str | None = last_name - self.is_forum: bool | None = is_forum - self.is_direct_messages: bool | None = is_direct_messages - - self._id_attrs = (self.id,) - - self._freeze() - - SENDER: Final[str] = constants.ChatType.SENDER + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.ChatType, value, value) + + id: int = tg_field(compare=True) + type: str = tg_field(converter=_type_converter) + + if TYPE_CHECKING: + # These fields are shared and should be be declared by each inheriting class. + # They're not declared here since they're optional and some child classes + # (e.g ChatFullInfo) have required fields that are unique to them, which leads + # to TypeError: non-default argument '..' follows default argument + # They are declared here under `TYPE_CHECKING` to support static checking of the shared + # (link, *_name, mention_*) methods below + @property + def title(self) -> str | None: ... + @property + def username(self) -> str | None: ... + @property + def first_name(self) -> str | None: ... + @property + def last_name(self) -> str | None: ... + @property + def is_forum(self) -> bool | None: ... + @property + def is_direct_messages(self) -> bool | None: ... + + SENDER: ClassVar[str] = constants.ChatType.SENDER """:const:`telegram.constants.ChatType.SENDER` .. versionadded:: 13.5 """ - PRIVATE: Final[str] = constants.ChatType.PRIVATE + PRIVATE: ClassVar[str] = constants.ChatType.PRIVATE """:const:`telegram.constants.ChatType.PRIVATE`""" - GROUP: Final[str] = constants.ChatType.GROUP + GROUP: ClassVar[str] = constants.ChatType.GROUP """:const:`telegram.constants.ChatType.GROUP`""" - SUPERGROUP: Final[str] = constants.ChatType.SUPERGROUP + SUPERGROUP: ClassVar[str] = constants.ChatType.SUPERGROUP """:const:`telegram.constants.ChatType.SUPERGROUP`""" - CHANNEL: Final[str] = constants.ChatType.CHANNEL + CHANNEL: ClassVar[str] = constants.ChatType.CHANNEL """:const:`telegram.constants.ChatType.CHANNEL`""" @property @@ -4209,6 +4198,7 @@ async def delete_all_reactions( ) +@tg_dataclass() class Chat(_ChatBase): """This object represents a chat. @@ -4271,4 +4261,9 @@ class Chat(_ChatBase): .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups """ - __slots__ = () + title: str | None = tg_field(default=None) + username: str | None = tg_field(default=None) + first_name: str | None = tg_field(default=None) + last_name: str | None = tg_field(default=None) + is_forum: bool | None = tg_field(default=None) + is_direct_messages: bool | None = tg_field(default=None) diff --git a/src/telegram/_chatadministratorrights.py b/src/telegram/_chatadministratorrights.py index 09b4b70adf6..4fe964496e5 100644 --- a/src/telegram/_chatadministratorrights.py +++ b/src/telegram/_chatadministratorrights.py @@ -18,10 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the class which represents a Telegram ChatAdministratorRights.""" +import dataclasses + from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ChatAdministratorRights(TelegramObject): """Represents the rights of an administrator in a chat. @@ -174,90 +177,25 @@ class ChatAdministratorRights(TelegramObject): .. versionadded:: 22.7 """ - __slots__ = ( - "can_change_info", - "can_delete_messages", - "can_delete_stories", - "can_edit_messages", - "can_edit_stories", - "can_invite_users", - "can_manage_chat", - "can_manage_direct_messages", - "can_manage_tags", - "can_manage_topics", - "can_manage_video_chats", - "can_pin_messages", - "can_post_messages", - "can_post_stories", - "can_promote_members", - "can_restrict_members", - "is_anonymous", - ) - - def __init__( - self, - is_anonymous: bool, - can_manage_chat: bool, - can_delete_messages: bool, - can_manage_video_chats: bool, - can_restrict_members: bool, - can_promote_members: bool, - can_change_info: bool, - can_invite_users: bool, - can_post_stories: bool, - can_edit_stories: bool, - can_delete_stories: bool, - can_post_messages: bool | None = None, - can_edit_messages: bool | None = None, - can_pin_messages: bool | None = None, - can_manage_topics: bool | None = None, - can_manage_direct_messages: bool | None = None, - can_manage_tags: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - # Required - self.is_anonymous: bool = is_anonymous - self.can_manage_chat: bool = can_manage_chat - self.can_delete_messages: bool = can_delete_messages - self.can_manage_video_chats: bool = can_manage_video_chats - self.can_restrict_members: bool = can_restrict_members - self.can_promote_members: bool = can_promote_members - self.can_change_info: bool = can_change_info - self.can_invite_users: bool = can_invite_users - self.can_post_stories: bool = can_post_stories - self.can_edit_stories: bool = can_edit_stories - self.can_delete_stories: bool = can_delete_stories - # Optionals - self.can_post_messages: bool | None = can_post_messages - self.can_edit_messages: bool | None = can_edit_messages - self.can_pin_messages: bool | None = can_pin_messages - self.can_manage_topics: bool | None = can_manage_topics - self.can_manage_direct_messages: bool | None = can_manage_direct_messages - self.can_manage_tags: bool | None = can_manage_tags - - self._id_attrs = ( - self.is_anonymous, - self.can_manage_chat, - self.can_delete_messages, - self.can_manage_video_chats, - self.can_restrict_members, - self.can_promote_members, - self.can_change_info, - self.can_invite_users, - self.can_post_messages, - self.can_edit_messages, - self.can_pin_messages, - self.can_manage_topics, - self.can_post_stories, - self.can_edit_stories, - self.can_delete_stories, - self.can_manage_direct_messages, - self.can_manage_tags, - ) - - self._freeze() + # Required + is_anonymous: bool = tg_field(compare=True) + can_manage_chat: bool = tg_field(compare=True) + can_delete_messages: bool = tg_field(compare=True) + can_manage_video_chats: bool = tg_field(compare=True) + can_restrict_members: bool = tg_field(compare=True) + can_promote_members: bool = tg_field(compare=True) + can_change_info: bool = tg_field(compare=True) + can_invite_users: bool = tg_field(compare=True) + can_post_stories: bool = tg_field(compare=True) + can_edit_stories: bool = tg_field(compare=True) + can_delete_stories: bool = tg_field(compare=True) + # Optionals + can_post_messages: bool | None = tg_field(compare=True, default=None) + can_edit_messages: bool | None = tg_field(compare=True, default=None) + can_pin_messages: bool | None = tg_field(compare=True, default=None) + can_manage_topics: bool | None = tg_field(compare=True, default=None) + can_manage_direct_messages: bool | None = tg_field(compare=True, default=None) + can_manage_tags: bool | None = tg_field(compare=True, default=None) @classmethod def all_rights(cls) -> "ChatAdministratorRights": @@ -268,7 +206,9 @@ def all_rights(cls) -> "ChatAdministratorRights": .. versionadded:: 20.0 """ - return cls(*(True,) * len(cls.__slots__)) + return cls( + *(True for field in dataclasses.fields(cls) if field.init and not field.kw_only) + ) @classmethod def no_rights(cls) -> "ChatAdministratorRights": @@ -278,4 +218,6 @@ def no_rights(cls) -> "ChatAdministratorRights": .. versionadded:: 20.0 """ - return cls(*(False,) * len(cls.__slots__)) + return cls( + *(False for field in dataclasses.fields(cls) if field.init and not field.kw_only) + ) diff --git a/src/telegram/_chatbackground.py b/src/telegram/_chatbackground.py index fe833b0f518..186e460c0b1 100644 --- a/src/telegram/_chatbackground.py +++ b/src/telegram/_chatbackground.py @@ -18,17 +18,17 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects related to chat backgrounds.""" -from collections.abc import Sequence -from typing import ClassVar, Final +from typing import ClassVar from telegram import constants from telegram._files.document import Document from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class BackgroundFill(TelegramObject): """Base class for Telegram BackgroundFill Objects. It can be one of: @@ -52,8 +52,6 @@ class BackgroundFill(TelegramObject): or :attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`. """ - __slots__ = ("type",) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "type", { @@ -63,29 +61,23 @@ class BackgroundFill(TelegramObject): }, ) - SOLID: Final[constants.BackgroundFillType] = constants.BackgroundFillType.SOLID + SOLID: ClassVar[constants.BackgroundFillType] = constants.BackgroundFillType.SOLID """:const:`telegram.constants.BackgroundFillType.SOLID`""" - GRADIENT: Final[constants.BackgroundFillType] = constants.BackgroundFillType.GRADIENT + GRADIENT: ClassVar[constants.BackgroundFillType] = constants.BackgroundFillType.GRADIENT """:const:`telegram.constants.BackgroundFillType.GRADIENT`""" - FREEFORM_GRADIENT: Final[constants.BackgroundFillType] = ( + FREEFORM_GRADIENT: ClassVar[constants.BackgroundFillType] = ( constants.BackgroundFillType.FREEFORM_GRADIENT ) """:const:`telegram.constants.BackgroundFillType.FREEFORM_GRADIENT`""" - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required by all subclasses - self.type: str = enum.get_member(constants.BackgroundFillType, type, type) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.BackgroundFillType, value, value) - self._id_attrs = (self.type,) - self._freeze() + type: str = tg_field(compare=True, converter=_type_converter) +@tg_dataclass() class BackgroundFillSolid(BackgroundFill): """ The background is filled using the selected color. @@ -104,22 +96,13 @@ class BackgroundFillSolid(BackgroundFill): color (:obj:`int`): The color of the background fill in the `RGB24` format. """ - __slots__ = ("color",) - - def __init__( - self, - color: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.SOLID, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.color: int = color + # Attribute only (init=False) + type: str = tg_field(init=False, default=BackgroundFill.SOLID) - self._id_attrs = (self.color,) + color: int = tg_field(compare=True) +@tg_dataclass() class BackgroundFillGradient(BackgroundFill): """ The background is a gradient fill. @@ -148,26 +131,15 @@ class BackgroundFillGradient(BackgroundFill): 0-:tg-const:`telegram.constants.BackgroundFillLimit.MAX_ROTATION_ANGLE`. """ - __slots__ = ("bottom_color", "rotation_angle", "top_color") - - def __init__( - self, - top_color: int, - bottom_color: int, - rotation_angle: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.GRADIENT, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.top_color: int = top_color - self.bottom_color: int = bottom_color - self.rotation_angle: int = rotation_angle + # Attribute only (init=False) + type: str = tg_field(init=False, default=BackgroundFill.GRADIENT) - self._id_attrs = (self.top_color, self.bottom_color, self.rotation_angle) + top_color: int = tg_field(compare=True) + bottom_color: int = tg_field(compare=True) + rotation_angle: int = tg_field(compare=True) +@tg_dataclass() class BackgroundFillFreeformGradient(BackgroundFill): """ The background is a freeform gradient that rotates after every message in the chat. @@ -188,22 +160,13 @@ class BackgroundFillFreeformGradient(BackgroundFill): generate the freeform gradient in the `RGB24` format """ - __slots__ = ("colors",) + # Attribute only (init=False) + type: str = tg_field(init=False, default=BackgroundFill.FREEFORM_GRADIENT) - def __init__( - self, - colors: Sequence[int], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.FREEFORM_GRADIENT, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.colors: tuple[int, ...] = parse_sequence_arg(colors) - - self._id_attrs = (self.colors,) + colors: tuple[int, ...] = tg_field(compare=True, converter=parse_sequence_arg) +@tg_dataclass() class BackgroundType(TelegramObject): """Base class for Telegram BackgroundType Objects. It can be one of: @@ -231,8 +194,6 @@ class BackgroundType(TelegramObject): """ - __slots__ = ("type",) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "type", { @@ -243,29 +204,23 @@ class BackgroundType(TelegramObject): }, ) - FILL: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.FILL + FILL: ClassVar[constants.BackgroundTypeType] = constants.BackgroundTypeType.FILL """:const:`telegram.constants.BackgroundTypeType.FILL`""" - WALLPAPER: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.WALLPAPER + WALLPAPER: ClassVar[constants.BackgroundTypeType] = constants.BackgroundTypeType.WALLPAPER """:const:`telegram.constants.BackgroundTypeType.WALLPAPER`""" - PATTERN: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.PATTERN + PATTERN: ClassVar[constants.BackgroundTypeType] = constants.BackgroundTypeType.PATTERN """:const:`telegram.constants.BackgroundTypeType.PATTERN`""" - CHAT_THEME: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.CHAT_THEME + CHAT_THEME: ClassVar[constants.BackgroundTypeType] = constants.BackgroundTypeType.CHAT_THEME """:const:`telegram.constants.BackgroundTypeType.CHAT_THEME`""" - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required by all subclasses - self.type: str = enum.get_member(constants.BackgroundTypeType, type, type) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.BackgroundTypeType, value, value) - self._id_attrs = (self.type,) - self._freeze() + type: str = tg_field(compare=True, converter=_type_converter) +@tg_dataclass() class BackgroundTypeFill(BackgroundType): """ The background is automatically filled based on the selected colors. @@ -290,24 +245,14 @@ class BackgroundTypeFill(BackgroundType): 0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`. """ - __slots__ = ("dark_theme_dimming", "fill") - - def __init__( - self, - fill: BackgroundFill, - dark_theme_dimming: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.FILL, api_kwargs=api_kwargs) + # Attribute only (init=False) + type: str = tg_field(init=False, default=BackgroundType.FILL) - with self._unfrozen(): - self.fill: BackgroundFill = fill - self.dark_theme_dimming: int = dark_theme_dimming - - self._id_attrs = (self.fill, self.dark_theme_dimming) + fill: BackgroundFill = tg_field(compare=True) + dark_theme_dimming: int = tg_field(compare=True) +@tg_dataclass() class BackgroundTypeWallpaper(BackgroundType): """ The background is a wallpaper in the `JPEG` format. @@ -340,30 +285,18 @@ class BackgroundTypeWallpaper(BackgroundType): when the device is tilted """ - __slots__ = ("dark_theme_dimming", "document", "is_blurred", "is_moving") - - def __init__( - self, - document: Document, - dark_theme_dimming: int, - is_blurred: bool | None = None, - is_moving: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.WALLPAPER, api_kwargs=api_kwargs) - - with self._unfrozen(): - # Required - self.document: Document = document - self.dark_theme_dimming: int = dark_theme_dimming - # Optionals - self.is_blurred: bool | None = is_blurred - self.is_moving: bool | None = is_moving + # Attribute only (init=False) + type: str = tg_field(init=False, default=BackgroundType.WALLPAPER) - self._id_attrs = (self.document, self.dark_theme_dimming) + # Required + document: Document = tg_field(compare=True) + dark_theme_dimming: int = tg_field(compare=True) + # Optionals + is_blurred: bool | None = tg_field(default=None) + is_moving: bool | None = tg_field(default=None) +@tg_dataclass() class BackgroundTypePattern(BackgroundType): """ The background is a ``.PNG`` or ``.TGV`` (gzipped subset of ``SVG`` with ``MIME`` type @@ -404,38 +337,19 @@ class BackgroundTypePattern(BackgroundType): when the device is tilted. """ - __slots__ = ( - "document", - "fill", - "intensity", - "is_inverted", - "is_moving", - ) + # Attribute only (init=False) + type: str = tg_field(init=False, default=BackgroundType.PATTERN) - def __init__( - self, - document: Document, - fill: BackgroundFill, - intensity: int, - is_inverted: bool | None = None, - is_moving: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.PATTERN, api_kwargs=api_kwargs) - - with self._unfrozen(): - # Required - self.document: Document = document - self.fill: BackgroundFill = fill - self.intensity: int = intensity - # Optionals - self.is_inverted: bool | None = is_inverted - self.is_moving: bool | None = is_moving - - self._id_attrs = (self.document, self.fill, self.intensity) + # Required + document: Document = tg_field(compare=True) + fill: BackgroundFill = tg_field(compare=True) + intensity: int = tg_field(compare=True) + # Optionals + is_inverted: bool | None = tg_field(default=None) + is_moving: bool | None = tg_field(default=None) +@tg_dataclass() class BackgroundTypeChatTheme(BackgroundType): """ The background is taken directly from a built-in chat theme. @@ -454,22 +368,12 @@ class BackgroundTypeChatTheme(BackgroundType): theme_name (:obj:`str`): Name of the chat theme, which is usually an emoji. """ - __slots__ = ("theme_name",) - - def __init__( - self, - theme_name: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.CHAT_THEME, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.theme_name: str = theme_name - - self._id_attrs = (self.theme_name,) + # Attribute only (init=False) + type: str = tg_field(init=False, default=BackgroundType.CHAT_THEME) + theme_name: str = tg_field(compare=True) +@tg_dataclass() class ChatBackground(TelegramObject): """ This object represents a chat background. @@ -486,16 +390,4 @@ class ChatBackground(TelegramObject): type (:class:`telegram.BackgroundType`): Type of the background. """ - __slots__ = ("type",) - - def __init__( - self, - type: BackgroundType, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.type: BackgroundType = type - - self._id_attrs = (self.type,) - self._freeze() + type: BackgroundType = tg_field(compare=True) diff --git a/src/telegram/_chatboost.py b/src/telegram/_chatboost.py index 4889f9c9ec8..879a94e2b50 100644 --- a/src/telegram/_chatboost.py +++ b/src/telegram/_chatboost.py @@ -19,8 +19,7 @@ """This module contains the classes that represent Telegram ChatBoosts.""" import datetime as dtm -from collections.abc import Sequence -from typing import ClassVar, Final +from typing import ClassVar from telegram import constants from telegram._chat import Chat @@ -28,9 +27,10 @@ from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ChatBoostAdded(TelegramObject): """ This object represents a service message about a user boosting a chat. @@ -49,21 +49,10 @@ class ChatBoostAdded(TelegramObject): """ - __slots__ = ("boost_count",) - - def __init__( - self, - boost_count: int, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.boost_count: int = boost_count - self._id_attrs = (self.boost_count,) - - self._freeze() + boost_count: int = tg_field(compare=True) +@tg_dataclass() class ChatBoostSource(TelegramObject): """ Base class for Telegram ChatBoostSource objects. It can be one of: @@ -88,8 +77,6 @@ class ChatBoostSource(TelegramObject): or :attr:`~telegram.ChatBoostSource.GIVEAWAY`. """ - __slots__ = ("source",) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "source", { @@ -99,23 +86,21 @@ class ChatBoostSource(TelegramObject): }, ) - PREMIUM: Final[str] = constants.ChatBoostSources.PREMIUM + PREMIUM: ClassVar[str] = constants.ChatBoostSources.PREMIUM """:const:`telegram.constants.ChatBoostSources.PREMIUM`""" - GIFT_CODE: Final[str] = constants.ChatBoostSources.GIFT_CODE + GIFT_CODE: ClassVar[str] = constants.ChatBoostSources.GIFT_CODE """:const:`telegram.constants.ChatBoostSources.GIFT_CODE`""" - GIVEAWAY: Final[str] = constants.ChatBoostSources.GIVEAWAY + GIVEAWAY: ClassVar[str] = constants.ChatBoostSources.GIVEAWAY """:const:`telegram.constants.ChatBoostSources.GIVEAWAY`""" - def __init__(self, source: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) + @staticmethod + def _source_converter(value: str) -> str: + return enum.get_member(constants.ChatBoostSources, value, value) - # Required by all subclasses: - self.source: str = enum.get_member(constants.ChatBoostSources, source, source) - - self._id_attrs = (self.source,) - self._freeze() + source: str = tg_field(compare=True, default=None, converter=_source_converter) +@tg_dataclass() class ChatBoostSourcePremium(ChatBoostSource): """ The boost was obtained by subscribing to Telegram Premium or by gifting a Telegram Premium @@ -132,15 +117,13 @@ class ChatBoostSourcePremium(ChatBoostSource): user (:class:`telegram.User`): User that boosted the chat. """ - __slots__ = ("user",) - - def __init__(self, user: User, *, api_kwargs: JSONDict | None = None): - super().__init__(source=self.PREMIUM, api_kwargs=api_kwargs) + # Attribute only (init=False) + source: str = tg_field(init=False, default=ChatBoostSource.PREMIUM) - with self._unfrozen(): - self.user: User = user + user: User = tg_field() +@tg_dataclass() class ChatBoostSourceGiftCode(ChatBoostSource): """ The boost was obtained by the creation of Telegram Premium gift codes to boost a chat. Each @@ -158,15 +141,13 @@ class ChatBoostSourceGiftCode(ChatBoostSource): user (:class:`telegram.User`): User for which the gift code was created. """ - __slots__ = ("user",) + # Attribute only (init=False) + source: str = tg_field(init=False, default=ChatBoostSource.PREMIUM) - def __init__(self, user: User, *, api_kwargs: JSONDict | None = None): - super().__init__(source=self.GIFT_CODE, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.user: User = user + user: User = tg_field() +@tg_dataclass() class ChatBoostSourceGiveaway(ChatBoostSource): """ The boost was obtained by the creation of a Telegram Premium giveaway or a Telegram Star. @@ -202,26 +183,16 @@ class ChatBoostSourceGiveaway(ChatBoostSource): there was no user to win the prize. """ - __slots__ = ("giveaway_message_id", "is_unclaimed", "prize_star_count", "user") - - def __init__( - self, - giveaway_message_id: int, - user: User | None = None, - is_unclaimed: bool | None = None, - prize_star_count: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(source=self.GIVEAWAY, api_kwargs=api_kwargs) + # Attribute only (init=False) + source: str = tg_field(init=False, default=ChatBoostSource.GIVEAWAY) - with self._unfrozen(): - self.giveaway_message_id: int = giveaway_message_id - self.user: User | None = user - self.prize_star_count: int | None = prize_star_count - self.is_unclaimed: bool | None = is_unclaimed + giveaway_message_id: int = tg_field() + user: User | None = tg_field(default=None) + is_unclaimed: bool | None = tg_field(default=None) + prize_star_count: int | None = tg_field(default=None) +@tg_dataclass() class ChatBoost(TelegramObject): """ This object contains information about a chat boost. @@ -250,28 +221,13 @@ class ChatBoost(TelegramObject): source (:class:`telegram.ChatBoostSource`): Source of the added boost. """ - __slots__ = ("add_date", "boost_id", "expiration_date", "source") - - def __init__( - self, - boost_id: str, - add_date: dtm.datetime, - expiration_date: dtm.datetime, - source: ChatBoostSource, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.boost_id: str = boost_id - self.add_date: dtm.datetime = add_date - self.expiration_date: dtm.datetime = expiration_date - self.source: ChatBoostSource = source - - self._id_attrs = (self.boost_id, self.add_date, self.expiration_date, self.source) - self._freeze() + boost_id: str = tg_field(compare=True) + add_date: dtm.datetime = tg_field(compare=True) + expiration_date: dtm.datetime = tg_field(compare=True) + source: ChatBoostSource = tg_field(compare=True) +@tg_dataclass() class ChatBoostUpdated(TelegramObject): """This object represents a boost added to a chat or changed. @@ -289,24 +245,11 @@ class ChatBoostUpdated(TelegramObject): boost (:class:`telegram.ChatBoost`): Information about the chat boost. """ - __slots__ = ("boost", "chat") - - def __init__( - self, - chat: Chat, - boost: ChatBoost, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.chat: Chat = chat - self.boost: ChatBoost = boost - - self._id_attrs = (self.chat.id, self.boost) - self._freeze() + chat: Chat = tg_field(compare=True) + boost: ChatBoost = tg_field(compare=True) +@tg_dataclass() class ChatBoostRemoved(TelegramObject): """ This object represents a boost removed from a chat. @@ -329,28 +272,13 @@ class ChatBoostRemoved(TelegramObject): source (:class:`telegram.ChatBoostSource`): Source of the removed boost. """ - __slots__ = ("boost_id", "chat", "remove_date", "source") - - def __init__( - self, - chat: Chat, - boost_id: str, - remove_date: dtm.datetime, - source: ChatBoostSource, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.chat: Chat = chat - self.boost_id: str = boost_id - self.remove_date: dtm.datetime = remove_date - self.source: ChatBoostSource = source - - self._id_attrs = (self.chat, self.boost_id, self.remove_date, self.source) - self._freeze() + chat: Chat = tg_field(compare=True) + boost_id: str = tg_field(compare=True) + remove_date: dtm.datetime = tg_field(compare=True) + source: ChatBoostSource = tg_field(compare=True) +@tg_dataclass() class UserChatBoosts(TelegramObject): """This object represents a list of boosts added to a chat by a user. @@ -367,17 +295,4 @@ class UserChatBoosts(TelegramObject): boosts (tuple[:class:`telegram.ChatBoost`]): List of boosts added to the chat by the user. """ - __slots__ = ("boosts",) - - def __init__( - self, - boosts: Sequence[ChatBoost], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.boosts: tuple[ChatBoost, ...] = parse_sequence_arg(boosts) - - self._id_attrs = (self.boosts,) - self._freeze() + boosts: tuple[ChatBoost, ...] = tg_field(compare=True, converter=parse_sequence_arg) diff --git a/src/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py index 7eb9c4461ae..eb0a364f998 100644 --- a/src/telegram/_chatfullinfo.py +++ b/src/telegram/_chatfullinfo.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# pylint: disable=redefined-builtin # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2026 @@ -20,7 +19,6 @@ """This module contains an object that represents a Telegram ChatFullInfo.""" import datetime as dtm -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._birthdate import Birthdate @@ -37,15 +35,16 @@ parse_sequence_arg, to_timedelta, ) +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import ( get_timedelta_value, ) -from telegram._utils.types import JSONDict, TimePeriod if TYPE_CHECKING: from telegram import BusinessIntro, BusinessLocation, BusinessOpeningHours, Message +@tg_dataclass() class ChatFullInfo(_ChatBase): """ This object contains full information about a chat. @@ -443,172 +442,71 @@ class ChatFullInfo(_ChatBase): .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups """ - __slots__ = ( - "_message_auto_delete_time", - "_slow_mode_delay", - "accent_color_id", - "accepted_gift_types", - "active_usernames", - "available_reactions", - "background_custom_emoji_id", - "bio", - "birthdate", - "business_intro", - "business_location", - "business_opening_hours", - "can_send_paid_media", - "can_set_sticker_set", - "custom_emoji_sticker_set_name", - "description", - "emoji_status_custom_emoji_id", - "emoji_status_expiration_date", - "first_profile_audio", - "has_aggressive_anti_spam_enabled", - "has_hidden_members", - "has_private_forwards", - "has_protected_content", - "has_restricted_voice_and_video_messages", - "has_visible_history", - "invite_link", - "join_by_request", - "join_to_send_messages", - "linked_chat_id", - "location", - "max_reaction_count", - "paid_message_star_count", - "parent_chat", - "permissions", - "personal_chat", - "photo", - "pinned_message", - "profile_accent_color_id", - "profile_background_custom_emoji_id", - "rating", - "sticker_set_name", - "unique_gift_colors", - "unrestrict_boost_count", - ) + accent_color_id: int = tg_field(compare=True) + max_reaction_count: int = tg_field(compare=True) + accepted_gift_types: AcceptedGiftTypes = tg_field(compare=True) - def __init__( - self, - id: int, - type: str, - accent_color_id: int, - max_reaction_count: int, - accepted_gift_types: AcceptedGiftTypes, - title: str | None = None, - username: str | None = None, - first_name: str | None = None, - last_name: str | None = None, - is_forum: bool | None = None, - photo: ChatPhoto | None = None, - active_usernames: Sequence[str] | None = None, - birthdate: Birthdate | None = None, - business_intro: "BusinessIntro | None" = None, - business_location: "BusinessLocation | None" = None, - business_opening_hours: "BusinessOpeningHours | None" = None, - personal_chat: "Chat | None" = None, - available_reactions: Sequence[ReactionType] | None = None, - background_custom_emoji_id: str | None = None, - profile_accent_color_id: int | None = None, - profile_background_custom_emoji_id: str | None = None, - emoji_status_custom_emoji_id: str | None = None, - emoji_status_expiration_date: dtm.datetime | None = None, - bio: str | None = None, - has_private_forwards: bool | None = None, - has_restricted_voice_and_video_messages: bool | None = None, - join_to_send_messages: bool | None = None, - join_by_request: bool | None = None, - description: str | None = None, - invite_link: str | None = None, - pinned_message: "Message | None" = None, - permissions: ChatPermissions | None = None, - slow_mode_delay: TimePeriod | None = None, - unrestrict_boost_count: int | None = None, - message_auto_delete_time: TimePeriod | None = None, - has_aggressive_anti_spam_enabled: bool | None = None, - has_hidden_members: bool | None = None, - has_protected_content: bool | None = None, - has_visible_history: bool | None = None, - sticker_set_name: str | None = None, - can_set_sticker_set: bool | None = None, - custom_emoji_sticker_set_name: str | None = None, - linked_chat_id: int | None = None, - location: ChatLocation | None = None, - can_send_paid_media: bool | None = None, - is_direct_messages: bool | None = None, - parent_chat: Chat | None = None, - rating: UserRating | None = None, - unique_gift_colors: UniqueGiftColors | None = None, - paid_message_star_count: int | None = None, - first_profile_audio: Audio | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - id=id, - type=type, - title=title, - username=username, - first_name=first_name, - last_name=last_name, - is_forum=is_forum, - is_direct_messages=is_direct_messages, - api_kwargs=api_kwargs, - ) - # Required and unique to this class- - with self._unfrozen(): - self.max_reaction_count: int = max_reaction_count - self.photo: ChatPhoto | None = photo - self.bio: str | None = bio - self.has_private_forwards: bool | None = has_private_forwards - self.description: str | None = description - self.invite_link: str | None = invite_link - self.pinned_message: Message | None = pinned_message - self.permissions: ChatPermissions | None = permissions - self._slow_mode_delay: dtm.timedelta | None = to_timedelta(slow_mode_delay) - self._message_auto_delete_time: dtm.timedelta | None = to_timedelta( - message_auto_delete_time - ) - self.has_protected_content: bool | None = has_protected_content - self.has_visible_history: bool | None = has_visible_history - self.sticker_set_name: str | None = sticker_set_name - self.can_set_sticker_set: bool | None = can_set_sticker_set - self.linked_chat_id: int | None = linked_chat_id - self.location: ChatLocation | None = location - self.join_to_send_messages: bool | None = join_to_send_messages - self.join_by_request: bool | None = join_by_request - self.has_restricted_voice_and_video_messages: bool | None = ( - has_restricted_voice_and_video_messages - ) - self.active_usernames: tuple[str, ...] = parse_sequence_arg(active_usernames) - self.emoji_status_custom_emoji_id: str | None = emoji_status_custom_emoji_id - self.emoji_status_expiration_date: dtm.datetime | None = emoji_status_expiration_date - self.has_aggressive_anti_spam_enabled: bool | None = has_aggressive_anti_spam_enabled - self.has_hidden_members: bool | None = has_hidden_members - self.available_reactions: tuple[ReactionType, ...] | None = parse_sequence_arg( - available_reactions - ) - self.accent_color_id: int | None = accent_color_id - self.background_custom_emoji_id: str | None = background_custom_emoji_id - self.profile_accent_color_id: int | None = profile_accent_color_id - self.profile_background_custom_emoji_id: str | None = ( - profile_background_custom_emoji_id - ) - self.unrestrict_boost_count: int | None = unrestrict_boost_count - self.custom_emoji_sticker_set_name: str | None = custom_emoji_sticker_set_name - self.birthdate: Birthdate | None = birthdate - self.personal_chat: Chat | None = personal_chat - self.business_intro: BusinessIntro | None = business_intro - self.business_location: BusinessLocation | None = business_location - self.business_opening_hours: BusinessOpeningHours | None = business_opening_hours - self.can_send_paid_media: bool | None = can_send_paid_media - self.accepted_gift_types: AcceptedGiftTypes = accepted_gift_types - self.parent_chat: Chat | None = parent_chat - self.rating: UserRating | None = rating - self.unique_gift_colors: UniqueGiftColors | None = unique_gift_colors - self.paid_message_star_count: int | None = paid_message_star_count - self.first_profile_audio: Audio | None = first_profile_audio + title: str | None = tg_field(compare=True, default=None) + username: str | None = tg_field(compare=True, default=None) + first_name: str | None = tg_field(compare=True, default=None) + last_name: str | None = tg_field(compare=True, default=None) + is_forum: bool | None = tg_field(compare=True, default=None) + + photo: ChatPhoto | None = tg_field(compare=True, default=None) + active_usernames: tuple[str, ...] = tg_field( + compare=True, default=None, converter=parse_sequence_arg + ) + birthdate: Birthdate | None = tg_field(compare=True, default=None) + business_intro: "BusinessIntro | None" = tg_field(compare=True, default=None) + business_location: "BusinessLocation | None" = tg_field(compare=True, default=None) + business_opening_hours: "BusinessOpeningHours | None" = tg_field(compare=True, default=None) + personal_chat: "Chat | None" = tg_field(compare=True, default=None) + available_reactions: tuple[ReactionType, ...] = tg_field( + compare=True, default=None, converter=parse_sequence_arg + ) + background_custom_emoji_id: str | None = tg_field(compare=True, default=None) + profile_accent_color_id: int | None = tg_field(compare=True, default=None) + profile_background_custom_emoji_id: str | None = tg_field(compare=True, default=None) + emoji_status_custom_emoji_id: str | None = tg_field(compare=True, default=None) + emoji_status_expiration_date: dtm.datetime | None = tg_field(compare=True, default=None) + bio: str | None = tg_field(compare=True, default=None) + has_private_forwards: bool | None = tg_field(compare=True, default=None) + has_restricted_voice_and_video_messages: bool | None = tg_field(compare=True, default=None) + join_to_send_messages: bool | None = tg_field(compare=True, default=None) + join_by_request: bool | None = tg_field(compare=True, default=None) + description: str | None = tg_field(compare=True, default=None) + invite_link: str | None = tg_field(compare=True, default=None) + pinned_message: "Message | None" = tg_field(compare=True, default=None) + permissions: ChatPermissions | None = tg_field(compare=True, default=None) + _slow_mode_delay: dtm.timedelta | None = tg_field( + compare=True, + default=None, + alias="slow_mode_delay", + converter=to_timedelta, + ) + unrestrict_boost_count: int | None = tg_field(compare=True, default=None) + _message_auto_delete_time: dtm.timedelta | None = tg_field( + compare=True, + default=None, + alias="message_auto_delete_time", + converter=to_timedelta, + ) + has_aggressive_anti_spam_enabled: bool | None = tg_field(compare=True, default=None) + has_hidden_members: bool | None = tg_field(compare=True, default=None) + has_protected_content: bool | None = tg_field(compare=True, default=None) + has_visible_history: bool | None = tg_field(compare=True, default=None) + sticker_set_name: str | None = tg_field(compare=True, default=None) + can_set_sticker_set: bool | None = tg_field(compare=True, default=None) + custom_emoji_sticker_set_name: str | None = tg_field(compare=True, default=None) + linked_chat_id: int | None = tg_field(compare=True, default=None) + location: ChatLocation | None = tg_field(compare=True, default=None) + can_send_paid_media: bool | None = tg_field(compare=True, default=None) + is_direct_messages: bool | None = tg_field(compare=True, default=None) + parent_chat: Chat | None = tg_field(compare=True, default=None) + rating: UserRating | None = tg_field(compare=True, default=None) + unique_gift_colors: UniqueGiftColors | None = tg_field(compare=True, default=None) + paid_message_star_count: int | None = tg_field(compare=True, default=None) + first_profile_audio: Audio | None = tg_field(compare=True, default=None) @property def slow_mode_delay(self) -> int | dtm.timedelta | None: diff --git a/src/telegram/_chatinvitelink.py b/src/telegram/_chatinvitelink.py index f107f585904..a4fa8ad799c 100644 --- a/src/telegram/_chatinvitelink.py +++ b/src/telegram/_chatinvitelink.py @@ -23,10 +23,11 @@ from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod +@tg_dataclass() class ChatInviteLink(TelegramObject): """This object represents an invite link for a chat. @@ -122,63 +123,22 @@ class ChatInviteLink(TelegramObject): """ - __slots__ = ( - "_subscription_period", - "creates_join_request", - "creator", - "expire_date", - "invite_link", - "is_primary", - "is_revoked", - "member_limit", - "name", - "pending_join_request_count", - "subscription_price", + # Required + invite_link: str = tg_field(compare=True) + creator: User = tg_field(compare=True) + creates_join_request: bool = tg_field(compare=True) + is_primary: bool = tg_field(compare=True) + is_revoked: bool = tg_field(compare=True) + + # Optionals + expire_date: dtm.datetime | None = tg_field(default=None) + member_limit: int | None = tg_field(default=None) + name: str | None = tg_field(default=None) + pending_join_request_count: int | None = tg_field(default=None) + _subscription_period: dtm.timedelta | None = tg_field( + default=None, alias="subscription_period", converter=to_timedelta ) - - def __init__( - self, - invite_link: str, - creator: User, - creates_join_request: bool, - is_primary: bool, - is_revoked: bool, - expire_date: dtm.datetime | None = None, - member_limit: int | None = None, - name: str | None = None, - pending_join_request_count: int | None = None, - subscription_period: TimePeriod | None = None, - subscription_price: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.invite_link: str = invite_link - self.creator: User = creator - self.creates_join_request: bool = creates_join_request - self.is_primary: bool = is_primary - self.is_revoked: bool = is_revoked - - # Optionals - self.expire_date: dtm.datetime | None = expire_date - self.member_limit: int | None = member_limit - self.name: str | None = name - self.pending_join_request_count: int | None = ( - int(pending_join_request_count) if pending_join_request_count is not None else None - ) - self._subscription_period: dtm.timedelta | None = to_timedelta(subscription_period) - self.subscription_price: int | None = subscription_price - - self._id_attrs = ( - self.invite_link, - self.creates_join_request, - self.creator, - self.is_primary, - self.is_revoked, - ) - - self._freeze() + subscription_price: int | None = tg_field(default=None) @property def subscription_period(self) -> int | dtm.timedelta | None: diff --git a/src/telegram/_chatjoinrequest.py b/src/telegram/_chatjoinrequest.py index 16c72609205..b5ec697ed5c 100644 --- a/src/telegram/_chatjoinrequest.py +++ b/src/telegram/_chatjoinrequest.py @@ -24,10 +24,12 @@ from telegram._chatinvitelink import ChatInviteLink from telegram._telegramobject import TelegramObject from telegram._user import User +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput +@tg_dataclass() class ChatJoinRequest(TelegramObject): """This object represents a join request sent to a chat. @@ -96,33 +98,15 @@ class ChatJoinRequest(TelegramObject): """ - __slots__ = ("bio", "chat", "date", "from_user", "invite_link", "user_chat_id") + # Required + chat: Chat = tg_field(compare=True) + from_user: User = tg_field(compare=True) + date: dtm.datetime = tg_field(compare=True) + user_chat_id: int = tg_field() - def __init__( - self, - chat: Chat, - from_user: User, - date: dtm.datetime, - user_chat_id: int, - bio: str | None = None, - invite_link: ChatInviteLink | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.chat: Chat = chat - self.from_user: User = from_user - self.date: dtm.datetime = date - self.user_chat_id: int = user_chat_id - - # Optionals - self.bio: str | None = bio - self.invite_link: ChatInviteLink | None = invite_link - - self._id_attrs = (self.chat, self.from_user, self.date) - - self._freeze() + # Optionals + bio: str | None = tg_field(default=None) + invite_link: ChatInviteLink | None = tg_field(default=None) async def approve( self, diff --git a/src/telegram/_chatlocation.py b/src/telegram/_chatlocation.py index 75afd1db57b..39fef304ab6 100644 --- a/src/telegram/_chatlocation.py +++ b/src/telegram/_chatlocation.py @@ -18,14 +18,15 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a location to which a chat is connected.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._files.location import Location from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ChatLocation(TelegramObject): """This object represents a location to which a chat is connected. @@ -47,29 +48,15 @@ class ChatLocation(TelegramObject): """ - __slots__ = ("address", "location") + location: Location = tg_field(compare=True) + address: str = tg_field() - def __init__( - self, - location: Location, - address: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.location: Location = location - self.address: str = address - - self._id_attrs = (self.location,) - - self._freeze() - - MIN_ADDRESS: Final[int] = constants.LocationLimit.MIN_CHAT_LOCATION_ADDRESS + MIN_ADDRESS: ClassVar[int] = constants.LocationLimit.MIN_CHAT_LOCATION_ADDRESS """:const:`telegram.constants.LocationLimit.MIN_CHAT_LOCATION_ADDRESS` .. versionadded:: 20.0 """ - MAX_ADDRESS: Final[int] = constants.LocationLimit.MAX_CHAT_LOCATION_ADDRESS + MAX_ADDRESS: ClassVar[int] = constants.LocationLimit.MAX_CHAT_LOCATION_ADDRESS """:const:`telegram.constants.LocationLimit.MAX_CHAT_LOCATION_ADDRESS` .. versionadded:: 20.0 diff --git a/src/telegram/_chatmember.py b/src/telegram/_chatmember.py index 5fe338d53bb..9fbea7102ab 100644 --- a/src/telegram/_chatmember.py +++ b/src/telegram/_chatmember.py @@ -19,15 +19,16 @@ """This module contains an object that represents a Telegram ChatMember.""" import datetime as dtm -from typing import ClassVar, Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ChatMember(TelegramObject): """Base class for Telegram ChatMember Objects. Currently, the following 6 types of chat members are supported: @@ -70,8 +71,6 @@ class ChatMember(TelegramObject): """ - __slots__ = ("status", "user") - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "status", { @@ -84,36 +83,29 @@ class ChatMember(TelegramObject): }, ) - ADMINISTRATOR: Final[str] = constants.ChatMemberStatus.ADMINISTRATOR + ADMINISTRATOR: ClassVar[str] = constants.ChatMemberStatus.ADMINISTRATOR """:const:`telegram.constants.ChatMemberStatus.ADMINISTRATOR`""" - OWNER: Final[str] = constants.ChatMemberStatus.OWNER + OWNER: ClassVar[str] = constants.ChatMemberStatus.OWNER """:const:`telegram.constants.ChatMemberStatus.OWNER`""" - BANNED: Final[str] = constants.ChatMemberStatus.BANNED + BANNED: ClassVar[str] = constants.ChatMemberStatus.BANNED """:const:`telegram.constants.ChatMemberStatus.BANNED`""" - LEFT: Final[str] = constants.ChatMemberStatus.LEFT + LEFT: ClassVar[str] = constants.ChatMemberStatus.LEFT """:const:`telegram.constants.ChatMemberStatus.LEFT`""" - MEMBER: Final[str] = constants.ChatMemberStatus.MEMBER + MEMBER: ClassVar[str] = constants.ChatMemberStatus.MEMBER """:const:`telegram.constants.ChatMemberStatus.MEMBER`""" - RESTRICTED: Final[str] = constants.ChatMemberStatus.RESTRICTED + RESTRICTED: ClassVar[str] = constants.ChatMemberStatus.RESTRICTED """:const:`telegram.constants.ChatMemberStatus.RESTRICTED`""" - def __init__( - self, - user: User, - status: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required by all subclasses - self.user: User = user - self.status: str = enum.get_member(constants.ChatMemberStatus, status, status) - - self._id_attrs = (self.user, self.status) + @staticmethod + def _status_converter(value: str) -> str: + return enum.get_member(constants.BotCommandScopeType, value, value) - self._freeze() + # Required by all subclasses + user: User = tg_field(compare=True) + status: str = tg_field(compare=True, converter=_status_converter) +@tg_dataclass() class ChatMemberOwner(ChatMember): """ Represents a chat member that owns the chat @@ -137,22 +129,14 @@ class ChatMemberOwner(ChatMember): this user. """ - __slots__ = ("custom_title", "is_anonymous") + # Attribute only (init=False) + status: str = tg_field(init=False, default=ChatMember.OWNER) - def __init__( - self, - user: User, - is_anonymous: bool, - custom_title: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(status=ChatMember.OWNER, user=user, api_kwargs=api_kwargs) - with self._unfrozen(): - self.is_anonymous: bool = is_anonymous - self.custom_title: str | None = custom_title + is_anonymous: bool = tg_field() + custom_title: str | None = tg_field(default=None) +@tg_dataclass() class ChatMemberAdministrator(ChatMember): """ Represents a chat member that has some additional privileges. @@ -308,77 +292,33 @@ class ChatMemberAdministrator(ChatMember): .. versionadded:: 22.7 """ - __slots__ = ( - "can_be_edited", - "can_change_info", - "can_delete_messages", - "can_delete_stories", - "can_edit_messages", - "can_edit_stories", - "can_invite_users", - "can_manage_chat", - "can_manage_direct_messages", - "can_manage_tags", - "can_manage_topics", - "can_manage_video_chats", - "can_pin_messages", - "can_post_messages", - "can_post_stories", - "can_promote_members", - "can_restrict_members", - "custom_title", - "is_anonymous", - ) - - def __init__( - self, - user: User, - can_be_edited: bool, - is_anonymous: bool, - can_manage_chat: bool, - can_delete_messages: bool, - can_manage_video_chats: bool, - can_restrict_members: bool, - can_promote_members: bool, - can_change_info: bool, - can_invite_users: bool, - can_post_stories: bool, - can_edit_stories: bool, - can_delete_stories: bool, - can_post_messages: bool | None = None, - can_edit_messages: bool | None = None, - can_pin_messages: bool | None = None, - can_manage_topics: bool | None = None, - custom_title: str | None = None, - can_manage_direct_messages: bool | None = None, - can_manage_tags: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(status=ChatMember.ADMINISTRATOR, user=user, api_kwargs=api_kwargs) - with self._unfrozen(): - self.can_be_edited: bool = can_be_edited - self.is_anonymous: bool = is_anonymous - self.can_manage_chat: bool = can_manage_chat - self.can_delete_messages: bool = can_delete_messages - self.can_manage_video_chats: bool = can_manage_video_chats - self.can_restrict_members: bool = can_restrict_members - self.can_promote_members: bool = can_promote_members - self.can_change_info: bool = can_change_info - self.can_invite_users: bool = can_invite_users - self.can_post_stories: bool = can_post_stories - self.can_edit_stories: bool = can_edit_stories - self.can_delete_stories: bool = can_delete_stories - # Optionals - self.can_post_messages: bool | None = can_post_messages - self.can_edit_messages: bool | None = can_edit_messages - self.can_pin_messages: bool | None = can_pin_messages - self.can_manage_topics: bool | None = can_manage_topics - self.custom_title: str | None = custom_title - self.can_manage_direct_messages: bool | None = can_manage_direct_messages - self.can_manage_tags: bool | None = can_manage_tags - - + # Attribute only (init=False) + status: str = tg_field(init=False, default=ChatMember.ADMINISTRATOR) + + can_be_edited: bool = tg_field() + is_anonymous: bool = tg_field() + can_manage_chat: bool = tg_field() + can_delete_messages: bool = tg_field() + can_manage_video_chats: bool = tg_field() + can_restrict_members: bool = tg_field() + can_promote_members: bool = tg_field() + can_change_info: bool = tg_field() + can_invite_users: bool = tg_field() + can_post_stories: bool = tg_field() + can_edit_stories: bool = tg_field() + can_delete_stories: bool = tg_field() + + # Optionals + can_post_messages: bool | None = tg_field(default=None) + can_edit_messages: bool | None = tg_field(default=None) + can_pin_messages: bool | None = tg_field(default=None) + can_manage_topics: bool | None = tg_field(default=None) + custom_title: str | None = tg_field(default=None) + can_manage_direct_messages: bool | None = tg_field(default=None) + can_manage_tags: bool | None = tg_field(default=None) + + +@tg_dataclass() class ChatMemberMember(ChatMember): """ Represents a chat member that has no additional @@ -410,25 +350,14 @@ class ChatMemberMember(ChatMember): """ - __slots__ = ( - "tag", - "until_date", - ) + # Attribute only (init=False) + status: str = tg_field(init=False, default=ChatMember.MEMBER) - def __init__( - self, - user: User, - until_date: dtm.datetime | None = None, - tag: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(status=ChatMember.MEMBER, user=user, api_kwargs=api_kwargs) - with self._unfrozen(): - self.until_date: dtm.datetime | None = until_date - self.tag: str | None = tag + until_date: dtm.datetime | None = tg_field() + tag: str | None = tg_field() +@tg_dataclass() class ChatMemberRestricted(ChatMember): """ Represents a chat member that is under certain restrictions @@ -564,79 +493,33 @@ class ChatMemberRestricted(ChatMember): """ - __slots__ = ( - "can_add_web_page_previews", - "can_change_info", - "can_edit_tag", - "can_invite_users", - "can_manage_topics", - "can_pin_messages", - "can_react_to_messages", - "can_send_audios", - "can_send_documents", - "can_send_messages", - "can_send_other_messages", - "can_send_photos", - "can_send_polls", - "can_send_video_notes", - "can_send_videos", - "can_send_voice_notes", - "is_member", - "tag", - "until_date", - ) - __REMOVED_API_FIELDS__: ClassVar[frozenset[str]] = frozenset({"can_send_media_messages"}) - def __init__( - self, - user: User, - is_member: bool, - can_change_info: bool, - can_invite_users: bool, - can_pin_messages: bool, - can_send_messages: bool, - can_send_polls: bool, - can_send_other_messages: bool, - can_add_web_page_previews: bool, - can_manage_topics: bool, - until_date: dtm.datetime, - can_send_audios: bool, - can_send_documents: bool, - can_send_photos: bool, - can_send_videos: bool, - can_send_video_notes: bool, - can_send_voice_notes: bool, - can_edit_tag: bool, - can_react_to_messages: bool, - tag: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(status=ChatMember.RESTRICTED, user=user, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.is_member: bool = is_member - self.can_change_info: bool = can_change_info - self.can_invite_users: bool = can_invite_users - self.can_pin_messages: bool = can_pin_messages - self.can_send_messages: bool = can_send_messages - self.can_send_polls: bool = can_send_polls - self.can_send_other_messages: bool = can_send_other_messages - self.can_add_web_page_previews: bool = can_add_web_page_previews - self.can_manage_topics: bool = can_manage_topics - self.until_date: dtm.datetime = until_date - self.can_send_audios: bool = can_send_audios - self.can_send_documents: bool = can_send_documents - self.can_send_photos: bool = can_send_photos - self.can_send_videos: bool = can_send_videos - self.can_send_video_notes: bool = can_send_video_notes - self.can_send_voice_notes: bool = can_send_voice_notes - self.can_edit_tag: bool = can_edit_tag - self.can_react_to_messages: bool = can_react_to_messages - self.tag: str | None = tag - - + # Attribute only (init=False) + status: str = tg_field(init=False, default=ChatMember.RESTRICTED) + + is_member: bool = tg_field() + can_change_info: bool = tg_field() + can_invite_users: bool = tg_field() + can_pin_messages: bool = tg_field() + can_send_messages: bool = tg_field() + can_send_polls: bool = tg_field() + can_send_other_messages: bool = tg_field() + can_add_web_page_previews: bool = tg_field() + can_manage_topics: bool = tg_field() + until_date: dtm.datetime = tg_field() + can_send_audios: bool = tg_field() + can_send_documents: bool = tg_field() + can_send_photos: bool = tg_field() + can_send_videos: bool = tg_field() + can_send_video_notes: bool = tg_field() + can_send_voice_notes: bool = tg_field() + can_edit_tag: bool = tg_field() + can_react_to_messages: bool = tg_field() + tag: str | None = tg_field(default=None) + + +@tg_dataclass() class ChatMemberLeft(ChatMember): """ Represents a chat member that isn't currently a member of the chat, @@ -653,18 +536,11 @@ class ChatMemberLeft(ChatMember): user (:class:`telegram.User`): Information about the user. """ - __slots__ = () - - def __init__( - self, - user: User, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(status=ChatMember.LEFT, user=user, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + status: str = tg_field(init=False, default=ChatMember.LEFT) +@tg_dataclass() class ChatMemberBanned(ChatMember): """ Represents a chat member that was banned in the chat and @@ -692,15 +568,7 @@ class ChatMemberBanned(ChatMember): """ - __slots__ = ("until_date",) - - def __init__( - self, - user: User, - until_date: dtm.datetime, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(status=ChatMember.BANNED, user=user, api_kwargs=api_kwargs) - with self._unfrozen(): - self.until_date: dtm.datetime = until_date + # Attribute only (init=False) + status: str = tg_field(init=False, default=ChatMember.BANNED) + + until_date: dtm.datetime = tg_field() diff --git a/src/telegram/_chatmemberupdated.py b/src/telegram/_chatmemberupdated.py index 640292b9b8d..a33691592e5 100644 --- a/src/telegram/_chatmemberupdated.py +++ b/src/telegram/_chatmemberupdated.py @@ -25,9 +25,10 @@ from telegram._chatmember import ChatMember from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ChatMemberUpdated(TelegramObject): """This object represents changes in the status of a chat member. @@ -89,52 +90,17 @@ class ChatMemberUpdated(TelegramObject): """ - __slots__ = ( - "chat", - "date", - "from_user", - "invite_link", - "new_chat_member", - "old_chat_member", - "via_chat_folder_invite_link", - "via_join_request", - ) - - def __init__( - self, - chat: Chat, - from_user: User, - date: dtm.datetime, - old_chat_member: ChatMember, - new_chat_member: ChatMember, - invite_link: ChatInviteLink | None = None, - via_chat_folder_invite_link: bool | None = None, - via_join_request: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.chat: Chat = chat - self.from_user: User = from_user - self.date: dtm.datetime = date - self.old_chat_member: ChatMember = old_chat_member - self.new_chat_member: ChatMember = new_chat_member - self.via_chat_folder_invite_link: bool | None = via_chat_folder_invite_link - - # Optionals - self.invite_link: ChatInviteLink | None = invite_link - self.via_join_request: bool | None = via_join_request - - self._id_attrs = ( - self.chat, - self.from_user, - self.date, - self.old_chat_member, - self.new_chat_member, - ) - - self._freeze() + # Required + chat: Chat = tg_field(compare=True) + from_user: User = tg_field(compare=True) + date: dtm.datetime = tg_field(compare=True) + old_chat_member: ChatMember = tg_field(compare=True) + new_chat_member: ChatMember = tg_field(compare=True) + + # Optionals + invite_link: ChatInviteLink | None = tg_field(default=None) + via_chat_folder_invite_link: bool | None = tg_field(default=None) + via_join_request: bool | None = tg_field(default=None) def _get_attribute_difference(self, attribute: str) -> tuple[object, object]: try: diff --git a/src/telegram/_chatowner.py b/src/telegram/_chatowner.py index 80531752050..b0cd5025278 100644 --- a/src/telegram/_chatowner.py +++ b/src/telegram/_chatowner.py @@ -20,9 +20,10 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ChatOwnerChanged(TelegramObject): """This object represents a service message about an ownership change in the chat. @@ -39,22 +40,10 @@ class ChatOwnerChanged(TelegramObject): """ - __slots__ = ("new_owner",) - - def __init__( - self, - new_owner: User, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.new_owner: User = new_owner - - self._id_attrs = (self.new_owner,) - - self._freeze() + new_owner: User = tg_field(compare=True) +@tg_dataclass() class ChatOwnerLeft(TelegramObject): """This object represents a service message about the chat owner leaving the chat. @@ -73,17 +62,4 @@ class ChatOwnerLeft(TelegramObject): """ - __slots__ = ("new_owner",) - - def __init__( - self, - new_owner: User | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.new_owner: User | None = new_owner - - self._id_attrs = (self.new_owner,) - - self._freeze() + new_owner: User | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_chatpermissions.py b/src/telegram/_chatpermissions.py index ac8d810dba0..0d7ccb89475 100644 --- a/src/telegram/_chatpermissions.py +++ b/src/telegram/_chatpermissions.py @@ -18,10 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ChatPermission.""" +import dataclasses + from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ChatPermissions(TelegramObject): """Describes actions that a non-administrator user is allowed to take in a chat. @@ -154,91 +157,29 @@ class ChatPermissions(TelegramObject): """ - __slots__ = ( - "can_add_web_page_previews", - "can_change_info", - "can_edit_tag", - "can_invite_users", - "can_manage_topics", - "can_pin_messages", - "can_react_to_messages", - "can_send_audios", - "can_send_documents", - "can_send_messages", - "can_send_other_messages", - "can_send_photos", - "can_send_polls", - "can_send_video_notes", - "can_send_videos", - "can_send_voice_notes", - ) - __REMOVED_API_FIELDS__ = frozenset( { "can_send_media_messages", } ) - def __init__( - self, - can_send_messages: bool | None = None, - can_send_polls: bool | None = None, - can_send_other_messages: bool | None = None, - can_add_web_page_previews: bool | None = None, - can_change_info: bool | None = None, - can_invite_users: bool | None = None, - can_pin_messages: bool | None = None, - can_manage_topics: bool | None = None, - can_send_audios: bool | None = None, - can_send_documents: bool | None = None, - can_send_photos: bool | None = None, - can_send_videos: bool | None = None, - can_send_video_notes: bool | None = None, - can_send_voice_notes: bool | None = None, - can_edit_tag: bool | None = None, - can_react_to_messages: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.can_send_messages: bool | None = can_send_messages - self.can_send_polls: bool | None = can_send_polls - self.can_send_other_messages: bool | None = can_send_other_messages - self.can_add_web_page_previews: bool | None = can_add_web_page_previews - self.can_change_info: bool | None = can_change_info - self.can_invite_users: bool | None = can_invite_users - self.can_pin_messages: bool | None = can_pin_messages - self.can_manage_topics: bool | None = can_manage_topics - self.can_send_audios: bool | None = can_send_audios - self.can_send_documents: bool | None = can_send_documents - self.can_send_photos: bool | None = can_send_photos - self.can_send_videos: bool | None = can_send_videos - self.can_send_video_notes: bool | None = can_send_video_notes - self.can_send_voice_notes: bool | None = can_send_voice_notes - self.can_edit_tag: bool | None = can_edit_tag - self.can_react_to_messages: bool | None = can_react_to_messages - - self._id_attrs = ( - self.can_send_messages, - self.can_send_polls, - self.can_send_other_messages, - self.can_add_web_page_previews, - self.can_change_info, - self.can_invite_users, - self.can_pin_messages, - self.can_manage_topics, - self.can_send_audios, - self.can_send_documents, - self.can_send_photos, - self.can_send_videos, - self.can_send_video_notes, - self.can_send_voice_notes, - self.can_edit_tag, - self.can_react_to_messages, - ) - - self._freeze() + # Optional + can_send_messages: bool | None = tg_field(compare=True, default=None) + can_send_polls: bool | None = tg_field(compare=True, default=None) + can_send_other_messages: bool | None = tg_field(compare=True, default=None) + can_add_web_page_previews: bool | None = tg_field(compare=True, default=None) + can_change_info: bool | None = tg_field(compare=True, default=None) + can_invite_users: bool | None = tg_field(compare=True, default=None) + can_pin_messages: bool | None = tg_field(compare=True, default=None) + can_manage_topics: bool | None = tg_field(compare=True, default=None) + can_send_audios: bool | None = tg_field(compare=True, default=None) + can_send_documents: bool | None = tg_field(compare=True, default=None) + can_send_photos: bool | None = tg_field(compare=True, default=None) + can_send_videos: bool | None = tg_field(compare=True, default=None) + can_send_video_notes: bool | None = tg_field(compare=True, default=None) + can_send_voice_notes: bool | None = tg_field(compare=True, default=None) + can_edit_tag: bool | None = tg_field(compare=True, default=None) + can_react_to_messages: bool | None = tg_field(compare=True, default=None) @classmethod def all_permissions(cls) -> "ChatPermissions": @@ -250,7 +191,9 @@ def all_permissions(cls) -> "ChatPermissions": .. versionadded:: 20.0 """ - return cls(*(True,) * len(cls.__slots__)) + return cls( + *(True for field in dataclasses.fields(cls) if field.init and not field.kw_only) + ) @classmethod def no_permissions(cls) -> "ChatPermissions": @@ -260,4 +203,6 @@ def no_permissions(cls) -> "ChatPermissions": .. versionadded:: 20.0 """ - return cls(*(False,) * len(cls.__slots__)) + return cls( + *(False for field in dataclasses.fields(cls) if field.init and not field.kw_only) + ) diff --git a/src/telegram/_checklists.py b/src/telegram/_checklists.py index a4b94022022..d2ff7cdb60e 100644 --- a/src/telegram/_checklists.py +++ b/src/telegram/_checklists.py @@ -19,7 +19,6 @@ """This module contains an objects related to Telegram checklists.""" import datetime as dtm -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._chat import Chat @@ -27,13 +26,14 @@ from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.entities import parse_message_entities, parse_message_entity -from telegram._utils.types import JSONDict if TYPE_CHECKING: from telegram import Message +@tg_dataclass() class ChecklistTask(TelegramObject): """ Describes a task in a checklist. @@ -78,37 +78,12 @@ class ChecklistTask(TelegramObject): |datetime_localization| """ - __slots__ = ( - "completed_by_chat", - "completed_by_user", - "completion_date", - "id", - "text", - "text_entities", - ) - - def __init__( - self, - id: int, # pylint: disable=redefined-builtin - text: str, - text_entities: Sequence[MessageEntity] | None = None, - completed_by_user: User | None = None, - completion_date: dtm.datetime | None = None, - completed_by_chat: Chat | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.id: int = id - self.text: str = text - self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) - self.completed_by_user: User | None = completed_by_user - self.completed_by_chat: Chat | None = completed_by_chat - self.completion_date: dtm.datetime | None = completion_date - - self._id_attrs = (self.id,) - - self._freeze() + id: int = tg_field(compare=True) + text: str = tg_field() + text_entities: tuple[MessageEntity, ...] = tg_field(default=None, converter=parse_sequence_arg) + completed_by_user: User | None = tg_field(default=None) + completion_date: dtm.datetime | None = tg_field(default=None) + completed_by_chat: Chat | None = tg_field(default=None) def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`text` @@ -151,6 +126,7 @@ def parse_entities(self, types: list[str] | None = None) -> dict[MessageEntity, return parse_message_entities(self.text, self.text_entities, types) +@tg_dataclass() class Checklist(TelegramObject): """ Describes a checklist. @@ -181,34 +157,13 @@ class Checklist(TelegramObject): creator of the list can mark tasks as done or not done """ - __slots__ = ( - "others_can_add_tasks", - "others_can_mark_tasks_as_done", - "tasks", - "title", - "title_entities", + title: str = tg_field() + tasks: tuple[ChecklistTask, ...] = tg_field(compare=True, converter=parse_sequence_arg) + title_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - title: str, - tasks: Sequence[ChecklistTask], - title_entities: Sequence[MessageEntity] | None = None, - others_can_add_tasks: bool | None = None, - others_can_mark_tasks_as_done: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.title: str = title - self.title_entities: tuple[MessageEntity, ...] = parse_sequence_arg(title_entities) - self.tasks: tuple[ChecklistTask, ...] = parse_sequence_arg(tasks) - self.others_can_add_tasks: bool | None = others_can_add_tasks - self.others_can_mark_tasks_as_done: bool | None = others_can_mark_tasks_as_done - - self._id_attrs = (self.tasks,) - - self._freeze() + others_can_add_tasks: bool | None = tg_field(default=None) + others_can_mark_tasks_as_done: bool | None = tg_field(default=None) def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`title` @@ -251,6 +206,7 @@ def parse_entities(self, types: list[str] | None = None) -> dict[MessageEntity, return parse_message_entities(self.title, self.title_entities, types) +@tg_dataclass() class ChecklistTasksDone(TelegramObject): """ Describes a service message about checklist tasks marked as done or not done. @@ -282,32 +238,16 @@ class ChecklistTasksDone(TelegramObject): were marked as not done """ - __slots__ = ( - "checklist_message", - "marked_as_done_task_ids", - "marked_as_not_done_task_ids", + checklist_message: "Message | None" = tg_field(default=None) + marked_as_done_task_ids: tuple[int, ...] = tg_field( + compare=True, default=None, converter=parse_sequence_arg + ) + marked_as_not_done_task_ids: tuple[int, ...] = tg_field( + compare=True, default=None, converter=parse_sequence_arg ) - - def __init__( - self, - checklist_message: "Message | None" = None, - marked_as_done_task_ids: Sequence[int] | None = None, - marked_as_not_done_task_ids: Sequence[int] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.checklist_message: Message | None = checklist_message - self.marked_as_done_task_ids: tuple[int, ...] = parse_sequence_arg(marked_as_done_task_ids) - self.marked_as_not_done_task_ids: tuple[int, ...] = parse_sequence_arg( - marked_as_not_done_task_ids - ) - - self._id_attrs = (self.marked_as_done_task_ids, self.marked_as_not_done_task_ids) - - self._freeze() +@tg_dataclass() class ChecklistTasksAdded(TelegramObject): """ Describes a service message about tasks added to a checklist. @@ -332,19 +272,5 @@ class ChecklistTasksAdded(TelegramObject): tasks (Tuple[:class:`telegram.ChecklistTask`]): List of tasks added to the checklist """ - __slots__ = ("checklist_message", "tasks") - - def __init__( - self, - tasks: Sequence[ChecklistTask], - checklist_message: "Message | None" = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.checklist_message: Message | None = checklist_message - self.tasks: tuple[ChecklistTask, ...] = parse_sequence_arg(tasks) - - self._id_attrs = (self.tasks,) - - self._freeze() + tasks: tuple[ChecklistTask, ...] = tg_field(compare=True, converter=parse_sequence_arg) + checklist_message: "Message | None" = tg_field(default=None) diff --git a/src/telegram/_choseninlineresult.py b/src/telegram/_choseninlineresult.py index 8f8b733cf81..81d00f4988c 100644 --- a/src/telegram/_choseninlineresult.py +++ b/src/telegram/_choseninlineresult.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# pylint: disable=too-many-arguments # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2026 @@ -23,12 +22,13 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field if TYPE_CHECKING: from telegram._files.location import Location +@tg_dataclass() class ChosenInlineResult(TelegramObject): """ Represents a result of an inline query that was chosen by the user and sent to their chat @@ -64,28 +64,10 @@ class ChosenInlineResult(TelegramObject): """ - __slots__ = ("from_user", "inline_message_id", "location", "query", "result_id") - - def __init__( - self, - result_id: str, - from_user: User, - query: str, - location: "Location | None" = None, - inline_message_id: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Required - self.result_id: str = result_id - self.from_user: User = from_user - self.query: str = query - # Optionals - self.location: Location | None = location - self.inline_message_id: str | None = inline_message_id - - self._id_attrs = (self.result_id,) - - self._freeze() + # Required + result_id: str = tg_field(compare=True) + from_user: User = tg_field() + query: str = tg_field() + # Optionals + location: "Location | None" = tg_field(default=None) + inline_message_id: str | None = tg_field(default=None) diff --git a/src/telegram/_copytextbutton.py b/src/telegram/_copytextbutton.py index 0bd5939081a..7f1957b129b 100644 --- a/src/telegram/_copytextbutton.py +++ b/src/telegram/_copytextbutton.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram CopyTextButton.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class CopyTextButton(TelegramObject): """ This object represents an inline keyboard button that copies specified text to the clipboard. @@ -43,12 +44,4 @@ class CopyTextButton(TelegramObject): """ - __slots__ = ("text",) - - def __init__(self, text: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.text: str = text - - self._id_attrs = (self.text,) - - self._freeze() + text: str = tg_field(compare=True) diff --git a/src/telegram/_dice.py b/src/telegram/_dice.py index 4f8893e6745..f5a4ee6bf87 100644 --- a/src/telegram/_dice.py +++ b/src/telegram/_dice.py @@ -18,13 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Dice.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class Dice(TelegramObject): """ This object represents an animated emoji with a random value for currently supported base @@ -88,73 +89,65 @@ class Dice(TelegramObject): """ - __slots__ = ("emoji", "value") + value: int = tg_field(compare=True) + emoji: str = tg_field(compare=True) - def __init__(self, value: int, emoji: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.value: int = value - self.emoji: str = emoji - - self._id_attrs = (self.value, self.emoji) - - self._freeze() - - DICE: Final[str] = constants.DiceEmoji.DICE + DICE: ClassVar[str] = constants.DiceEmoji.DICE """:const:`telegram.constants.DiceEmoji.DICE`""" - DARTS: Final[str] = constants.DiceEmoji.DARTS + DARTS: ClassVar[str] = constants.DiceEmoji.DARTS """:const:`telegram.constants.DiceEmoji.DARTS`""" - BASKETBALL: Final[str] = constants.DiceEmoji.BASKETBALL + BASKETBALL: ClassVar[str] = constants.DiceEmoji.BASKETBALL """:const:`telegram.constants.DiceEmoji.BASKETBALL`""" - FOOTBALL: Final[str] = constants.DiceEmoji.FOOTBALL + FOOTBALL: ClassVar[str] = constants.DiceEmoji.FOOTBALL """:const:`telegram.constants.DiceEmoji.FOOTBALL`""" - SLOT_MACHINE: Final[str] = constants.DiceEmoji.SLOT_MACHINE + SLOT_MACHINE: ClassVar[str] = constants.DiceEmoji.SLOT_MACHINE """:const:`telegram.constants.DiceEmoji.SLOT_MACHINE`""" - BOWLING: Final[str] = constants.DiceEmoji.BOWLING + BOWLING: ClassVar[str] = constants.DiceEmoji.BOWLING """ :const:`telegram.constants.DiceEmoji.BOWLING` .. versionadded:: 13.4 """ - ALL_EMOJI: Final[list[str]] = list(constants.DiceEmoji) + ALL_EMOJI: ClassVar[list[str]] = list(constants.DiceEmoji) """list[:obj:`str`]: A list of all available dice emoji.""" - MIN_VALUE: Final[int] = constants.DiceLimit.MIN_VALUE + MIN_VALUE: ClassVar[int] = constants.DiceLimit.MIN_VALUE """:const:`telegram.constants.DiceLimit.MIN_VALUE` .. versionadded:: 20.0 """ - MAX_VALUE_BOWLING: Final[int] = constants.DiceLimit.MAX_VALUE_BOWLING + MAX_VALUE_BOWLING: ClassVar[int] = constants.DiceLimit.MAX_VALUE_BOWLING """:const:`telegram.constants.DiceLimit.MAX_VALUE_BOWLING` .. versionadded:: 20.0 """ - MAX_VALUE_DARTS: Final[int] = constants.DiceLimit.MAX_VALUE_DARTS + MAX_VALUE_DARTS: ClassVar[int] = constants.DiceLimit.MAX_VALUE_DARTS """:const:`telegram.constants.DiceLimit.MAX_VALUE_DARTS` .. versionadded:: 20.0 """ - MAX_VALUE_DICE: Final[int] = constants.DiceLimit.MAX_VALUE_DICE + MAX_VALUE_DICE: ClassVar[int] = constants.DiceLimit.MAX_VALUE_DICE """:const:`telegram.constants.DiceLimit.MAX_VALUE_DICE` .. versionadded:: 20.0 """ - MAX_VALUE_BASKETBALL: Final[int] = constants.DiceLimit.MAX_VALUE_BASKETBALL + MAX_VALUE_BASKETBALL: ClassVar[int] = constants.DiceLimit.MAX_VALUE_BASKETBALL """:const:`telegram.constants.DiceLimit.MAX_VALUE_BASKETBALL` .. versionadded:: 20.0 """ - MAX_VALUE_FOOTBALL: Final[int] = constants.DiceLimit.MAX_VALUE_FOOTBALL + MAX_VALUE_FOOTBALL: ClassVar[int] = constants.DiceLimit.MAX_VALUE_FOOTBALL """:const:`telegram.constants.DiceLimit.MAX_VALUE_FOOTBALL` .. versionadded:: 20.0 """ - MAX_VALUE_SLOT_MACHINE: Final[int] = constants.DiceLimit.MAX_VALUE_SLOT_MACHINE + MAX_VALUE_SLOT_MACHINE: ClassVar[int] = constants.DiceLimit.MAX_VALUE_SLOT_MACHINE """:const:`telegram.constants.DiceLimit.MAX_VALUE_SLOT_MACHINE` .. versionadded:: 20.0 diff --git a/src/telegram/_directmessagepricechanged.py b/src/telegram/_directmessagepricechanged.py index 0e9f750d446..f7d32873045 100644 --- a/src/telegram/_directmessagepricechanged.py +++ b/src/telegram/_directmessagepricechanged.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Direct Message Price.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class DirectMessagePriceChanged(TelegramObject): """ Describes a service message about a change in the price of direct messages sent to a channel @@ -52,19 +53,5 @@ class DirectMessagePriceChanged(TelegramObject): administrators. Defaults to ``0``. """ - __slots__ = ("are_direct_messages_enabled", "direct_message_star_count") - - def __init__( - self, - are_direct_messages_enabled: bool, - direct_message_star_count: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.are_direct_messages_enabled: bool = are_direct_messages_enabled - self.direct_message_star_count: int | None = direct_message_star_count - - self._id_attrs = (self.are_direct_messages_enabled, self.direct_message_star_count) - - self._freeze() + are_direct_messages_enabled: bool = tg_field(compare=True) + direct_message_star_count: int | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_directmessagestopic.py b/src/telegram/_directmessagestopic.py index 4aeee4f2dda..6c2065eb797 100644 --- a/src/telegram/_directmessagestopic.py +++ b/src/telegram/_directmessagestopic.py @@ -20,9 +20,10 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class DirectMessagesTopic(TelegramObject): """ This class represents a topic for direct messages in a chat. @@ -54,18 +55,8 @@ class DirectMessagesTopic(TelegramObject): """ - __slots__ = ("topic_id", "user") + # Required: + topic_id: int = tg_field(compare=True) - def __init__( - self, topic_id: int, user: User | None = None, *, api_kwargs: JSONDict | None = None - ): - super().__init__(api_kwargs=api_kwargs) - - # Required: - self.topic_id: int = topic_id - - # Optionals: - self.user: User | None = user - - self._id_attrs = (self.topic_id, self.user) - self._freeze() + # Optionals: + user: User | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_forcereply.py b/src/telegram/_forcereply.py index 228aef19aa5..14956dabbb0 100644 --- a/src/telegram/_forcereply.py +++ b/src/telegram/_forcereply.py @@ -18,13 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ForceReply.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ForceReply(TelegramObject): """ Upon receiving a message with this object, Telegram clients will display a reply interface to @@ -76,30 +77,18 @@ class ForceReply(TelegramObject): """ - __slots__ = ("force_reply", "input_field_placeholder", "selective") + # Attribute only (init=False) + force_reply: bool = tg_field(init=False, default=True) - def __init__( - self, - selective: bool | None = None, - input_field_placeholder: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.force_reply: bool = True - self.selective: bool | None = selective - self.input_field_placeholder: str | None = input_field_placeholder + selective: bool | None = tg_field(compare=True) + input_field_placeholder: str | None = tg_field(default=None) - self._id_attrs = (self.selective,) - - self._freeze() - - MIN_INPUT_FIELD_PLACEHOLDER: Final[int] = constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER + MIN_INPUT_FIELD_PLACEHOLDER: ClassVar[int] = constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER """:const:`telegram.constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER` .. versionadded:: 20.0 """ - MAX_INPUT_FIELD_PLACEHOLDER: Final[int] = constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER + MAX_INPUT_FIELD_PLACEHOLDER: ClassVar[int] = constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER """:const:`telegram.constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER` .. versionadded:: 20.0 diff --git a/src/telegram/_forumtopic.py b/src/telegram/_forumtopic.py index 51c6d8df71f..af35f06382b 100644 --- a/src/telegram/_forumtopic.py +++ b/src/telegram/_forumtopic.py @@ -19,9 +19,10 @@ """This module contains objects related to Telegram forum topics.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ForumTopic(TelegramObject): """ This object represents a forum topic. @@ -55,36 +56,14 @@ class ForumTopic(TelegramObject): .. versionadded:: 22.6 """ - __slots__ = ( - "icon_color", - "icon_custom_emoji_id", - "is_name_implicit", - "message_thread_id", - "name", - ) - - def __init__( - self, - message_thread_id: int, - name: str, - icon_color: int, - icon_custom_emoji_id: str | None = None, - is_name_implicit: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.message_thread_id: int = message_thread_id - self.name: str = name - self.icon_color: int = icon_color - self.icon_custom_emoji_id: str | None = icon_custom_emoji_id - self.is_name_implicit: bool | None = is_name_implicit - - self._id_attrs = (self.message_thread_id, self.name, self.icon_color) - - self._freeze() + message_thread_id: int = tg_field(compare=True) + name: str = tg_field(compare=True) + icon_color: int = tg_field(compare=True) + icon_custom_emoji_id: str | None = tg_field(default=None) + is_name_implicit: bool | None = tg_field(default=None) +@tg_dataclass() class ForumTopicCreated(TelegramObject): """ This object represents the content of a service message about a new forum topic created in @@ -116,28 +95,13 @@ class ForumTopicCreated(TelegramObject): .. versionadded:: 22.6 """ - __slots__ = ("icon_color", "icon_custom_emoji_id", "is_name_implicit", "name") - - def __init__( - self, - name: str, - icon_color: int, - icon_custom_emoji_id: str | None = None, - is_name_implicit: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.name: str = name - self.icon_color: int = icon_color - self.icon_custom_emoji_id: str | None = icon_custom_emoji_id - self.is_name_implicit: bool | None = is_name_implicit - - self._id_attrs = (self.name, self.icon_color) - - self._freeze() + name: str = tg_field(compare=True) + icon_color: int = tg_field(compare=True) + icon_custom_emoji_id: str | None = tg_field(default=None) + is_name_implicit: bool | None = tg_field(default=None) +@tg_dataclass() class ForumTopicClosed(TelegramObject): """ This object represents a service message about a forum topic closed in the chat. @@ -146,14 +110,8 @@ class ForumTopicClosed(TelegramObject): .. versionadded:: 20.0 """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(api_kwargs=api_kwargs) - - self._freeze() - +@tg_dataclass() class ForumTopicReopened(TelegramObject): """ This object represents a service message about a forum topic reopened in the chat. @@ -162,14 +120,8 @@ class ForumTopicReopened(TelegramObject): .. versionadded:: 20.0 """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(api_kwargs=api_kwargs) - - self._freeze() - +@tg_dataclass() class ForumTopicEdited(TelegramObject): """ This object represents a service message about an edited forum topic. @@ -190,24 +142,11 @@ class ForumTopicEdited(TelegramObject): the topic icon, if it was edited; an empty string if the icon was removed. """ - __slots__ = ("icon_custom_emoji_id", "name") - - def __init__( - self, - name: str | None = None, - icon_custom_emoji_id: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.name: str | None = name - self.icon_custom_emoji_id: str | None = icon_custom_emoji_id - - self._id_attrs = (self.name, self.icon_custom_emoji_id) - - self._freeze() + name: str | None = tg_field(compare=True, default=None) + icon_custom_emoji_id: str | None = tg_field(compare=True, default=None) +@tg_dataclass() class GeneralForumTopicHidden(TelegramObject): """ This object represents a service message about General forum topic hidden in the chat. @@ -216,14 +155,8 @@ class GeneralForumTopicHidden(TelegramObject): .. versionadded:: 20.0 """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - - self._freeze() - +@tg_dataclass() class GeneralForumTopicUnhidden(TelegramObject): """ This object represents a service message about General forum topic unhidden in the chat. @@ -231,10 +164,3 @@ class GeneralForumTopicUnhidden(TelegramObject): .. versionadded:: 20.0 """ - - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - - self._freeze() diff --git a/src/telegram/_gifts.py b/src/telegram/_gifts.py index ad843b336b6..7f41ab6f9b1 100644 --- a/src/telegram/_gifts.py +++ b/src/telegram/_gifts.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# pylint: disable=redefined-builtin # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2026 @@ -19,17 +18,16 @@ # along with this program. If not, see [http://www.gnu.org/licenses/] """This module contains classes related to gifs sent by bots.""" -from collections.abc import Sequence - from telegram._chat import Chat from telegram._files.sticker import Sticker from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.entities import parse_message_entities, parse_message_entity -from telegram._utils.types import JSONDict +@tg_dataclass() class GiftBackground(TelegramObject): """This object describes the background of a gift. @@ -51,34 +49,12 @@ class GiftBackground(TelegramObject): """ - __slots__ = ( - "center_color", - "edge_color", - "text_color", - ) - - def __init__( - self, - center_color: int, - edge_color: int, - text_color: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.center_color: int = center_color - self.edge_color: int = edge_color - self.text_color: int = text_color - - self._id_attrs = ( - self.center_color, - self.edge_color, - self.text_color, - ) - - self._freeze() + center_color: int = tg_field(compare=True) + edge_color: int = tg_field(compare=True) + text_color: int = tg_field(compare=True) +@tg_dataclass() class Gift(TelegramObject): """This object represents a gift that can be sent by the bot. @@ -171,60 +147,22 @@ class Gift(TelegramObject): """ - __slots__ = ( - "background", - "has_colors", - "id", - "is_premium", - "personal_remaining_count", - "personal_total_count", - "publisher_chat", - "remaining_count", - "star_count", - "sticker", - "total_count", - "unique_gift_variant_count", - "upgrade_star_count", - ) - - def __init__( - self, - id: str, - sticker: Sticker, - star_count: int, - total_count: int | None = None, - remaining_count: int | None = None, - upgrade_star_count: int | None = None, - publisher_chat: Chat | None = None, - personal_total_count: int | None = None, - personal_remaining_count: int | None = None, - background: GiftBackground | None = None, - is_premium: bool | None = None, - has_colors: bool | None = None, - unique_gift_variant_count: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.id: str = id - self.sticker: Sticker = sticker - self.star_count: int = star_count - self.total_count: int | None = total_count - self.remaining_count: int | None = remaining_count - self.upgrade_star_count: int | None = upgrade_star_count - self.publisher_chat: Chat | None = publisher_chat - self.personal_total_count: int | None = personal_total_count - self.personal_remaining_count: int | None = personal_remaining_count - self.background: GiftBackground | None = background - self.is_premium: bool | None = is_premium - self.has_colors: bool | None = has_colors - self.unique_gift_variant_count: int | None = unique_gift_variant_count - - self._id_attrs = (self.id,) - - self._freeze() - - + id: str = tg_field(compare=True) + sticker: Sticker = tg_field() + star_count: int = tg_field() + total_count: int | None = tg_field(default=None) + remaining_count: int | None = tg_field(default=None) + upgrade_star_count: int | None = tg_field(default=None) + publisher_chat: Chat | None = tg_field(default=None) + personal_total_count: int | None = tg_field(default=None) + personal_remaining_count: int | None = tg_field(default=None) + background: GiftBackground | None = tg_field(default=None) + is_premium: bool | None = tg_field(default=None) + has_colors: bool | None = tg_field(default=None) + unique_gift_variant_count: int | None = tg_field(default=None) + + +@tg_dataclass() class Gifts(TelegramObject): """This object represent a list of gifts. @@ -241,22 +179,10 @@ class Gifts(TelegramObject): """ - __slots__ = ("gifts",) - - def __init__( - self, - gifts: Sequence[Gift], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.gifts: tuple[Gift, ...] = parse_sequence_arg(gifts) - - self._id_attrs = (self.gifts,) - - self._freeze() + gifts: tuple[Gift, ...] = tg_field(compare=True, converter=parse_sequence_arg) +@tg_dataclass() class GiftInfo(TelegramObject): """Describes a service message about a regular gift that was sent or received. @@ -317,51 +243,16 @@ class GiftInfo(TelegramObject): """ - __slots__ = ( - "can_be_upgraded", - "convert_star_count", - "entities", - "gift", - "is_private", - "is_upgrade_separate", - "owned_gift_id", - "prepaid_upgrade_star_count", - "text", - "unique_gift_number", - ) - - def __init__( - self, - gift: Gift, - owned_gift_id: str | None = None, - convert_star_count: int | None = None, - prepaid_upgrade_star_count: int | None = None, - can_be_upgraded: bool | None = None, - text: str | None = None, - entities: Sequence[MessageEntity] | None = None, - is_private: bool | None = None, - unique_gift_number: int | None = None, - is_upgrade_separate: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.gift: Gift = gift - # Optional - self.owned_gift_id: str | None = owned_gift_id - self.convert_star_count: int | None = convert_star_count - self.prepaid_upgrade_star_count: int | None = prepaid_upgrade_star_count - self.can_be_upgraded: bool | None = can_be_upgraded - self.text: str | None = text - self.entities: tuple[MessageEntity, ...] = parse_sequence_arg(entities) - self.is_private: bool | None = is_private - self.unique_gift_number: int | None = unique_gift_number - self.is_upgrade_separate: bool | None = is_upgrade_separate - - self._id_attrs = (self.gift,) - - self._freeze() + gift: Gift = tg_field(compare=True) + owned_gift_id: str | None = tg_field(default=None) + convert_star_count: int | None = tg_field(default=None) + prepaid_upgrade_star_count: int | None = tg_field(default=None) + can_be_upgraded: bool | None = tg_field(default=None) + text: str | None = tg_field(default=None) + entities: tuple[MessageEntity, ...] = tg_field(default=None, converter=parse_sequence_arg) + is_private: bool | None = tg_field(default=None) + unique_gift_number: int | None = tg_field(default=None) + is_upgrade_separate: bool | None = tg_field(default=None) def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`text` @@ -418,6 +309,7 @@ def parse_entities(self, types: list[str] | None = None) -> dict[MessageEntity, return parse_message_entities(self.text, self.entities, types) +@tg_dataclass() class AcceptedGiftTypes(TelegramObject): """This object describes the types of gifts that can be gifted to a user or a chat. @@ -455,37 +347,8 @@ class AcceptedGiftTypes(TelegramObject): """ - __slots__ = ( - "gifts_from_channels", - "limited_gifts", - "premium_subscription", - "unique_gifts", - "unlimited_gifts", - ) - - def __init__( - self, - unlimited_gifts: bool, - limited_gifts: bool, - unique_gifts: bool, - premium_subscription: bool, - gifts_from_channels: bool, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.unlimited_gifts: bool = unlimited_gifts - self.limited_gifts: bool = limited_gifts - self.unique_gifts: bool = unique_gifts - self.premium_subscription: bool = premium_subscription - self.gifts_from_channels: bool = gifts_from_channels - - self._id_attrs = ( - self.unlimited_gifts, - self.limited_gifts, - self.unique_gifts, - self.premium_subscription, - self.gifts_from_channels, - ) - - self._freeze() + unlimited_gifts: bool = tg_field(compare=True) + limited_gifts: bool = tg_field(compare=True) + unique_gifts: bool = tg_field(compare=True) + premium_subscription: bool = tg_field(compare=True) + gifts_from_channels: bool = tg_field(compare=True) diff --git a/src/telegram/_giveaway.py b/src/telegram/_giveaway.py index d44a4eb04c3..eacd42417c4 100644 --- a/src/telegram/_giveaway.py +++ b/src/telegram/_giveaway.py @@ -19,19 +19,19 @@ """This module contains an objects that are related to Telegram giveaways.""" import datetime as dtm -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._chat import Chat from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field if TYPE_CHECKING: from telegram import Message +@tg_dataclass() class Giveaway(TelegramObject): """This object represents a message about a scheduled giveaway. @@ -90,53 +90,18 @@ class Giveaway(TelegramObject): giveaways only. """ - __slots__ = ( - "chats", - "country_codes", - "has_public_winners", - "only_new_members", - "premium_subscription_month_count", - "prize_description", - "prize_star_count", - "winner_count", - "winners_selection_date", - ) - - def __init__( - self, - chats: Sequence[Chat], - winners_selection_date: dtm.datetime, - winner_count: int, - only_new_members: bool | None = None, - has_public_winners: bool | None = None, - prize_description: str | None = None, - country_codes: Sequence[str] | None = None, - premium_subscription_month_count: int | None = None, - prize_star_count: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.chats: tuple[Chat, ...] = tuple(chats) - self.winners_selection_date: dtm.datetime = winners_selection_date - self.winner_count: int = winner_count - self.only_new_members: bool | None = only_new_members - self.has_public_winners: bool | None = has_public_winners - self.prize_description: str | None = prize_description - self.country_codes: tuple[str, ...] = parse_sequence_arg(country_codes) - self.premium_subscription_month_count: int | None = premium_subscription_month_count - self.prize_star_count: int | None = prize_star_count - - self._id_attrs = ( - self.chats, - self.winners_selection_date, - self.winner_count, - ) - - self._freeze() + chats: tuple[Chat, ...] = tg_field(compare=True, converter=parse_sequence_arg) + winners_selection_date: dtm.datetime = tg_field(compare=True) + winner_count: int = tg_field(compare=True) + only_new_members: bool | None = tg_field(default=None) + has_public_winners: bool | None = tg_field(default=None) + prize_description: str | None = tg_field(default=None) + country_codes: tuple[str, ...] = tg_field(default=None, converter=parse_sequence_arg) + premium_subscription_month_count: int | None = tg_field(default=None) + prize_star_count: int | None = tg_field(default=None) +@tg_dataclass() class GiveawayCreated(TelegramObject): """This object represents a service message about the creation of a scheduled giveaway. @@ -154,15 +119,10 @@ class GiveawayCreated(TelegramObject): """ - __slots__ = ("prize_star_count",) - - def __init__(self, prize_star_count: int | None = None, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.prize_star_count: int | None = prize_star_count - - self._freeze() + prize_star_count: int | None = tg_field(default=None) +@tg_dataclass() class GiveawayWinners(TelegramObject): """This object represents a message about the completion of a giveaway with public winners. @@ -219,64 +179,21 @@ class GiveawayWinners(TelegramObject): prize_description (:obj:`str`): Optional. Description of additional giveaway prize """ - __slots__ = ( - "additional_chat_count", - "chat", - "giveaway_message_id", - "only_new_members", - "premium_subscription_month_count", - "prize_description", - "prize_star_count", - "unclaimed_prize_count", - "was_refunded", - "winner_count", - "winners", - "winners_selection_date", - ) - - def __init__( - self, - chat: Chat, - giveaway_message_id: int, - winners_selection_date: dtm.datetime, - winner_count: int, - winners: Sequence[User], - additional_chat_count: int | None = None, - premium_subscription_month_count: int | None = None, - unclaimed_prize_count: int | None = None, - only_new_members: bool | None = None, - was_refunded: bool | None = None, - prize_description: str | None = None, - prize_star_count: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.chat: Chat = chat - self.giveaway_message_id: int = giveaway_message_id - self.winners_selection_date: dtm.datetime = winners_selection_date - self.winner_count: int = winner_count - self.winners: tuple[User, ...] = tuple(winners) - self.additional_chat_count: int | None = additional_chat_count - self.premium_subscription_month_count: int | None = premium_subscription_month_count - self.unclaimed_prize_count: int | None = unclaimed_prize_count - self.only_new_members: bool | None = only_new_members - self.was_refunded: bool | None = was_refunded - self.prize_description: str | None = prize_description - self.prize_star_count: int | None = prize_star_count - - self._id_attrs = ( - self.chat, - self.giveaway_message_id, - self.winners_selection_date, - self.winner_count, - self.winners, - ) - - self._freeze() - - + chat: Chat = tg_field(compare=True) + giveaway_message_id: int = tg_field(compare=True) + winners_selection_date: dtm.datetime = tg_field(compare=True) + winner_count: int = tg_field(compare=True) + winners: tuple[User, ...] = tg_field(compare=True, converter=parse_sequence_arg) + additional_chat_count: int | None = tg_field(default=None) + premium_subscription_month_count: int | None = tg_field(default=None) + unclaimed_prize_count: int | None = tg_field(default=None) + only_new_members: bool | None = tg_field(default=None) + was_refunded: bool | None = tg_field(default=None) + prize_description: str | None = tg_field(default=None) + prize_star_count: int | None = tg_field(default=None) + + +@tg_dataclass() class GiveawayCompleted(TelegramObject): """This object represents a service message about the completion of a giveaway without public winners. @@ -307,27 +224,7 @@ class GiveawayCompleted(TelegramObject): .. versionadded:: 21.6 """ - __slots__ = ("giveaway_message", "is_star_giveaway", "unclaimed_prize_count", "winner_count") - - def __init__( - self, - winner_count: int, - unclaimed_prize_count: int | None = None, - giveaway_message: "Message | None" = None, - is_star_giveaway: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.winner_count: int = winner_count - self.unclaimed_prize_count: int | None = unclaimed_prize_count - self.giveaway_message: Message | None = giveaway_message - self.is_star_giveaway: bool | None = is_star_giveaway - - self._id_attrs = ( - self.winner_count, - self.unclaimed_prize_count, - ) - - self._freeze() + winner_count: int = tg_field(compare=True) + unclaimed_prize_count: int | None = tg_field(compare=True, default=None) + giveaway_message: "Message | None" = tg_field(default=None) + is_star_giveaway: bool | None = tg_field(default=None) diff --git a/src/telegram/_inputchecklist.py b/src/telegram/_inputchecklist.py index 21ae2f491f6..c1bcafc398d 100644 --- a/src/telegram/_inputchecklist.py +++ b/src/telegram/_inputchecklist.py @@ -18,15 +18,15 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an objects that are related to Telegram input checklists.""" -from collections.abc import Sequence - from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput +@tg_dataclass() class InputChecklistTask(TelegramObject): """ Describes a task to add to a checklist. @@ -70,33 +70,13 @@ class InputChecklistTask(TelegramObject): """ - __slots__ = ( - "id", - "parse_mode", - "text", - "text_entities", - ) - - def __init__( - self, - id: int, # pylint: disable=redefined-builtin - text: str, - parse_mode: ODVInput[str] = DEFAULT_NONE, - text_entities: Sequence[MessageEntity] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.id: int = id - self.text: str = text - self.parse_mode: ODVInput[str] = parse_mode - self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) - - self._id_attrs = (self.id,) - - self._freeze() + id: int = tg_field(compare=True) + text: str = tg_field() + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + text_entities: tuple[MessageEntity, ...] = tg_field(default=None, converter=parse_sequence_arg) +@tg_dataclass() class InputChecklist(TelegramObject): """ Describes a checklist to create. @@ -153,34 +133,11 @@ class InputChecklist(TelegramObject): """ - __slots__ = ( - "others_can_add_tasks", - "others_can_mark_tasks_as_done", - "parse_mode", - "tasks", - "title", - "title_entities", + title: str = tg_field() + tasks: tuple[InputChecklistTask, ...] = tg_field(compare=True, converter=parse_sequence_arg) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + title_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - title: str, - tasks: Sequence[InputChecklistTask], - parse_mode: ODVInput[str] = DEFAULT_NONE, - title_entities: Sequence[MessageEntity] | None = None, - others_can_add_tasks: bool | None = None, - others_can_mark_tasks_as_done: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.title: str = title - self.tasks: tuple[InputChecklistTask, ...] = parse_sequence_arg(tasks) - self.parse_mode: ODVInput[str] = parse_mode - self.title_entities: tuple[MessageEntity, ...] = parse_sequence_arg(title_entities) - self.others_can_add_tasks: bool | None = others_can_add_tasks - self.others_can_mark_tasks_as_done: bool | None = others_can_mark_tasks_as_done - - self._id_attrs = (self.tasks,) - - self._freeze() + others_can_add_tasks: bool | None = tg_field(default=None) + others_can_mark_tasks_as_done: bool | None = tg_field(default=None) diff --git a/src/telegram/_keyboardbutton.py b/src/telegram/_keyboardbutton.py index 94e86a64d0c..70643656f34 100644 --- a/src/telegram/_keyboardbutton.py +++ b/src/telegram/_keyboardbutton.py @@ -25,10 +25,11 @@ KeyboardButtonRequestUsers, ) from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._webappinfo import WebAppInfo +@tg_dataclass() class KeyboardButton(TelegramObject): """ This object represents one button of the reply keyboard. At most one of the optional fields @@ -182,65 +183,22 @@ class KeyboardButton(TelegramObject): .. versionadded:: 22.8 """ - __slots__ = ( - "icon_custom_emoji_id", - "request_chat", - "request_contact", - "request_location", - "request_managed_bot", - "request_poll", - "request_users", - "style", - "text", - "web_app", - ) - __REMOVED_API_FIELDS__ = frozenset( { "request_user", } ) - def __init__( - self, - text: str, - request_contact: bool | None = None, - request_location: bool | None = None, - request_poll: KeyboardButtonPollType | None = None, - web_app: WebAppInfo | None = None, - request_chat: KeyboardButtonRequestChat | None = None, - request_users: KeyboardButtonRequestUsers | None = None, - style: str | None = None, - icon_custom_emoji_id: str | None = None, - request_managed_bot: KeyboardButtonRequestManagedBot | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Required - self.text: str = text - # Optionals - self.request_contact: bool | None = request_contact - self.request_location: bool | None = request_location - self.request_poll: KeyboardButtonPollType | None = request_poll - self.web_app: WebAppInfo | None = web_app - self.request_users: KeyboardButtonRequestUsers | None = request_users - self.request_chat: KeyboardButtonRequestChat | None = request_chat - self.style: str | None = style - self.icon_custom_emoji_id: str | None = icon_custom_emoji_id - self.request_managed_bot: KeyboardButtonRequestManagedBot | None = request_managed_bot - - self._id_attrs = ( - self.text, - self.request_contact, - self.request_location, - self.request_poll, - self.web_app, - self.request_users, - self.request_chat, - self.style, - self.icon_custom_emoji_id, - ) - - self._freeze() + # Required + text: str = tg_field(compare=True) + + # Optionals + request_contact: bool | None = tg_field(compare=True, default=None) + request_location: bool | None = tg_field(compare=True, default=None) + request_poll: KeyboardButtonPollType | None = tg_field(compare=True, default=None) + web_app: WebAppInfo | None = tg_field(compare=True, default=None) + request_chat: KeyboardButtonRequestChat | None = tg_field(compare=True, default=None) + request_users: KeyboardButtonRequestUsers | None = tg_field(compare=True, default=None) + style: str | None = tg_field(compare=True, default=None) + icon_custom_emoji_id: str | None = tg_field(compare=True, default=None) + request_managed_bot: KeyboardButtonRequestManagedBot | None = tg_field(default=None) diff --git a/src/telegram/_keyboardbuttonpolltype.py b/src/telegram/_keyboardbuttonpolltype.py index 86baadb8fa4..1974ca96dbc 100644 --- a/src/telegram/_keyboardbuttonpolltype.py +++ b/src/telegram/_keyboardbuttonpolltype.py @@ -20,10 +20,11 @@ from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram.constants import PollType +@tg_dataclass() class KeyboardButtonPollType(TelegramObject): """This object represents type of a poll, which is allowed to be created and sent when the corresponding button is pressed. @@ -46,17 +47,8 @@ class KeyboardButtonPollType(TelegramObject): Otherwise, the user will be allowed to create a poll of any type. """ - __slots__ = ("type",) + @staticmethod + def _type_converter(value: str | None) -> str | None: + return enum.get_member(PollType, value, value) - def __init__( - self, - type: str | None = None, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.type: str | None = enum.get_member(PollType, type, type) - - self._id_attrs = (self.type,) - - self._freeze() + type: str | None = tg_field(compare=True, default=None, converter=_type_converter) diff --git a/src/telegram/_keyboardbuttonrequest.py b/src/telegram/_keyboardbuttonrequest.py index b5e1bb062ce..14fc3a66141 100644 --- a/src/telegram/_keyboardbuttonrequest.py +++ b/src/telegram/_keyboardbuttonrequest.py @@ -20,9 +20,10 @@ from telegram._chatadministratorrights import ChatAdministratorRights from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class KeyboardButtonRequestUsers(TelegramObject): """This object defines the criteria used to request a suitable user. The identifier of the selected user will be shared with the bot when the corresponding button is pressed. `More @@ -87,45 +88,19 @@ class KeyboardButtonRequestUsers(TelegramObject): """ - __slots__ = ( - "max_quantity", - "request_id", - "request_name", - "request_photo", - "request_username", - "user_is_bot", - "user_is_premium", - ) - - def __init__( - self, - request_id: int, - user_is_bot: bool | None = None, - user_is_premium: bool | None = None, - max_quantity: int | None = None, - request_name: bool | None = None, - request_username: bool | None = None, - request_photo: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.request_id: int = request_id - - # Optionals - self.user_is_bot: bool | None = user_is_bot - self.user_is_premium: bool | None = user_is_premium - self.max_quantity: int | None = max_quantity - self.request_name: bool | None = request_name - self.request_username: bool | None = request_username - self.request_photo: bool | None = request_photo - - self._id_attrs = (self.request_id,) - - self._freeze() + # Required + request_id: int = tg_field(compare=True) + # Optionals + user_is_bot: bool | None = tg_field(default=None) + user_is_premium: bool | None = tg_field(default=None) + max_quantity: int | None = tg_field(default=None) + request_name: bool | None = tg_field(default=None) + request_username: bool | None = tg_field(default=None) + request_photo: bool | None = tg_field(default=None) + +@tg_dataclass() class KeyboardButtonRequestChat(TelegramObject): """This object defines the criteria used to request a suitable chat. The identifier of the selected user will be shared with the bot when the corresponding button is pressed. `More @@ -199,57 +174,23 @@ class KeyboardButtonRequestChat(TelegramObject): .. versionadded:: 21.1 """ - __slots__ = ( - "bot_administrator_rights", - "bot_is_member", - "chat_has_username", - "chat_is_channel", - "chat_is_created", - "chat_is_forum", - "request_id", - "request_photo", - "request_title", - "request_username", - "user_administrator_rights", - ) - - def __init__( - self, - request_id: int, - chat_is_channel: bool, - chat_is_forum: bool | None = None, - chat_has_username: bool | None = None, - chat_is_created: bool | None = None, - user_administrator_rights: ChatAdministratorRights | None = None, - bot_administrator_rights: ChatAdministratorRights | None = None, - bot_is_member: bool | None = None, - request_title: bool | None = None, - request_username: bool | None = None, - request_photo: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # required - self.request_id: int = request_id - self.chat_is_channel: bool = chat_is_channel - - # optional - self.chat_is_forum: bool | None = chat_is_forum - self.chat_has_username: bool | None = chat_has_username - self.chat_is_created: bool | None = chat_is_created - self.user_administrator_rights: ChatAdministratorRights | None = user_administrator_rights - self.bot_administrator_rights: ChatAdministratorRights | None = bot_administrator_rights - self.bot_is_member: bool | None = bot_is_member - self.request_title: bool | None = request_title - self.request_username: bool | None = request_username - self.request_photo: bool | None = request_photo - - self._id_attrs = (self.request_id,) - - self._freeze() + # required + request_id: int = tg_field(compare=True) + chat_is_channel: bool = tg_field() + + # optional + chat_is_forum: bool | None = tg_field(default=None) + chat_has_username: bool | None = tg_field(default=None) + chat_is_created: bool | None = tg_field(default=None) + user_administrator_rights: ChatAdministratorRights | None = tg_field(default=None) + bot_administrator_rights: ChatAdministratorRights | None = tg_field(default=None) + bot_is_member: bool | None = tg_field(default=None) + request_title: bool | None = tg_field(default=None) + request_username: bool | None = tg_field(default=None) + request_photo: bool | None = tg_field(default=None) +@tg_dataclass() class KeyboardButtonRequestManagedBot(TelegramObject): """ This object defines the parameters for the creation of a managed bot. @@ -275,26 +216,9 @@ class KeyboardButtonRequestManagedBot(TelegramObject): suggested_username (:obj:`str`): Optional. Suggested username for the bot. """ - __slots__ = ( - "request_id", - "suggested_name", - "suggested_username", - ) - - def __init__( - self, - request_id: int, - suggested_name: str | None = None, - suggested_username: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.request_id: int = request_id - # Optional - self.suggested_name: str | None = suggested_name - self.suggested_username: str | None = suggested_username - - self._id_attrs = (self.request_id,) - self._freeze() + # Required + request_id: int = tg_field(compare=True) + + # Optional + suggested_name: str | None = tg_field(default=None) + suggested_username: str | None = tg_field(default=None) diff --git a/src/telegram/_linkpreviewoptions.py b/src/telegram/_linkpreviewoptions.py index 5ad13de7a16..01d74483514 100644 --- a/src/telegram/_linkpreviewoptions.py +++ b/src/telegram/_linkpreviewoptions.py @@ -19,10 +19,12 @@ """This module contains the LinkPreviewOptions class.""" from telegram._telegramobject import TelegramObject +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput +@tg_dataclass() class LinkPreviewOptions(TelegramObject): """ Describes the options used for link preview generation. @@ -62,39 +64,9 @@ class LinkPreviewOptions(TelegramObject): text. """ - __slots__ = ( - "is_disabled", - "prefer_large_media", - "prefer_small_media", - "show_above_text", - "url", - ) - - def __init__( - self, - is_disabled: ODVInput[bool] = DEFAULT_NONE, - url: ODVInput[str] = DEFAULT_NONE, - prefer_small_media: ODVInput[bool] = DEFAULT_NONE, - prefer_large_media: ODVInput[bool] = DEFAULT_NONE, - show_above_text: ODVInput[bool] = DEFAULT_NONE, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Optionals - - self.is_disabled: ODVInput[bool] = is_disabled - self.url: ODVInput[str] = url - self.prefer_small_media: ODVInput[bool] = prefer_small_media - self.prefer_large_media: ODVInput[bool] = prefer_large_media - self.show_above_text: ODVInput[bool] = show_above_text - - self._id_attrs = ( - self.is_disabled, - self.url, - self.prefer_small_media, - self.prefer_large_media, - self.show_above_text, - ) - self._freeze() + # Optionals + is_disabled: ODVInput[bool] = tg_field(compare=True, default=DEFAULT_NONE) + url: ODVInput[str] = tg_field(compare=True, default=DEFAULT_NONE) + prefer_small_media: ODVInput[bool] = tg_field(compare=True, default=DEFAULT_NONE) + prefer_large_media: ODVInput[bool] = tg_field(compare=True, default=DEFAULT_NONE) + show_above_text: ODVInput[bool] = tg_field(compare=True, default=DEFAULT_NONE) diff --git a/src/telegram/_loginurl.py b/src/telegram/_loginurl.py index 9b0ce84c377..9f9276cd717 100644 --- a/src/telegram/_loginurl.py +++ b/src/telegram/_loginurl.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram LoginUrl.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class LoginUrl(TelegramObject): """This object represents a parameter of the inline keyboard button used to automatically authorize a user. Serves as a great replacement for the Telegram Login Widget when the user is @@ -80,25 +81,9 @@ class LoginUrl(TelegramObject): """ - __slots__ = ("bot_username", "forward_text", "request_write_access", "url") - - def __init__( - self, - url: str, - forward_text: str | None = None, - bot_username: str | None = None, - request_write_access: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.url: str = url - # Optional - self.forward_text: str | None = forward_text - self.bot_username: str | None = bot_username - self.request_write_access: bool | None = request_write_access - - self._id_attrs = (self.url,) - - self._freeze() + # Required + url: str = tg_field(compare=True) + # Optional + forward_text: str | None = tg_field() + bot_username: str | None = tg_field() + request_write_access: bool | None = tg_field() diff --git a/src/telegram/_managedbot.py b/src/telegram/_managedbot.py index 3bcec651f39..65635dcd761 100644 --- a/src/telegram/_managedbot.py +++ b/src/telegram/_managedbot.py @@ -20,11 +20,10 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.types import ( - JSONDict, -) +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ManagedBotCreated(TelegramObject): """ This object contains information about the bot that was created to be managed by the current @@ -43,20 +42,10 @@ class ManagedBotCreated(TelegramObject): using the method :meth:`~telegram.Bot.get_managed_bot_token`. """ - __slots__ = ("bot",) - - def __init__( - self, - bot: User, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.bot: User = bot - self._id_attrs = (self.bot,) - self._freeze() + bot: User = tg_field(compare=True) +@tg_dataclass() class ManagedBotUpdated(TelegramObject): """ This object contains information about the creation, token update, or owner update of a bot @@ -78,22 +67,6 @@ class ManagedBotUpdated(TelegramObject): using the method :meth:`~telegram.Bot.get_managed_bot_token`. """ - __slots__ = ("bot", "user") - - def __init__( - self, - user: User, - bot: User, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.user: User = user - self.bot: User = bot - - self._id_attrs = ( - self.user, - self.bot, - ) - self._freeze() + # Required + user: User = tg_field(compare=True) + bot: User = tg_field(compare=True) diff --git a/src/telegram/_menubutton.py b/src/telegram/_menubutton.py index c7615587496..7032af8430b 100644 --- a/src/telegram/_menubutton.py +++ b/src/telegram/_menubutton.py @@ -18,15 +18,16 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects related to Telegram menu buttons.""" -from typing import ClassVar, Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._webappinfo import WebAppInfo +@tg_dataclass() class MenuButton(TelegramObject): """This object describes the bot's menu button in a private chat. It should be one of @@ -51,8 +52,6 @@ class MenuButton(TelegramObject): type (:obj:`str`): Type of menu button that the instance represents. """ - __slots__ = ("type",) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "type", { @@ -62,27 +61,21 @@ class MenuButton(TelegramObject): }, ) - def __init__( - self, - type: str, - *, - api_kwargs: JSONDict | None = None, - ): # pylint: disable=redefined-builtin - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.MenuButtonType, type, type) - - self._id_attrs = (self.type,) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.MenuButtonType, value, value) - self._freeze() + type: str = tg_field(compare=True, converter=_type_converter) - COMMANDS: Final[str] = constants.MenuButtonType.COMMANDS + COMMANDS: ClassVar[str] = constants.MenuButtonType.COMMANDS """:const:`telegram.constants.MenuButtonType.COMMANDS`""" - WEB_APP: Final[str] = constants.MenuButtonType.WEB_APP + WEB_APP: ClassVar[str] = constants.MenuButtonType.WEB_APP """:const:`telegram.constants.MenuButtonType.WEB_APP`""" - DEFAULT: Final[str] = constants.MenuButtonType.DEFAULT + DEFAULT: ClassVar[str] = constants.MenuButtonType.DEFAULT """:const:`telegram.constants.MenuButtonType.DEFAULT`""" +@tg_dataclass() class MenuButtonCommands(MenuButton): """Represents a menu button, which opens the bot's list of commands. @@ -93,13 +86,11 @@ class MenuButtonCommands(MenuButton): type (:obj:`str`): :tg-const:`telegram.constants.MenuButtonType.COMMANDS`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None): - super().__init__(type=constants.MenuButtonType.COMMANDS, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=MenuButton.COMMANDS) +@tg_dataclass() class MenuButtonWebApp(MenuButton): """Represents a menu button, which launches a `Web App `_. @@ -131,17 +122,14 @@ class MenuButtonWebApp(MenuButton): will be opened as if the user pressed the link. """ - __slots__ = ("text", "web_app") + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=MenuButton.WEB_APP) - def __init__(self, text: str, web_app: WebAppInfo, *, api_kwargs: JSONDict | None = None): - super().__init__(type=constants.MenuButtonType.WEB_APP, api_kwargs=api_kwargs) - with self._unfrozen(): - self.text: str = text - self.web_app: WebAppInfo = web_app - - self._id_attrs = (self.type, self.text, self.web_app) + text: str = tg_field(compare=True) + web_app: WebAppInfo = tg_field(compare=True) +@tg_dataclass() class MenuButtonDefault(MenuButton): """Describes that no specific value for the menu button was set. @@ -150,8 +138,5 @@ class MenuButtonDefault(MenuButton): type (:obj:`str`): :tg-const:`telegram.constants.MenuButtonType.DEFAULT`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None): - super().__init__(type=constants.MenuButtonType.DEFAULT, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=MenuButton.DEFAULT) diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 0551278b16f..8ce66d880eb 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# pylint: disable=too-many-instance-attributes, too-many-arguments +# pylint: disable=too-many-arguments # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2026 @@ -19,11 +19,12 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Message.""" +import dataclasses import datetime as dtm import re from collections.abc import Sequence from html import escape -from typing import TYPE_CHECKING, ClassVar, TypedDict +from typing import TYPE_CHECKING, Any, ClassVar, TypedDict from telegram._chat import Chat from telegram._chatbackground import ChatBackground @@ -76,6 +77,7 @@ from telegram._uniquegift import UniqueGiftInfo from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import to_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.entities import parse_message_entities, parse_message_entity @@ -143,6 +145,7 @@ class _ReplyKwargs(TypedDict): reply_parameters: ReplyParameters +@tg_dataclass() class MaybeInaccessibleMessage(TelegramObject): """Base class for Telegram Message Objects. @@ -173,24 +176,9 @@ class MaybeInaccessibleMessage(TelegramObject): chat (:class:`telegram.Chat`): Conversation the message belongs to. """ - __slots__ = ("chat", "date", "message_id") - - def __init__( - self, - chat: Chat, - message_id: int, - date: dtm.datetime, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.chat: Chat = chat - self.message_id: int = message_id - self.date: dtm.datetime = date - - self._id_attrs = (self.message_id, self.chat) - - self._freeze() + chat: Chat = tg_field(compare=True) + message_id: int = tg_field(compare=True) + date: dtm.datetime = tg_field() @property def is_accessible(self) -> bool: @@ -217,6 +205,7 @@ def de_json( return super().de_json(data=data, bot=bot) +@tg_dataclass() class InaccessibleMessage(MaybeInaccessibleMessage): """This object represents an inaccessible message. @@ -238,19 +227,10 @@ class InaccessibleMessage(MaybeInaccessibleMessage): chat (:class:`telegram.Chat`): Chat the message belongs to. """ - __slots__ = () - - def __init__( - self, - chat: Chat, - message_id: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(chat=chat, message_id=message_id, date=ZERO_DATE, api_kwargs=api_kwargs) - self._freeze() + date: dtm.datetime = tg_field(init=False, default=ZERO_DATE) +@tg_dataclass() class Message(MaybeInaccessibleMessage): __REMOVED_API_FIELDS__: ClassVar[frozenset[str]] = frozenset( { @@ -1202,379 +1182,131 @@ class Message(MaybeInaccessibleMessage): """ # fmt: on - __slots__ = ( - "_effective_attachment", - "animation", - "audio", - "author_signature", - "boost_added", - "business_connection_id", - "caption", - "caption_entities", - "channel_chat_created", - "chat_background_set", - "chat_owner_changed", - "chat_owner_left", - "chat_shared", - "checklist", - "checklist_tasks_added", - "checklist_tasks_done", - "connected_website", - "contact", - "delete_chat_photo", - "dice", - "direct_message_price_changed", - "direct_messages_topic", - "document", - "edit_date", - "effect_id", - "entities", - "external_reply", - "forum_topic_closed", - "forum_topic_created", - "forum_topic_edited", - "forum_topic_reopened", - "forward_origin", - "from_user", - "game", - "general_forum_topic_hidden", - "general_forum_topic_unhidden", - "gift", - "gift_upgrade_sent", - "giveaway", - "giveaway_completed", - "giveaway_created", - "giveaway_winners", - "group_chat_created", - "guest_bot_caller_chat", - "guest_bot_caller_user", - "guest_query_id", - "has_media_spoiler", - "has_protected_content", - "invoice", - "is_automatic_forward", - "is_from_offline", - "is_paid_post", - "is_topic_message", - "left_chat_member", - "link_preview_options", - "live_photo", - "location", - "managed_bot_created", - "media_group_id", - "message_auto_delete_timer_changed", - "message_thread_id", - "migrate_from_chat_id", - "migrate_to_chat_id", - "new_chat_members", - "new_chat_photo", - "new_chat_title", - "paid_media", - "paid_message_price_changed", - "paid_star_count", - "passport_data", - "photo", - "pinned_message", - "poll", - "poll_option_added", - "poll_option_deleted", - "proximity_alert_triggered", - "quote", - "refunded_payment", - "reply_markup", - "reply_to_checklist_task_id", - "reply_to_message", - "reply_to_poll_option_id", - "reply_to_story", - "sender_boost_count", - "sender_business_bot", - "sender_chat", - "sender_tag", - "show_caption_above_media", - "sticker", - "story", - "successful_payment", - "suggested_post_approval_failed", - "suggested_post_approved", - "suggested_post_declined", - "suggested_post_info", - "suggested_post_paid", - "suggested_post_refunded", - "supergroup_chat_created", - "text", - "unique_gift", - "users_shared", - "venue", - "via_bot", - "video", - "video_chat_ended", - "video_chat_participants_invited", - "video_chat_scheduled", - "video_chat_started", - "video_note", - "voice", - "web_app_data", - "write_access_allowed", - ) - def __init__( - self, - message_id: int, - date: dtm.datetime, - chat: Chat, - from_user: User | None = None, - reply_to_message: "Message | None" = None, - edit_date: dtm.datetime | None = None, - text: str | None = None, - entities: Sequence["MessageEntity"] | None = None, - caption_entities: Sequence["MessageEntity"] | None = None, - audio: Audio | None = None, - document: Document | None = None, - game: Game | None = None, - photo: Sequence[PhotoSize] | None = None, - sticker: Sticker | None = None, - video: Video | None = None, - voice: Voice | None = None, - video_note: VideoNote | None = None, - new_chat_members: Sequence[User] | None = None, - caption: str | None = None, - contact: "Contact | None" = None, - location: "Location | None" = None, - venue: Venue | None = None, - left_chat_member: User | None = None, - new_chat_title: str | None = None, - new_chat_photo: Sequence[PhotoSize] | None = None, - delete_chat_photo: bool | None = None, - group_chat_created: bool | None = None, - supergroup_chat_created: bool | None = None, - channel_chat_created: bool | None = None, - migrate_to_chat_id: int | None = None, - migrate_from_chat_id: int | None = None, - pinned_message: MaybeInaccessibleMessage | None = None, - invoice: Invoice | None = None, - successful_payment: SuccessfulPayment | None = None, - author_signature: str | None = None, - media_group_id: str | None = None, - connected_website: str | None = None, - animation: Animation | None = None, - passport_data: PassportData | None = None, - poll: Poll | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - dice: Dice | None = None, - via_bot: User | None = None, - proximity_alert_triggered: ProximityAlertTriggered | None = None, - sender_chat: Chat | None = None, - video_chat_started: VideoChatStarted | None = None, - video_chat_ended: VideoChatEnded | None = None, - video_chat_participants_invited: VideoChatParticipantsInvited | None = None, - message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged | None = None, - video_chat_scheduled: VideoChatScheduled | None = None, - is_automatic_forward: bool | None = None, - has_protected_content: bool | None = None, - web_app_data: WebAppData | None = None, - is_topic_message: bool | None = None, - message_thread_id: int | None = None, - forum_topic_created: ForumTopicCreated | None = None, - forum_topic_closed: ForumTopicClosed | None = None, - forum_topic_reopened: ForumTopicReopened | None = None, - forum_topic_edited: ForumTopicEdited | None = None, - general_forum_topic_hidden: GeneralForumTopicHidden | None = None, - general_forum_topic_unhidden: GeneralForumTopicUnhidden | None = None, - write_access_allowed: WriteAccessAllowed | None = None, - has_media_spoiler: bool | None = None, - chat_shared: ChatShared | None = None, - story: Story | None = None, - giveaway: "Giveaway | None" = None, - giveaway_completed: "GiveawayCompleted | None" = None, - giveaway_created: "GiveawayCreated | None" = None, - giveaway_winners: "GiveawayWinners | None" = None, - users_shared: UsersShared | None = None, - link_preview_options: LinkPreviewOptions | None = None, - external_reply: "ExternalReplyInfo | None" = None, - quote: "TextQuote | None" = None, - forward_origin: "MessageOrigin | None" = None, - reply_to_story: Story | None = None, - boost_added: ChatBoostAdded | None = None, - sender_boost_count: int | None = None, - business_connection_id: str | None = None, - sender_business_bot: User | None = None, - is_from_offline: bool | None = None, - chat_background_set: ChatBackground | None = None, - effect_id: str | None = None, - show_caption_above_media: bool | None = None, - paid_media: PaidMediaInfo | None = None, - refunded_payment: RefundedPayment | None = None, - gift: GiftInfo | None = None, - unique_gift: UniqueGiftInfo | None = None, - paid_message_price_changed: PaidMessagePriceChanged | None = None, - paid_star_count: int | None = None, - direct_message_price_changed: DirectMessagePriceChanged | None = None, - checklist: Checklist | None = None, - checklist_tasks_done: ChecklistTasksDone | None = None, - checklist_tasks_added: ChecklistTasksAdded | None = None, - is_paid_post: bool | None = None, - direct_messages_topic: DirectMessagesTopic | None = None, - reply_to_checklist_task_id: int | None = None, - suggested_post_declined: "SuggestedPostDeclined | None" = None, - suggested_post_paid: "SuggestedPostPaid | None" = None, - suggested_post_refunded: "SuggestedPostRefunded | None" = None, - suggested_post_info: "SuggestedPostInfo | None" = None, - suggested_post_approved: "SuggestedPostApproved | None" = None, - suggested_post_approval_failed: "SuggestedPostApprovalFailed | None" = None, - gift_upgrade_sent: GiftInfo | None = None, - chat_owner_changed: ChatOwnerChanged | None = None, - chat_owner_left: ChatOwnerLeft | None = None, - sender_tag: str | None = None, - poll_option_added: PollOptionAdded | None = None, - poll_option_deleted: PollOptionDeleted | None = None, - reply_to_poll_option_id: str | None = None, - managed_bot_created: ManagedBotCreated | None = None, - guest_bot_caller_user: User | None = None, - guest_bot_caller_chat: Chat | None = None, - guest_query_id: str | None = None, - live_photo: LivePhoto | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(chat=chat, message_id=message_id, date=date, api_kwargs=api_kwargs) - - with self._unfrozen(): - # Required - self.message_id: int = message_id - # Optionals - self.from_user: User | None = from_user - self.sender_chat: Chat | None = sender_chat - self.date: dtm.datetime = date - self.chat: Chat = chat - self.is_automatic_forward: bool | None = is_automatic_forward - self.reply_to_message: Message | None = reply_to_message - self.edit_date: dtm.datetime | None = edit_date - self.has_protected_content: bool | None = has_protected_content - self.text: str | None = text - self.entities: tuple[MessageEntity, ...] = parse_sequence_arg(entities) - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.audio: Audio | None = audio - self.game: Game | None = game - self.document: Document | None = document - self.photo: tuple[PhotoSize, ...] = parse_sequence_arg(photo) - self.sticker: Sticker | None = sticker - self.video: Video | None = video - self.voice: Voice | None = voice - self.video_note: VideoNote | None = video_note - self.caption: str | None = caption - self.contact: Contact | None = contact - self.location: Location | None = location - self.venue: Venue | None = venue - self.new_chat_members: tuple[User, ...] = parse_sequence_arg(new_chat_members) - self.left_chat_member: User | None = left_chat_member - self.new_chat_title: str | None = new_chat_title - self.new_chat_photo: tuple[PhotoSize, ...] = parse_sequence_arg(new_chat_photo) - self.delete_chat_photo: bool | None = bool(delete_chat_photo) - self.group_chat_created: bool | None = bool(group_chat_created) - self.supergroup_chat_created: bool | None = bool(supergroup_chat_created) - self.migrate_to_chat_id: int | None = migrate_to_chat_id - self.migrate_from_chat_id: int | None = migrate_from_chat_id - self.channel_chat_created: bool | None = bool(channel_chat_created) - self.message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged | None = ( - message_auto_delete_timer_changed - ) - self.pinned_message: MaybeInaccessibleMessage | None = pinned_message - self.invoice: Invoice | None = invoice - self.successful_payment: SuccessfulPayment | None = successful_payment - self.connected_website: str | None = connected_website - self.author_signature: str | None = author_signature - self.media_group_id: str | None = media_group_id - self.animation: Animation | None = animation - self.passport_data: PassportData | None = passport_data - self.poll: Poll | None = poll - self.dice: Dice | None = dice - self.via_bot: User | None = via_bot - self.proximity_alert_triggered: ProximityAlertTriggered | None = ( - proximity_alert_triggered - ) - self.video_chat_scheduled: VideoChatScheduled | None = video_chat_scheduled - self.video_chat_started: VideoChatStarted | None = video_chat_started - self.video_chat_ended: VideoChatEnded | None = video_chat_ended - self.video_chat_participants_invited: VideoChatParticipantsInvited | None = ( - video_chat_participants_invited - ) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.web_app_data: WebAppData | None = web_app_data - self.is_topic_message: bool | None = is_topic_message - self.message_thread_id: int | None = message_thread_id - self.forum_topic_created: ForumTopicCreated | None = forum_topic_created - self.forum_topic_closed: ForumTopicClosed | None = forum_topic_closed - self.forum_topic_reopened: ForumTopicReopened | None = forum_topic_reopened - self.forum_topic_edited: ForumTopicEdited | None = forum_topic_edited - self.general_forum_topic_hidden: GeneralForumTopicHidden | None = ( - general_forum_topic_hidden - ) - self.general_forum_topic_unhidden: GeneralForumTopicUnhidden | None = ( - general_forum_topic_unhidden - ) - self.write_access_allowed: WriteAccessAllowed | None = write_access_allowed - self.has_media_spoiler: bool | None = has_media_spoiler - self.checklist: Checklist | None = checklist - self.users_shared: UsersShared | None = users_shared - self.chat_shared: ChatShared | None = chat_shared - self.story: Story | None = story - self.giveaway: Giveaway | None = giveaway - self.giveaway_completed: GiveawayCompleted | None = giveaway_completed - self.giveaway_created: GiveawayCreated | None = giveaway_created - self.giveaway_winners: GiveawayWinners | None = giveaway_winners - self.link_preview_options: LinkPreviewOptions | None = link_preview_options - self.external_reply: ExternalReplyInfo | None = external_reply - self.quote: TextQuote | None = quote - self.forward_origin: MessageOrigin | None = forward_origin - self.reply_to_story: Story | None = reply_to_story - self.boost_added: ChatBoostAdded | None = boost_added - self.sender_boost_count: int | None = sender_boost_count - self.business_connection_id: str | None = business_connection_id - self.sender_business_bot: User | None = sender_business_bot - self.is_from_offline: bool | None = is_from_offline - self.chat_background_set: ChatBackground | None = chat_background_set - self.checklist_tasks_done: ChecklistTasksDone | None = checklist_tasks_done - self.checklist_tasks_added: ChecklistTasksAdded | None = checklist_tasks_added - self.effect_id: str | None = effect_id - self.show_caption_above_media: bool | None = show_caption_above_media - self.paid_media: PaidMediaInfo | None = paid_media - self.refunded_payment: RefundedPayment | None = refunded_payment - self.gift: GiftInfo | None = gift - self.unique_gift: UniqueGiftInfo | None = unique_gift - self.paid_message_price_changed: PaidMessagePriceChanged | None = ( - paid_message_price_changed - ) - self.paid_star_count: int | None = paid_star_count - self.direct_message_price_changed: DirectMessagePriceChanged | None = ( - direct_message_price_changed - ) - self.is_paid_post: bool | None = is_paid_post - self.direct_messages_topic: DirectMessagesTopic | None = direct_messages_topic - self.reply_to_checklist_task_id: int | None = reply_to_checklist_task_id - self.suggested_post_declined: SuggestedPostDeclined | None = suggested_post_declined - self.suggested_post_paid: SuggestedPostPaid | None = suggested_post_paid - self.suggested_post_refunded: SuggestedPostRefunded | None = suggested_post_refunded - self.suggested_post_info: SuggestedPostInfo | None = suggested_post_info - self.suggested_post_approved: SuggestedPostApproved | None = suggested_post_approved - self.suggested_post_approval_failed: SuggestedPostApprovalFailed | None = ( - suggested_post_approval_failed - ) - self.gift_upgrade_sent: GiftInfo | None = gift_upgrade_sent - self.chat_owner_changed: ChatOwnerChanged | None = chat_owner_changed - self.chat_owner_left: ChatOwnerLeft | None = chat_owner_left - self.sender_tag: str | None = sender_tag - self.poll_option_added: PollOptionAdded | None = poll_option_added - self.poll_option_deleted: PollOptionDeleted | None = poll_option_deleted - self.reply_to_poll_option_id: str | None = reply_to_poll_option_id - self.managed_bot_created: ManagedBotCreated | None = managed_bot_created - self.guest_bot_caller_user: User | None = guest_bot_caller_user - self.guest_bot_caller_chat: Chat | None = guest_bot_caller_chat - self.guest_query_id: str | None = guest_query_id - self.live_photo: LivePhoto | None = live_photo - - self._effective_attachment = DEFAULT_NONE - - self._id_attrs = (self.message_id, self.chat) + # tags: deprecated NEXT.VERSION + # Remove these fields since they're inherited from parent + # and are only here to perseve previous (pre-dataclasses) ordering + message_id: int = tg_field(compare=True) + date: dtm.datetime = tg_field() + chat: Chat = tg_field(compare=True) + + from_user: User | None = tg_field(default=None) + reply_to_message: "Message | None" = tg_field(default=None) + edit_date: dtm.datetime | None = tg_field(default=None) + text: str | None = tg_field(default=None) + entities: tuple["MessageEntity", ...] = tg_field(default=None, converter=parse_sequence_arg) + caption_entities: tuple["MessageEntity", ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + audio: Audio | None = tg_field(default=None) + document: Document | None = tg_field(default=None) + game: Game | None = tg_field(default=None) + photo: tuple[PhotoSize, ...] = tg_field(default=None, converter=parse_sequence_arg) + sticker: Sticker | None = tg_field(default=None) + video: Video | None = tg_field(default=None) + voice: Voice | None = tg_field(default=None) + video_note: VideoNote | None = tg_field(default=None) + new_chat_members: tuple[User, ...] = tg_field(default=None, converter=parse_sequence_arg) + caption: str | None = tg_field(default=None) + contact: "Contact | None" = tg_field(default=None) + location: "Location | None" = tg_field(default=None) + venue: Venue | None = tg_field(default=None) + left_chat_member: User | None = tg_field(default=None) + new_chat_title: str | None = tg_field(default=None) + new_chat_photo: tuple[PhotoSize, ...] = tg_field(default=None, converter=parse_sequence_arg) + delete_chat_photo: bool | None = tg_field(default=None) + group_chat_created: bool | None = tg_field(default=None) + supergroup_chat_created: bool | None = tg_field(default=None) + channel_chat_created: bool | None = tg_field(default=None) + migrate_to_chat_id: int | None = tg_field(default=None) + migrate_from_chat_id: int | None = tg_field(default=None) + pinned_message: MaybeInaccessibleMessage | None = tg_field(default=None) + invoice: Invoice | None = tg_field(default=None) + successful_payment: SuccessfulPayment | None = tg_field(default=None) + author_signature: str | None = tg_field(default=None) + media_group_id: str | None = tg_field(default=None) + connected_website: str | None = tg_field(default=None) + animation: Animation | None = tg_field(default=None) + passport_data: PassportData | None = tg_field(default=None) + poll: Poll | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + dice: Dice | None = tg_field(default=None) + via_bot: User | None = tg_field(default=None) + proximity_alert_triggered: ProximityAlertTriggered | None = tg_field(default=None) + sender_chat: Chat | None = tg_field(default=None) + video_chat_started: VideoChatStarted | None = tg_field(default=None) + video_chat_ended: VideoChatEnded | None = tg_field(default=None) + video_chat_participants_invited: VideoChatParticipantsInvited | None = tg_field(default=None) + message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged | None = tg_field( + default=None + ) + video_chat_scheduled: VideoChatScheduled | None = tg_field(default=None) + is_automatic_forward: bool | None = tg_field(default=None) + has_protected_content: bool | None = tg_field(default=None) + web_app_data: WebAppData | None = tg_field(default=None) + is_topic_message: bool | None = tg_field(default=None) + message_thread_id: int | None = tg_field(default=None) + forum_topic_created: ForumTopicCreated | None = tg_field(default=None) + forum_topic_closed: ForumTopicClosed | None = tg_field(default=None) + forum_topic_reopened: ForumTopicReopened | None = tg_field(default=None) + forum_topic_edited: ForumTopicEdited | None = tg_field(default=None) + general_forum_topic_hidden: GeneralForumTopicHidden | None = tg_field(default=None) + general_forum_topic_unhidden: GeneralForumTopicUnhidden | None = tg_field(default=None) + write_access_allowed: WriteAccessAllowed | None = tg_field(default=None) + has_media_spoiler: bool | None = tg_field(default=None) + chat_shared: ChatShared | None = tg_field(default=None) + story: Story | None = tg_field(default=None) + giveaway: "Giveaway | None" = tg_field(default=None) + giveaway_completed: "GiveawayCompleted | None" = tg_field(default=None) + giveaway_created: "GiveawayCreated | None" = tg_field(default=None) + giveaway_winners: "GiveawayWinners | None" = tg_field(default=None) + users_shared: UsersShared | None = tg_field(default=None) + link_preview_options: LinkPreviewOptions | None = tg_field(default=None) + external_reply: "ExternalReplyInfo | None" = tg_field(default=None) + quote: "TextQuote | None" = tg_field(default=None) + forward_origin: "MessageOrigin | None" = tg_field(default=None) + reply_to_story: Story | None = tg_field(default=None) + boost_added: ChatBoostAdded | None = tg_field(default=None) + sender_boost_count: int | None = tg_field(default=None) + business_connection_id: str | None = tg_field(default=None) + sender_business_bot: User | None = tg_field(default=None) + is_from_offline: bool | None = tg_field(default=None) + chat_background_set: ChatBackground | None = tg_field(default=None) + effect_id: str | None = tg_field(default=None) + show_caption_above_media: bool | None = tg_field(default=None) + paid_media: PaidMediaInfo | None = tg_field(default=None) + refunded_payment: RefundedPayment | None = tg_field(default=None) + gift: GiftInfo | None = tg_field(default=None) + unique_gift: UniqueGiftInfo | None = tg_field(default=None) + paid_message_price_changed: PaidMessagePriceChanged | None = tg_field(default=None) + paid_star_count: int | None = tg_field(default=None) + direct_message_price_changed: DirectMessagePriceChanged | None = tg_field(default=None) + checklist: Checklist | None = tg_field(default=None) + checklist_tasks_done: ChecklistTasksDone | None = tg_field(default=None) + checklist_tasks_added: ChecklistTasksAdded | None = tg_field(default=None) + is_paid_post: bool | None = tg_field(default=None) + direct_messages_topic: DirectMessagesTopic | None = tg_field(default=None) + reply_to_checklist_task_id: int | None = tg_field(default=None) + suggested_post_declined: "SuggestedPostDeclined | None" = tg_field(default=None) + suggested_post_paid: "SuggestedPostPaid | None" = tg_field(default=None) + suggested_post_refunded: "SuggestedPostRefunded | None" = tg_field(default=None) + suggested_post_info: "SuggestedPostInfo | None" = tg_field(default=None) + suggested_post_approved: "SuggestedPostApproved | None" = tg_field(default=None) + suggested_post_approval_failed: "SuggestedPostApprovalFailed | None" = tg_field(default=None) + gift_upgrade_sent: GiftInfo | None = tg_field(default=None) + chat_owner_changed: ChatOwnerChanged | None = tg_field(default=None) + chat_owner_left: ChatOwnerLeft | None = tg_field(default=None) + sender_tag: str | None = tg_field(default=None) + poll_option_added: PollOptionAdded | None = tg_field(default=None) + poll_option_deleted: PollOptionDeleted | None = tg_field(default=None) + reply_to_poll_option_id: str | None = tg_field(default=None) + managed_bot_created: ManagedBotCreated | None = tg_field(default=None) + guest_bot_caller_user: User | None = tg_field(default=None) + guest_bot_caller_chat: Chat | None = tg_field(default=None) + guest_query_id: str | None = tg_field(default=None) + live_photo: LivePhoto | None = tg_field(default=None) + + _effective_attachment: Any = tg_field(init=False, default=DEFAULT_NONE) @property def chat_id(self) -> int: @@ -1680,7 +1412,7 @@ def effective_attachment( for attachment_type in MessageAttachmentType: if self[attachment_type]: - self._effective_attachment = self[attachment_type] # type: ignore[assignment] + object.__setattr__(self, "_effective_attachment", self[attachment_type]) if attachment_type == MessageAttachmentType.SUCCESSFUL_PAYMENT: warn( PTBDeprecationWarning( @@ -1692,7 +1424,7 @@ def effective_attachment( ) break else: - self._effective_attachment = None + object.__setattr__(self, "_effective_attachment", None) return self._effective_attachment # type: ignore[return-value] @@ -1789,12 +1521,13 @@ def compute_quote_position_and_entities( continue # create a new entity with the correct offset and length - # looping over slots rather manually accessing the attributes - # is more future-proof - kwargs = {attr: getattr(entity, attr) for attr in entity.__slots__} - kwargs["offset"] = offset - kwargs["length"] = e_length - entities.append(MessageEntity(**kwargs)) + entities.append( + dataclasses.replace( + entity, + offset=offset, + length=e_length, + ) + ) return position, tuple(entities) or None diff --git a/src/telegram/_messageautodeletetimerchanged.py b/src/telegram/_messageautodeletetimerchanged.py index 9a6e0a42852..c74804de0fe 100644 --- a/src/telegram/_messageautodeletetimerchanged.py +++ b/src/telegram/_messageautodeletetimerchanged.py @@ -24,10 +24,11 @@ from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod +@tg_dataclass() class MessageAutoDeleteTimerChanged(TelegramObject): """This object represents a service message about a change in auto-delete timer settings. @@ -52,20 +53,9 @@ class MessageAutoDeleteTimerChanged(TelegramObject): """ - __slots__ = ("_message_auto_delete_time",) - - def __init__( - self, - message_auto_delete_time: TimePeriod, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self._message_auto_delete_time: dtm.timedelta = to_timedelta(message_auto_delete_time) - - self._id_attrs = (self.message_auto_delete_time,) - - self._freeze() + _message_auto_delete_time: dtm.timedelta = tg_field( + compare=True, alias="message_auto_delete_time", converter=to_timedelta + ) @property def message_auto_delete_time(self) -> int | dtm.timedelta: diff --git a/src/telegram/_messageentity.py b/src/telegram/_messageentity.py index 62cf83e28f3..64016b61363 100644 --- a/src/telegram/_messageentity.py +++ b/src/telegram/_messageentity.py @@ -22,18 +22,19 @@ import datetime as dtm import itertools from collections.abc import Sequence -from typing import Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.strings import TextEncoding -from telegram._utils.types import JSONDict _SEM = Sequence["MessageEntity"] +@tg_dataclass() class MessageEntity(TelegramObject): """ This object represents one special entity in a text message. For example, hashtags, @@ -135,48 +136,21 @@ class MessageEntity(TelegramObject): """ - __slots__ = ( - "custom_emoji_id", - "date_time_format", - "language", - "length", - "offset", - "type", - "unix_time", - "url", - "user", - ) - - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - offset: int, - length: int, - url: str | None = None, - user: User | None = None, - language: str | None = None, - custom_emoji_id: str | None = None, - date_time_format: str | None = None, - unix_time: dtm.datetime | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.type: str = enum.get_member(constants.MessageEntityType, type, type) - self.offset: int = offset - self.length: int = length - # Optionals - self.url: str | None = url - self.user: User | None = user - self.language: str | None = language - self.custom_emoji_id: str | None = custom_emoji_id - self.date_time_format: str | None = date_time_format - self.unix_time: dtm.datetime | None = unix_time - - self._id_attrs = (self.type, self.offset, self.length) - - self._freeze() + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.MessageEntityType, value, value) + + # Required + type: str = tg_field(compare=True, converter=_type_converter) + offset: int = tg_field(compare=True) + length: int = tg_field(compare=True) + # Optionals + url: str | None = tg_field(default=None) + user: User | None = tg_field(default=None) + language: str | None = tg_field(default=None) + custom_emoji_id: str | None = tg_field(default=None) + date_time_format: str | None = tg_field(default=None) + unix_time: dtm.datetime | None = tg_field(default=None) @staticmethod def adjust_message_entities_to_utf_16(text: str, entities: _SEM) -> _SEM: @@ -388,60 +362,60 @@ async def prefix_message(update: Update, context: ContextTypes.DEFAULT_TYPE): return output_text, output_entities - ALL_TYPES: Final[list[str]] = list(constants.MessageEntityType) + ALL_TYPES: ClassVar[list[str]] = list(constants.MessageEntityType) """list[:obj:`str`]: A list of all available message entity types.""" - BLOCKQUOTE: Final[str] = constants.MessageEntityType.BLOCKQUOTE + BLOCKQUOTE: ClassVar[str] = constants.MessageEntityType.BLOCKQUOTE """:const:`telegram.constants.MessageEntityType.BLOCKQUOTE` .. versionadded:: 20.8 """ - BOLD: Final[str] = constants.MessageEntityType.BOLD + BOLD: ClassVar[str] = constants.MessageEntityType.BOLD """:const:`telegram.constants.MessageEntityType.BOLD`""" - BOT_COMMAND: Final[str] = constants.MessageEntityType.BOT_COMMAND + BOT_COMMAND: ClassVar[str] = constants.MessageEntityType.BOT_COMMAND """:const:`telegram.constants.MessageEntityType.BOT_COMMAND`""" - CASHTAG: Final[str] = constants.MessageEntityType.CASHTAG + CASHTAG: ClassVar[str] = constants.MessageEntityType.CASHTAG """:const:`telegram.constants.MessageEntityType.CASHTAG`""" - CODE: Final[str] = constants.MessageEntityType.CODE + CODE: ClassVar[str] = constants.MessageEntityType.CODE """:const:`telegram.constants.MessageEntityType.CODE`""" - CUSTOM_EMOJI: Final[str] = constants.MessageEntityType.CUSTOM_EMOJI + CUSTOM_EMOJI: ClassVar[str] = constants.MessageEntityType.CUSTOM_EMOJI """:const:`telegram.constants.MessageEntityType.CUSTOM_EMOJI` .. versionadded:: 20.0 """ - DATE_TIME: Final[str] = constants.MessageEntityType.DATE_TIME + DATE_TIME: ClassVar[str] = constants.MessageEntityType.DATE_TIME """:const:`telegram.constants.MessageEntityType.DATE_TIME` .. versionadded:: 22.7 """ - EMAIL: Final[str] = constants.MessageEntityType.EMAIL + EMAIL: ClassVar[str] = constants.MessageEntityType.EMAIL """:const:`telegram.constants.MessageEntityType.EMAIL`""" - EXPANDABLE_BLOCKQUOTE: Final[str] = constants.MessageEntityType.EXPANDABLE_BLOCKQUOTE + EXPANDABLE_BLOCKQUOTE: ClassVar[str] = constants.MessageEntityType.EXPANDABLE_BLOCKQUOTE """:const:`telegram.constants.MessageEntityType.EXPANDABLE_BLOCKQUOTE` .. versionadded:: 21.3 """ - HASHTAG: Final[str] = constants.MessageEntityType.HASHTAG + HASHTAG: ClassVar[str] = constants.MessageEntityType.HASHTAG """:const:`telegram.constants.MessageEntityType.HASHTAG`""" - ITALIC: Final[str] = constants.MessageEntityType.ITALIC + ITALIC: ClassVar[str] = constants.MessageEntityType.ITALIC """:const:`telegram.constants.MessageEntityType.ITALIC`""" - MENTION: Final[str] = constants.MessageEntityType.MENTION + MENTION: ClassVar[str] = constants.MessageEntityType.MENTION """:const:`telegram.constants.MessageEntityType.MENTION`""" - PHONE_NUMBER: Final[str] = constants.MessageEntityType.PHONE_NUMBER + PHONE_NUMBER: ClassVar[str] = constants.MessageEntityType.PHONE_NUMBER """:const:`telegram.constants.MessageEntityType.PHONE_NUMBER`""" - PRE: Final[str] = constants.MessageEntityType.PRE + PRE: ClassVar[str] = constants.MessageEntityType.PRE """:const:`telegram.constants.MessageEntityType.PRE`""" - SPOILER: Final[str] = constants.MessageEntityType.SPOILER + SPOILER: ClassVar[str] = constants.MessageEntityType.SPOILER """:const:`telegram.constants.MessageEntityType.SPOILER` .. versionadded:: 13.10 """ - STRIKETHROUGH: Final[str] = constants.MessageEntityType.STRIKETHROUGH + STRIKETHROUGH: ClassVar[str] = constants.MessageEntityType.STRIKETHROUGH """:const:`telegram.constants.MessageEntityType.STRIKETHROUGH`""" - TEXT_LINK: Final[str] = constants.MessageEntityType.TEXT_LINK + TEXT_LINK: ClassVar[str] = constants.MessageEntityType.TEXT_LINK """:const:`telegram.constants.MessageEntityType.TEXT_LINK`""" - TEXT_MENTION: Final[str] = constants.MessageEntityType.TEXT_MENTION + TEXT_MENTION: ClassVar[str] = constants.MessageEntityType.TEXT_MENTION """:const:`telegram.constants.MessageEntityType.TEXT_MENTION`""" - UNDERLINE: Final[str] = constants.MessageEntityType.UNDERLINE + UNDERLINE: ClassVar[str] = constants.MessageEntityType.UNDERLINE """:const:`telegram.constants.MessageEntityType.UNDERLINE`""" - URL: Final[str] = constants.MessageEntityType.URL + URL: ClassVar[str] = constants.MessageEntityType.URL """:const:`telegram.constants.MessageEntityType.URL`""" diff --git a/src/telegram/_messageid.py b/src/telegram/_messageid.py index 70420936b8b..6162e4fdf92 100644 --- a/src/telegram/_messageid.py +++ b/src/telegram/_messageid.py @@ -19,9 +19,10 @@ """This module contains an object that represents an instance of a Telegram MessageId.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class MessageId(TelegramObject): """This object represents a unique message identifier. @@ -41,12 +42,4 @@ class MessageId(TelegramObject): ``0`` and the relevant message will be unusable until it is actually sent. """ - __slots__ = ("message_id",) - - def __init__(self, message_id: int, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.message_id: int = message_id - - self._id_attrs = (self.message_id,) - - self._freeze() + message_id: int = tg_field(compare=True) diff --git a/src/telegram/_messageorigin.py b/src/telegram/_messageorigin.py index 63e22487721..e21cee3b44e 100644 --- a/src/telegram/_messageorigin.py +++ b/src/telegram/_messageorigin.py @@ -19,16 +19,17 @@ """This module contains the classes that represent Telegram MessageOigin.""" import datetime as dtm -from typing import ClassVar, Final +from typing import ClassVar from telegram import constants from telegram._chat import Chat from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class MessageOrigin(TelegramObject): """ Base class for telegram MessageOrigin object, it can be one of: @@ -58,11 +59,6 @@ class MessageOrigin(TelegramObject): |datetime_localization| """ - __slots__ = ( - "date", - "type", - ) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "type", { @@ -73,34 +69,25 @@ class MessageOrigin(TelegramObject): }, ) - USER: Final[str] = constants.MessageOriginType.USER + USER: ClassVar[str] = constants.MessageOriginType.USER """:const:`telegram.constants.MessageOriginType.USER`""" - HIDDEN_USER: Final[str] = constants.MessageOriginType.HIDDEN_USER + HIDDEN_USER: ClassVar[str] = constants.MessageOriginType.HIDDEN_USER """:const:`telegram.constants.MessageOriginType.HIDDEN_USER`""" - CHAT: Final[str] = constants.MessageOriginType.CHAT + CHAT: ClassVar[str] = constants.MessageOriginType.CHAT """:const:`telegram.constants.MessageOriginType.CHAT`""" - CHANNEL: Final[str] = constants.MessageOriginType.CHANNEL + CHANNEL: ClassVar[str] = constants.MessageOriginType.CHANNEL """:const:`telegram.constants.MessageOriginType.CHANNEL`""" - def __init__( - self, - type: str, # pylint: disable=W0622 - date: dtm.datetime, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required by all subclasses - self.type: str = enum.get_member(constants.MessageOriginType, type, type) - self.date: dtm.datetime = date - - self._id_attrs = ( - self.type, - self.date, - ) - self._freeze() + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.MessageOriginType, value, value) + # Required by all subclasses + type: str = tg_field(compare=True, converter=_type_converter) + date: dtm.datetime = tg_field(compare=True) + +@tg_dataclass() class MessageOriginUser(MessageOrigin): """ The message was originally sent by a known user. @@ -120,21 +107,13 @@ class MessageOriginUser(MessageOrigin): sender_user (:class:`telegram.User`): User that sent the message originally. """ - __slots__ = ("sender_user",) - - def __init__( - self, - date: dtm.datetime, - sender_user: User, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.USER, date=date, api_kwargs=api_kwargs) + # Attribute only (init=False) + type: str = tg_field(init=False, default=MessageOrigin.USER) - with self._unfrozen(): - self.sender_user: User = sender_user + sender_user: User = tg_field() +@tg_dataclass() class MessageOriginHiddenUser(MessageOrigin): """ The message was originally sent by an unknown user. @@ -154,21 +133,13 @@ class MessageOriginHiddenUser(MessageOrigin): sender_user_name (:obj:`str`): Name of the user that sent the message originally. """ - __slots__ = ("sender_user_name",) + # Attribute only (init=False) + type: str = tg_field(init=False, default=MessageOrigin.HIDDEN_USER) - def __init__( - self, - date: dtm.datetime, - sender_user_name: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.HIDDEN_USER, date=date, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.sender_user_name: str = sender_user_name + sender_user_name: str = tg_field() +@tg_dataclass() class MessageOriginChat(MessageOrigin): """ The message was originally sent on behalf of a chat to a group chat. @@ -192,26 +163,14 @@ class MessageOriginChat(MessageOrigin): administrator, original message author signature """ - __slots__ = ( - "author_signature", - "sender_chat", - ) - - def __init__( - self, - date: dtm.datetime, - sender_chat: Chat, - author_signature: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.CHAT, date=date, api_kwargs=api_kwargs) + # Attribute only (init=False) + type: str = tg_field(init=False, default=MessageOrigin.CHAT) - with self._unfrozen(): - self.sender_chat: Chat = sender_chat - self.author_signature: str | None = author_signature + sender_chat: Chat = tg_field() + author_signature: str | None = tg_field(default=None) +@tg_dataclass() class MessageOriginChannel(MessageOrigin): """ The message was originally sent to a channel chat. @@ -235,24 +194,9 @@ class MessageOriginChannel(MessageOrigin): author_signature (:obj:`str`): Optional. Signature of the original post author. """ - __slots__ = ( - "author_signature", - "chat", - "message_id", - ) + # Attribute only (init=False) + type: str = tg_field(init=False, default=MessageOrigin.CHANNEL) - def __init__( - self, - date: dtm.datetime, - chat: Chat, - message_id: int, - author_signature: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=self.CHANNEL, date=date, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.chat: Chat = chat - self.message_id: int = message_id - self.author_signature: str | None = author_signature + chat: Chat = tg_field() + message_id: int = tg_field() + author_signature: str | None = tg_field(default=None) diff --git a/src/telegram/_messagereactionupdated.py b/src/telegram/_messagereactionupdated.py index b2e0f16421a..2dbde411182 100644 --- a/src/telegram/_messagereactionupdated.py +++ b/src/telegram/_messagereactionupdated.py @@ -19,16 +19,16 @@ """This module contains an object that represents a Telegram MessageReaction Update.""" import datetime as dtm -from collections.abc import Sequence from telegram._chat import Chat from telegram._reaction import ReactionCount, ReactionType from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class MessageReactionCountUpdated(TelegramObject): """This class represents reaction changes on a message with anonymous reactions. @@ -55,33 +55,13 @@ class MessageReactionCountUpdated(TelegramObject): the message """ - __slots__ = ( - "chat", - "date", - "message_id", - "reactions", - ) - - def __init__( - self, - chat: Chat, - message_id: int, - date: dtm.datetime, - reactions: Sequence[ReactionCount], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.chat: Chat = chat - self.message_id: int = message_id - self.date: dtm.datetime = date - self.reactions: tuple[ReactionCount, ...] = parse_sequence_arg(reactions) - - self._id_attrs = (self.chat, self.message_id, self.date, self.reactions) - self._freeze() + chat: Chat = tg_field(compare=True) + message_id: int = tg_field(compare=True) + date: dtm.datetime = tg_field(compare=True) + reactions: tuple[ReactionCount, ...] = tg_field(compare=True, converter=parse_sequence_arg) +@tg_dataclass() class MessageReactionUpdated(TelegramObject): """This class represents a change of a reaction on a message performed by a user. @@ -120,45 +100,12 @@ class MessageReactionUpdated(TelegramObject): changed, if the user is anonymous. """ - __slots__ = ( - "actor_chat", - "chat", - "date", - "message_id", - "new_reaction", - "old_reaction", - "user", - ) - - def __init__( - self, - chat: Chat, - message_id: int, - date: dtm.datetime, - old_reaction: Sequence[ReactionType], - new_reaction: Sequence[ReactionType], - user: User | None = None, - actor_chat: Chat | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.chat: Chat = chat - self.message_id: int = message_id - self.date: dtm.datetime = date - self.old_reaction: tuple[ReactionType, ...] = parse_sequence_arg(old_reaction) - self.new_reaction: tuple[ReactionType, ...] = parse_sequence_arg(new_reaction) - - # Optional - self.user: User | None = user - self.actor_chat: Chat | None = actor_chat - - self._id_attrs = ( - self.chat, - self.message_id, - self.date, - self.old_reaction, - self.new_reaction, - ) - self._freeze() + # Required + chat: Chat = tg_field(compare=True) + message_id: int = tg_field(compare=True) + date: dtm.datetime = tg_field(compare=True) + old_reaction: tuple[ReactionType, ...] = tg_field(compare=True, converter=parse_sequence_arg) + new_reaction: tuple[ReactionType, ...] = tg_field(compare=True, converter=parse_sequence_arg) + # Optional + user: User | None = tg_field(default=None) + actor_chat: Chat | None = tg_field(default=None) diff --git a/src/telegram/_ownedgift.py b/src/telegram/_ownedgift.py index feee77383dd..a6c692970b8 100644 --- a/src/telegram/_ownedgift.py +++ b/src/telegram/_ownedgift.py @@ -19,8 +19,7 @@ """This module contains objects that represent owned gifts.""" import datetime as dtm -from collections.abc import Sequence -from typing import ClassVar, Final +from typing import ClassVar from telegram import constants from telegram._gifts import Gift @@ -30,10 +29,11 @@ from telegram._user import User from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.entities import parse_message_entities, parse_message_entity -from telegram._utils.types import JSONDict +@tg_dataclass() class OwnedGift(TelegramObject): """This object describes a gift received and owned by a user or a chat. Currently, it can be one of: @@ -53,11 +53,9 @@ class OwnedGift(TelegramObject): type (:obj:`str`): Type of the owned gift. """ - __slots__ = ("type",) - - REGULAR: Final[str] = constants.OwnedGiftType.REGULAR + REGULAR: ClassVar[str] = constants.OwnedGiftType.REGULAR """:const:`telegram.constants.OwnedGiftType.REGULAR`""" - UNIQUE: Final[str] = constants.OwnedGiftType.UNIQUE + UNIQUE: ClassVar[str] = constants.OwnedGiftType.UNIQUE """:const:`telegram.constants.OwnedGiftType.UNIQUE`""" __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( @@ -68,19 +66,14 @@ class OwnedGift(TelegramObject): }, ) - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.OwnedGiftType, type, type) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.OwnedGiftType, value, value) - self._id_attrs = (self.type,) - self._freeze() + type: str = tg_field(compare=True, converter=_type_converter) +@tg_dataclass() class OwnedGifts(TelegramObject): """Contains the list of gifts received and owned by a user or a chat. @@ -102,30 +95,12 @@ class OwnedGifts(TelegramObject): then there are no more results. """ - __slots__ = ( - "gifts", - "next_offset", - "total_count", - ) - - def __init__( - self, - total_count: int, - gifts: Sequence[OwnedGift], - next_offset: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.total_count: int = total_count - self.gifts: tuple[OwnedGift, ...] = parse_sequence_arg(gifts) - self.next_offset: str | None = next_offset - - self._id_attrs = (self.total_count, self.gifts) - - self._freeze() + total_count: int = tg_field(compare=True) + gifts: tuple[OwnedGift, ...] = tg_field(compare=True, converter=parse_sequence_arg) + next_offset: str | None = tg_field(default=None) +@tg_dataclass() class OwnedGiftRegular(OwnedGift): """Describes a regular gift owned by a user or a chat. @@ -201,61 +176,26 @@ class OwnedGiftRegular(OwnedGift): """ - __slots__ = ( - "can_be_upgraded", - "convert_star_count", - "entities", - "gift", - "is_private", - "is_saved", - "is_upgrade_separate", - "owned_gift_id", - "prepaid_upgrade_star_count", - "send_date", - "sender_user", - "text", - "unique_gift_number", - "was_refunded", - ) - - def __init__( - self, - gift: Gift, - send_date: dtm.datetime, - owned_gift_id: str | None = None, - sender_user: User | None = None, - text: str | None = None, - entities: Sequence[MessageEntity] | None = None, - is_private: bool | None = None, - is_saved: bool | None = None, - can_be_upgraded: bool | None = None, - was_refunded: bool | None = None, - convert_star_count: int | None = None, - prepaid_upgrade_star_count: int | None = None, - is_upgrade_separate: bool | None = None, - unique_gift_number: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=OwnedGift.REGULAR, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.gift: Gift = gift - self.send_date: dtm.datetime = send_date - self.owned_gift_id: str | None = owned_gift_id - self.sender_user: User | None = sender_user - self.text: str | None = text - self.entities: tuple[MessageEntity, ...] = parse_sequence_arg(entities) - self.is_private: bool | None = is_private - self.is_saved: bool | None = is_saved - self.can_be_upgraded: bool | None = can_be_upgraded - self.was_refunded: bool | None = was_refunded - self.convert_star_count: int | None = convert_star_count - self.prepaid_upgrade_star_count: int | None = prepaid_upgrade_star_count - self.is_upgrade_separate: bool | None = is_upgrade_separate - self.unique_gift_number: int | None = unique_gift_number - - self._id_attrs = (self.type, self.gift, self.send_date) + # Attribute only (init=False) + type: str = tg_field(init=False, default=OwnedGift.REGULAR) + + # Required + gift: Gift = tg_field(compare=True) + send_date: dtm.datetime = tg_field(compare=True) + + # Optional + owned_gift_id: str | None = tg_field(default=None) + sender_user: User | None = tg_field(default=None) + text: str | None = tg_field(default=None) + entities: tuple[MessageEntity, ...] = tg_field(default=None, converter=parse_sequence_arg) + is_private: bool | None = tg_field(default=None) + is_saved: bool | None = tg_field(default=None) + can_be_upgraded: bool | None = tg_field(default=None) + was_refunded: bool | None = tg_field(default=None) + convert_star_count: int | None = tg_field(default=None) + prepaid_upgrade_star_count: int | None = tg_field(default=None) + is_upgrade_separate: bool | None = tg_field(default=None) + unique_gift_number: int | None = tg_field(default=None) def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`text` @@ -312,6 +252,7 @@ def parse_entities(self, types: list[str] | None = None) -> dict[MessageEntity, return parse_message_entities(self.text, self.entities, types) +@tg_dataclass() class OwnedGiftUnique(OwnedGift): """ Describes a unique gift received and owned by a user or a chat. @@ -359,40 +300,17 @@ class OwnedGiftUnique(OwnedGift): .. versionadded:: 22.3 """ - __slots__ = ( - "can_be_transferred", - "gift", - "is_saved", - "next_transfer_date", - "owned_gift_id", - "send_date", - "sender_user", - "transfer_star_count", - ) + # Attribute only (init=False) + type: str = tg_field(init=False, default=OwnedGift.UNIQUE) + + # Required + gift: UniqueGift = tg_field(compare=True) + send_date: dtm.datetime = tg_field(compare=True) - def __init__( - self, - gift: UniqueGift, - send_date: dtm.datetime, - owned_gift_id: str | None = None, - sender_user: User | None = None, - is_saved: bool | None = None, - can_be_transferred: bool | None = None, - transfer_star_count: int | None = None, - next_transfer_date: dtm.datetime | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=OwnedGift.UNIQUE, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.gift: UniqueGift = gift - self.send_date: dtm.datetime = send_date - self.owned_gift_id: str | None = owned_gift_id - self.sender_user: User | None = sender_user - self.is_saved: bool | None = is_saved - self.can_be_transferred: bool | None = can_be_transferred - self.transfer_star_count: int | None = transfer_star_count - self.next_transfer_date: dtm.datetime | None = next_transfer_date - - self._id_attrs = (self.type, self.gift, self.send_date) + # Optional + owned_gift_id: str | None = tg_field(default=None) + sender_user: User | None = tg_field(default=None) + is_saved: bool | None = tg_field(default=None) + can_be_transferred: bool | None = tg_field(default=None) + transfer_star_count: int | None = tg_field(default=None) + next_transfer_date: dtm.datetime | None = tg_field(default=None) diff --git a/src/telegram/_paidmedia.py b/src/telegram/_paidmedia.py index 6b9ec318fb4..00dcf6144ec 100644 --- a/src/telegram/_paidmedia.py +++ b/src/telegram/_paidmedia.py @@ -19,8 +19,7 @@ """This module contains objects that represent paid media in Telegram.""" import datetime as dtm -from collections.abc import Sequence -from typing import TYPE_CHECKING, ClassVar, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._files.livephoto import LivePhoto @@ -31,14 +30,15 @@ parse_sequence_arg, to_timedelta, ) +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod if TYPE_CHECKING: from telegram._files.photosize import PhotoSize from telegram._user import User +@tg_dataclass() class PaidMedia(TelegramObject): """Describes the paid media added to a message. Currently, it can be one of: @@ -58,8 +58,6 @@ class PaidMedia(TelegramObject): type (:obj:`str`): Type of the paid media. """ - __slots__ = ("type",) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "type", { @@ -70,28 +68,23 @@ class PaidMedia(TelegramObject): }, ) - PREVIEW: Final[str] = constants.PaidMediaType.PREVIEW + PREVIEW: ClassVar[str] = constants.PaidMediaType.PREVIEW """:const:`telegram.constants.PaidMediaType.PREVIEW`""" - PHOTO: Final[str] = constants.PaidMediaType.PHOTO + PHOTO: ClassVar[str] = constants.PaidMediaType.PHOTO """:const:`telegram.constants.PaidMediaType.PHOTO`""" - VIDEO: Final[str] = constants.PaidMediaType.VIDEO + VIDEO: ClassVar[str] = constants.PaidMediaType.VIDEO """:const:`telegram.constants.PaidMediaType.VIDEO`""" - LIVE_PHOTO: Final[str] = constants.PaidMediaType.LIVE_PHOTO + LIVE_PHOTO: ClassVar[str] = constants.PaidMediaType.LIVE_PHOTO """:const:`telegram.constants.PaidMediaType.LIVE_PHOTO`""" - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.PaidMediaType, type, type) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.PaidMediaType, value, value) - self._id_attrs = (self.type,) - self._freeze() + type: str = tg_field(compare=True, converter=_type_converter) +@tg_dataclass() class PaidMediaPreview(PaidMedia): """The paid media isn't available before the payment. @@ -126,30 +119,21 @@ class PaidMediaPreview(PaidMedia): |time-period-int-deprecated| """ - __slots__ = ("_duration", "height", "width") - - def __init__( - self, - width: int | None = None, - height: int | None = None, - duration: TimePeriod | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=PaidMedia.PREVIEW, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.width: int | None = width - self.height: int | None = height - self._duration: dtm.timedelta | None = to_timedelta(duration) + # Attribute only (init=False) + type: str = tg_field(init=False, default=PaidMedia.PREVIEW) - self._id_attrs = (self.type, self.width, self.height, self._duration) + width: int | None = tg_field(compare=True, default=None) + height: int | None = tg_field(compare=True, default=None) + _duration: dtm.timedelta | None = tg_field( + compare=True, alias="duration", default=None, converter=to_timedelta + ) @property def duration(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._duration, attribute="duration") +@tg_dataclass() class PaidMediaPhoto(PaidMedia): """ The paid media is a photo. @@ -168,22 +152,13 @@ class PaidMediaPhoto(PaidMedia): photo (tuple[:class:`telegram.PhotoSize`]): The photo. """ - __slots__ = ("photo",) - - def __init__( - self, - photo: Sequence["PhotoSize"], - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=PaidMedia.PHOTO, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.photo: tuple[PhotoSize, ...] = parse_sequence_arg(photo) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=PaidMedia.PHOTO) - self._id_attrs = (self.type, self.photo) + photo: tuple["PhotoSize", ...] = tg_field(compare=True, converter=parse_sequence_arg) +@tg_dataclass() class PaidMediaVideo(PaidMedia): """ The paid media is a video. @@ -202,22 +177,13 @@ class PaidMediaVideo(PaidMedia): video (:class:`telegram.Video`): The video. """ - __slots__ = ("video",) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=PaidMedia.VIDEO) - def __init__( - self, - video: Video, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=PaidMedia.VIDEO, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.video: Video = video - - self._id_attrs = (self.type, self.video) + video: Video = tg_field(compare=True) +@tg_dataclass() class PaidMediaLivePhoto(PaidMedia): """ The paid media is a live photo. @@ -237,21 +203,13 @@ class PaidMediaLivePhoto(PaidMedia): """ - __slots__ = ("live_photo",) - - def __init__( - self, - live_photo: LivePhoto, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=PaidMedia.LIVE_PHOTO, api_kwargs=api_kwargs) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=PaidMedia.VIDEO) - with self._unfrozen(): - self.live_photo: LivePhoto = live_photo - self._id_attrs = (self.type, self.live_photo) + live_photo: LivePhoto = tg_field(compare=True) +@tg_dataclass() class PaidMediaInfo(TelegramObject): """ Describes the paid media added to a message. @@ -272,23 +230,11 @@ class PaidMediaInfo(TelegramObject): paid_media (tuple[:class:`telegram.PaidMedia`]): Information about the paid media. """ - __slots__ = ("paid_media", "star_count") - - def __init__( - self, - star_count: int, - paid_media: Sequence[PaidMedia], - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.star_count: int = star_count - self.paid_media: tuple[PaidMedia, ...] = parse_sequence_arg(paid_media) - - self._id_attrs = (self.star_count, self.paid_media) - self._freeze() + star_count: int = tg_field(compare=True) + paid_media: tuple[PaidMedia, ...] = tg_field(compare=True, converter=parse_sequence_arg) +@tg_dataclass() class PaidMediaPurchased(TelegramObject): """This object contains information about a paid media purchase. @@ -309,18 +255,5 @@ class PaidMediaPurchased(TelegramObject): paid_media_payload (:obj:`str`): Bot-specified paid media payload. """ - __slots__ = ("from_user", "paid_media_payload") - - def __init__( - self, - from_user: "User", - paid_media_payload: str, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.from_user: User = from_user - self.paid_media_payload: str = paid_media_payload - - self._id_attrs = (self.from_user, self.paid_media_payload) - self._freeze() + from_user: "User" = tg_field(compare=True) + paid_media_payload: str = tg_field(compare=True) diff --git a/src/telegram/_paidmessagepricechanged.py b/src/telegram/_paidmessagepricechanged.py index d5fa78692ee..3193e1f63b4 100644 --- a/src/telegram/_paidmessagepricechanged.py +++ b/src/telegram/_paidmessagepricechanged.py @@ -19,9 +19,10 @@ """This module contains an object that describes a price change of a paid message.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class PaidMessagePriceChanged(TelegramObject): """Describes a service message about a change in the price of paid messages within a chat. @@ -39,16 +40,4 @@ class PaidMessagePriceChanged(TelegramObject): non-administrator users of the supergroup chat for each sent message """ - __slots__ = ("paid_message_star_count",) - - def __init__( - self, - paid_message_star_count: int, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.paid_message_star_count: int = paid_message_star_count - - self._id_attrs = (self.paid_message_star_count,) - self._freeze() + paid_message_star_count: int = tg_field(compare=True) diff --git a/src/telegram/_poll.py b/src/telegram/_poll.py index cd864a4b623..e03a0c6cd0c 100644 --- a/src/telegram/_poll.py +++ b/src/telegram/_poll.py @@ -19,8 +19,7 @@ """This module contains an object that represents a Telegram Poll.""" import datetime as dtm -from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar, Final from telegram import constants from telegram._chat import Chat @@ -41,16 +40,18 @@ parse_sequence_arg, to_timedelta, ) +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.entities import parse_message_entities, parse_message_entity -from telegram._utils.types import JSONDict, ODVInput, TimePeriod +from telegram._utils.types import ODVInput if TYPE_CHECKING: from telegram._files.inputmedia import InputPollOptionMedia from telegram._message import MaybeInaccessibleMessage +@tg_dataclass() class PollMedia(TelegramObject): """ At most one of the optional fields can be present in any given object. @@ -97,58 +98,20 @@ class PollMedia(TelegramObject): video (:class:`telegram.Video`): Optional. Media is a video, information about the video """ - __slots__ = ( - "animation", - "audio", - "document", - "live_photo", - "location", - "photo", - "sticker", - "venue", - "video", + animation: Animation | None = tg_field(compare=True, default=None) + audio: Audio | None = tg_field(compare=True, default=None) + document: Document | None = tg_field(compare=True, default=None) + live_photo: LivePhoto | None = tg_field(compare=True, default=None) + location: Location | None = tg_field(compare=True, default=None) + photo: tuple[PhotoSize, ...] = tg_field( + compare=True, default=None, converter=parse_sequence_arg ) - - def __init__( - self, - animation: Animation | None = None, - audio: Audio | None = None, - document: Document | None = None, - live_photo: LivePhoto | None = None, - location: Location | None = None, - photo: Sequence[PhotoSize] | None = None, - sticker: Sticker | None = None, - venue: Venue | None = None, - video: Video | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.animation: Animation | None = animation - self.audio: Audio | None = audio - self.document: Document | None = document - self.live_photo: LivePhoto | None = live_photo - self.location: Location | None = location - self.photo: tuple[PhotoSize, ...] = parse_sequence_arg(photo) - self.sticker: Sticker | None = sticker - self.venue: Venue | None = venue - self.video: Video | None = video - - self._id_attrs = ( - self.animation, - self.audio, - self.document, - self.live_photo, - self.location, - self.photo, - self.sticker, - self.venue, - self.video, - ) - - self._freeze() + sticker: Sticker | None = tg_field(compare=True, default=None) + venue: Venue | None = tg_field(compare=True, default=None) + video: Video | None = tg_field(compare=True, default=None) +@tg_dataclass() class InputPollOption(TelegramObject): """ This object contains information about one answer option in a poll to be sent. @@ -189,28 +152,13 @@ class InputPollOption(TelegramObject): .. versionadded:: 22.8 """ - __slots__ = ("media", "text", "text_entities", "text_parse_mode") - - def __init__( - self, - text: str, - text_parse_mode: ODVInput[str] = DEFAULT_NONE, - text_entities: Sequence[MessageEntity] | None = None, - media: "InputPollOptionMedia | None" = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.text: str = text - self.text_parse_mode: ODVInput[str] = text_parse_mode - self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) - self.media: InputPollOptionMedia | None = media - - self._id_attrs = (self.text,) - - self._freeze() + text: str = tg_field(compare=True) + text_parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + text_entities: tuple[MessageEntity, ...] = tg_field(default=None, converter=parse_sequence_arg) + media: "InputPollOptionMedia | None" = tg_field(default=None) +@tg_dataclass() class PollOption(TelegramObject): """ This object contains information about one answer option in a poll. @@ -285,44 +233,17 @@ class PollOption(TelegramObject): .. versionadded:: 22.8 """ - __slots__ = ( - "added_by_chat", - "added_by_user", - "addition_date", - "media", - "persistent_id", - "text", - "text_entities", - "voter_count", - ) + # Required + text: str = tg_field(compare=True) + voter_count: int = tg_field(compare=True) + persistent_id: str = tg_field(compare=True) - def __init__( - self, - text: str, - voter_count: int, - persistent_id: str, - text_entities: Sequence[MessageEntity] | None = None, - added_by_user: User | None = None, - added_by_chat: Chat | None = None, - addition_date: dtm.datetime | None = None, - media: PollMedia | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.text: str = text - self.voter_count: int = voter_count - self.added_by_user: User | None = added_by_user - self.added_by_chat: Chat | None = added_by_chat - self.addition_date: dtm.datetime | None = addition_date - self.persistent_id: str = persistent_id - self.media: PollMedia | None = media - - self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) - - self._id_attrs = (self.text, self.voter_count, self.persistent_id) - - self._freeze() + # Optional + text_entities: tuple[MessageEntity, ...] = tg_field(default=None, converter=parse_sequence_arg) + added_by_user: User | None = tg_field(default=None) + added_by_chat: Chat | None = tg_field(default=None) + addition_date: dtm.datetime | None = tg_field(default=None) + media: PollMedia | None = tg_field(default=None) def parse_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`text` @@ -380,6 +301,7 @@ def parse_entities(self, types: list[str] | None = None) -> dict[MessageEntity, """ +@tg_dataclass() class PollAnswer(TelegramObject): """ This object represents an answer of a user in a non-anonymous poll. @@ -440,35 +362,16 @@ class PollAnswer(TelegramObject): .. versionadded:: 20.5 """ - __slots__ = ("option_ids", "option_persistent_ids", "poll_id", "user", "voter_chat") - - def __init__( - self, - poll_id: str, - option_ids: Sequence[int], - option_persistent_ids: Sequence[str], - user: User | None = None, - voter_chat: Chat | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.poll_id: str = poll_id - self.voter_chat: Chat | None = voter_chat - self.option_ids: tuple[int, ...] = parse_sequence_arg(option_ids) - self.user: User | None = user - self.option_persistent_ids: tuple[str, ...] = parse_sequence_arg(option_persistent_ids) - - self._id_attrs = ( - self.poll_id, - self.option_ids, - self.user, - self.voter_chat, - ) - - self._freeze() + # Required + poll_id: str = tg_field(compare=True) + option_ids: tuple[int, ...] = tg_field(compare=True, converter=parse_sequence_arg) + option_persistent_ids: tuple[str, ...] = tg_field(converter=parse_sequence_arg) + # Optional + user: User | None = tg_field(compare=True, default=None) + voter_chat: Chat | None = tg_field(compare=True, default=None) +@tg_dataclass() class PollOptionAdded(TelegramObject): """ Describes a service message about an option added to a poll. @@ -499,29 +402,14 @@ class PollOptionAdded(TelegramObject): entities that appear in the :paramref:`option_text`. """ - __slots__ = ("option_persistent_id", "option_text", "option_text_entities", "poll_message") - - def __init__( - self, - option_persistent_id: str, - option_text: str, - poll_message: "MaybeInaccessibleMessage | None" = None, - option_text_entities: Sequence[MessageEntity] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.option_persistent_id: str = option_persistent_id - self.option_text: str = option_text - self.poll_message: MaybeInaccessibleMessage | None = poll_message - - self.option_text_entities: tuple[MessageEntity, ...] = parse_sequence_arg( - option_text_entities - ) - - self._id_attrs = (self.option_persistent_id, self.option_text) - - self._freeze() + # Required + option_persistent_id: str = tg_field(compare=True) + option_text: str = tg_field(compare=True) + # Optional + poll_message: "MaybeInaccessibleMessage | None" = tg_field(default=None) + option_text_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) def parse_option_text_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`option_text` @@ -566,6 +454,7 @@ def parse_option_text_entities( return parse_message_entities(self.option_text, self.option_text_entities, types) +@tg_dataclass() class PollOptionDeleted(TelegramObject): """ Describes a service message about an option deleted from a poll. @@ -596,29 +485,14 @@ class PollOptionDeleted(TelegramObject): entities that appear in the option_text. """ - __slots__ = ("option_persistent_id", "option_text", "option_text_entities", "poll_message") - - def __init__( - self, - option_persistent_id: str, - option_text: str, - poll_message: "MaybeInaccessibleMessage | None" = None, - option_text_entities: Sequence[MessageEntity] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.option_persistent_id: str = option_persistent_id - self.option_text: str = option_text - self.poll_message: MaybeInaccessibleMessage | None = poll_message - - self.option_text_entities: tuple[MessageEntity, ...] = parse_sequence_arg( - option_text_entities - ) - - self._id_attrs = (self.option_persistent_id, self.option_text) - - self._freeze() + # Required + option_persistent_id: str = tg_field(compare=True) + option_text: str = tg_field(compare=True) + # Optional + poll_message: "MaybeInaccessibleMessage | None" = tg_field(default=None) + option_text_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) def parse_option_text_entity(self, entity: MessageEntity) -> str: """Returns the text in :attr:`option_text` @@ -663,6 +537,7 @@ def parse_option_text_entities( return parse_message_entities(self.option_text, self.option_text_entities, types) +@tg_dataclass() class Poll(TelegramObject): """ This object contains information about a poll. @@ -829,87 +704,41 @@ class Poll(TelegramObject): """ - __slots__ = ( - "_open_period", - "allows_multiple_answers", - "allows_revoting", - "close_date", - "correct_option_ids", - "country_codes", - "description", - "description_entities", - "explanation", - "explanation_entities", - "explanation_media", - "id", - "is_anonymous", - "is_closed", - "media", - "members_only", - "options", - "question", - "question_entities", - "total_voter_count", - "type", + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.PollType, value, value) + + # Required + id: str = tg_field(compare=True) + question: str = tg_field() + options: tuple[PollOption, ...] = tg_field(converter=parse_sequence_arg) + total_voter_count: int = tg_field() + is_closed: bool = tg_field() + is_anonymous: bool = tg_field() + type: str = tg_field(converter=_type_converter) + allows_multiple_answers: bool = tg_field() + allows_revoting: bool = tg_field() + members_only: bool = tg_field() + # Optional + explanation: str | None = tg_field(default=None) + explanation_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - question: str, - options: Sequence[PollOption], - total_voter_count: int, - is_closed: bool, - is_anonymous: bool, - type: str, # pylint: disable=redefined-builtin - allows_multiple_answers: bool, - allows_revoting: bool, - members_only: bool, - explanation: str | None = None, - explanation_entities: Sequence[MessageEntity] | None = None, - open_period: TimePeriod | None = None, - close_date: dtm.datetime | None = None, - question_entities: Sequence[MessageEntity] | None = None, - correct_option_ids: Sequence[int] | None = None, - description: str | None = None, - description_entities: Sequence[MessageEntity] | None = None, - country_codes: Sequence[str] | None = None, - media: PollMedia | None = None, - explanation_media: PollMedia | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.id: str = id - self.question: str = question - self.options: tuple[PollOption, ...] = parse_sequence_arg(options) - self.total_voter_count: int = total_voter_count - self.is_closed: bool = is_closed - self.is_anonymous: bool = is_anonymous - self.type: str = enum.get_member(constants.PollType, type, type) - self.allows_multiple_answers: bool = allows_multiple_answers - self.allows_revoting: bool = allows_revoting - self.members_only: bool = members_only - - self.correct_option_ids: tuple[int, ...] = parse_sequence_arg(correct_option_ids) - self.description: str | None = description - self.description_entities: tuple[MessageEntity, ...] = parse_sequence_arg( - description_entities - ) - self.explanation: str | None = explanation - self.explanation_entities: tuple[MessageEntity, ...] = parse_sequence_arg( - explanation_entities - ) - self._open_period: dtm.timedelta | None = to_timedelta(open_period) - self.close_date: dtm.datetime | None = close_date - self.question_entities: tuple[MessageEntity, ...] = parse_sequence_arg(question_entities) - self.country_codes: tuple[str, ...] = parse_sequence_arg(country_codes) - self.media: PollMedia | None = media - self.explanation_media: PollMedia | None = explanation_media - - self._id_attrs = (self.id,) - - self._freeze() + _open_period: dtm.timedelta | None = tg_field( + default=None, alias="open_period", converter=to_timedelta + ) + close_date: dtm.datetime | None = tg_field(default=None) + question_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + correct_option_ids: tuple[int, ...] = tg_field(default=None, converter=parse_sequence_arg) + description: str | None = tg_field(default=None) + description_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + country_codes: tuple[str, ...] = tg_field(default=None, converter=parse_sequence_arg) + media: PollMedia | None = tg_field(default=None) + explanation_media: PollMedia | None = tg_field(default=None) @property def open_period(self) -> int | dtm.timedelta | None: @@ -1073,66 +902,66 @@ def parse_description_entities( return parse_message_entities(self.description, self.description_entities, types) - REGULAR: Final[str] = constants.PollType.REGULAR + REGULAR: ClassVar[str] = constants.PollType.REGULAR """:const:`telegram.constants.PollType.REGULAR`""" - QUIZ: Final[str] = constants.PollType.QUIZ + QUIZ: ClassVar[str] = constants.PollType.QUIZ """:const:`telegram.constants.PollType.QUIZ`""" - MAX_EXPLANATION_LENGTH: Final[int] = constants.PollLimit.MAX_EXPLANATION_LENGTH + MAX_EXPLANATION_LENGTH: ClassVar[int] = constants.PollLimit.MAX_EXPLANATION_LENGTH """:const:`telegram.constants.PollLimit.MAX_EXPLANATION_LENGTH` .. versionadded:: 20.0 """ - MAX_EXPLANATION_LINE_FEEDS: Final[int] = constants.PollLimit.MAX_EXPLANATION_LINE_FEEDS + MAX_EXPLANATION_LINE_FEEDS: ClassVar[int] = constants.PollLimit.MAX_EXPLANATION_LINE_FEEDS """:const:`telegram.constants.PollLimit.MAX_EXPLANATION_LINE_FEEDS` .. versionadded:: 20.0 """ - MIN_OPEN_PERIOD: Final[int] = constants.PollLimit.MIN_OPEN_PERIOD + MIN_OPEN_PERIOD: ClassVar[int] = constants.PollLimit.MIN_OPEN_PERIOD """:const:`telegram.constants.PollLimit.MIN_OPEN_PERIOD` .. versionadded:: 20.0 """ - MAX_OPEN_PERIOD: Final[int] = constants.PollLimit.MAX_OPEN_PERIOD + MAX_OPEN_PERIOD: ClassVar[int] = constants.PollLimit.MAX_OPEN_PERIOD """:const:`telegram.constants.PollLimit.MAX_OPEN_PERIOD` .. versionadded:: 20.0 """ - MIN_QUESTION_LENGTH: Final[int] = constants.PollLimit.MIN_QUESTION_LENGTH + MIN_QUESTION_LENGTH: ClassVar[int] = constants.PollLimit.MIN_QUESTION_LENGTH """:const:`telegram.constants.PollLimit.MIN_QUESTION_LENGTH` .. versionadded:: 20.0 """ - MAX_QUESTION_LENGTH: Final[int] = constants.PollLimit.MAX_QUESTION_LENGTH + MAX_QUESTION_LENGTH: ClassVar[int] = constants.PollLimit.MAX_QUESTION_LENGTH """:const:`telegram.constants.PollLimit.MAX_QUESTION_LENGTH` .. versionadded:: 20.0 """ - MIN_OPTION_LENGTH: Final[int] = constants.PollLimit.MIN_OPTION_LENGTH + MIN_OPTION_LENGTH: ClassVar[int] = constants.PollLimit.MIN_OPTION_LENGTH """:const:`telegram.constants.PollLimit.MIN_OPTION_LENGTH` .. versionadded:: 20.0 """ - MAX_OPTION_LENGTH: Final[int] = constants.PollLimit.MAX_OPTION_LENGTH + MAX_OPTION_LENGTH: ClassVar[int] = constants.PollLimit.MAX_OPTION_LENGTH """:const:`telegram.constants.PollLimit.MAX_OPTION_LENGTH` .. versionadded:: 20.0 """ - MIN_OPTION_NUMBER: Final[int] = constants.PollLimit.MIN_OPTION_NUMBER + MIN_OPTION_NUMBER: ClassVar[int] = constants.PollLimit.MIN_OPTION_NUMBER """:const:`telegram.constants.PollLimit.MIN_OPTION_NUMBER` .. versionadded:: 20.0 """ - MAX_OPTION_NUMBER: Final[int] = constants.PollLimit.MAX_OPTION_NUMBER + MAX_OPTION_NUMBER: ClassVar[int] = constants.PollLimit.MAX_OPTION_NUMBER """:const:`telegram.constants.PollLimit.MAX_OPTION_NUMBER` .. versionadded:: 20.0 """ - MAX_DESCRIPTION_CHARACTERS: Final[int] = constants.PollLimit.MAX_DESCRIPTION_CHARACTERS + MAX_DESCRIPTION_CHARACTERS: ClassVar[int] = constants.PollLimit.MAX_DESCRIPTION_CHARACTERS """:const:`telegram.constants.PollLimit.MAX_DESCRIPTION_CHARACTERS` .. versionadded:: 22.8 """ - MIN_MEMBERSHIP_HOURS: Final[int] = constants.PollLimit.MIN_MEMBERSHIP_HOURS + MIN_MEMBERSHIP_HOURS: ClassVar[int] = constants.PollLimit.MIN_MEMBERSHIP_HOURS """:const:`telegram.constants.PollLimit.MIN_MEMBERSHIP_HOURS` .. versionadded:: 22.8 diff --git a/src/telegram/_preparedkeyboardbutton.py b/src/telegram/_preparedkeyboardbutton.py index cd83f6b0dd7..d570e8770eb 100644 --- a/src/telegram/_preparedkeyboardbutton.py +++ b/src/telegram/_preparedkeyboardbutton.py @@ -19,11 +19,10 @@ """This module contains an object that represents a Telegram PreparedKeyboardButton.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import ( - JSONDict, -) +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class PreparedKeyboardButton(TelegramObject): """ Describes a keyboard button to be used by a user of a Mini App. @@ -40,15 +39,4 @@ class PreparedKeyboardButton(TelegramObject): id (:obj:`str`): Unique identifier of the keyboard button. """ - __slots__ = ("id",) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.id: str = id - self._id_attrs = (self.id,) - self._freeze() + id: str = tg_field(compare=True) diff --git a/src/telegram/_proximityalerttriggered.py b/src/telegram/_proximityalerttriggered.py index 4907172ab71..0ca917c76df 100644 --- a/src/telegram/_proximityalerttriggered.py +++ b/src/telegram/_proximityalerttriggered.py @@ -20,9 +20,10 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ProximityAlertTriggered(TelegramObject): """ This object represents the content of a service message, sent whenever a user in the chat @@ -43,21 +44,6 @@ class ProximityAlertTriggered(TelegramObject): """ - __slots__ = ("distance", "traveler", "watcher") - - def __init__( - self, - traveler: User, - watcher: User, - distance: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.traveler: User = traveler - self.watcher: User = watcher - self.distance: int = distance - - self._id_attrs = (self.traveler, self.watcher, self.distance) - - self._freeze() + traveler: User = tg_field(compare=True) + watcher: User = tg_field(compare=True) + distance: int = tg_field(compare=True) diff --git a/src/telegram/_reaction.py b/src/telegram/_reaction.py index 1ae97580e62..7bf39c4ef7f 100644 --- a/src/telegram/_reaction.py +++ b/src/telegram/_reaction.py @@ -16,17 +16,17 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=redefined-builtin """This module contains objects that represents a Telegram ReactionType.""" -from typing import ClassVar, Final, Literal +from typing import ClassVar, Literal from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ReactionType(TelegramObject): """Base class for Telegram ReactionType Objects. There exist :class:`telegram.ReactionTypeEmoji`, :class:`telegram.ReactionTypeCustomEmoji` @@ -48,8 +48,6 @@ class ReactionType(TelegramObject): """ - __slots__ = ("type",) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "type", { @@ -59,29 +57,27 @@ class ReactionType(TelegramObject): }, ) - EMOJI: Final[constants.ReactionType] = constants.ReactionType.EMOJI + EMOJI: ClassVar[constants.ReactionType] = constants.ReactionType.EMOJI """:const:`telegram.constants.ReactionType.EMOJI`""" - CUSTOM_EMOJI: Final[constants.ReactionType] = constants.ReactionType.CUSTOM_EMOJI + CUSTOM_EMOJI: ClassVar[constants.ReactionType] = constants.ReactionType.CUSTOM_EMOJI """:const:`telegram.constants.ReactionType.CUSTOM_EMOJI`""" - PAID: Final[constants.ReactionType] = constants.ReactionType.PAID + PAID: ClassVar[constants.ReactionType] = constants.ReactionType.PAID """:const:`telegram.constants.ReactionType.PAID` .. versionadded:: 21.5 """ - def __init__( - self, - type: Literal["emoji", "custom_emoji", "paid"] | constants.ReactionType, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required by all subclasses - self.type: str = enum.get_member(constants.ReactionType, type, type) + @staticmethod + def _type_converter( + value: Literal["emoji", "custom_emoji", "paid"] | constants.ReactionType, + ) -> str: + return enum.get_member(constants.MessageOriginType, value, value) - self._freeze() + # Required by all subclasses + type: str = tg_field(converter=_type_converter) +@tg_dataclass() class ReactionTypeEmoji(ReactionType): """ Represents a reaction with a normal emoji. @@ -102,21 +98,13 @@ class ReactionTypeEmoji(ReactionType): :const:`telegram.constants.ReactionEmoji`. """ - __slots__ = ("emoji",) - - def __init__( - self, - emoji: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=ReactionType.EMOJI, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.emoji: str = emoji - self._id_attrs = (self.emoji,) + # Attribute only (init=False) + type: str = tg_field(init=False, default=ReactionType.EMOJI) + # Required + emoji: str = tg_field(compare=True) +@tg_dataclass() class ReactionTypeCustomEmoji(ReactionType): """ Represents a reaction with a custom emoji. @@ -136,21 +124,13 @@ class ReactionTypeCustomEmoji(ReactionType): """ - __slots__ = ("custom_emoji_id",) - - def __init__( - self, - custom_emoji_id: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=ReactionType.CUSTOM_EMOJI, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.custom_emoji_id: str = custom_emoji_id - self._id_attrs = (self.custom_emoji_id,) + # Attribute only (init=False) + type: str = tg_field(init=False, default=ReactionType.CUSTOM_EMOJI) + # Required + custom_emoji_id: str = tg_field(compare=True) +@tg_dataclass() class ReactionTypePaid(ReactionType): """ The reaction is paid. @@ -162,13 +142,11 @@ class ReactionTypePaid(ReactionType): always :tg-const:`telegram.ReactionType.PAID`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None): - super().__init__(type=ReactionType.PAID, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=ReactionType.PAID) +@tg_dataclass() class ReactionCount(TelegramObject): """This class represents a reaction added to a message along with the number of times it was added. @@ -187,25 +165,5 @@ class ReactionCount(TelegramObject): total_count (:obj:`int`): Number of times the reaction was added. """ - __slots__ = ( - "total_count", - "type", - ) - - def __init__( - self, - type: ReactionType, # pylint: disable=redefined-builtin - total_count: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.type: ReactionType = type - self.total_count: int = total_count - - self._id_attrs = ( - self.type, - self.total_count, - ) - self._freeze() + type: ReactionType = tg_field(compare=True) + total_count: int = tg_field(compare=True) diff --git a/src/telegram/_reply.py b/src/telegram/_reply.py index 6d544a8e957..6038d46bb7d 100644 --- a/src/telegram/_reply.py +++ b/src/telegram/_reply.py @@ -18,7 +18,6 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This modules contains objects that represents Telegram Replies""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._chat import Chat @@ -45,14 +44,16 @@ from telegram._story import Story from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput if TYPE_CHECKING: from telegram._files.contact import Contact from telegram._files.location import Location +@tg_dataclass() class ExternalReplyInfo(TelegramObject): """ This object contains information about a message that is being replied to, which may @@ -177,100 +178,37 @@ class ExternalReplyInfo(TelegramObject): """ - __slots__ = ( - "animation", - "audio", - "chat", - "checklist", - "contact", - "dice", - "document", - "game", - "giveaway", - "giveaway_winners", - "has_media_spoiler", - "invoice", - "link_preview_options", - "live_photo", - "location", - "message_id", - "origin", - "paid_media", - "photo", - "poll", - "sticker", - "story", - "venue", - "video", - "video_note", - "voice", - ) - - def __init__( - self, - origin: MessageOrigin, - chat: Chat | None = None, - message_id: int | None = None, - link_preview_options: LinkPreviewOptions | None = None, - animation: Animation | None = None, - audio: Audio | None = None, - document: Document | None = None, - photo: Sequence[PhotoSize] | None = None, - sticker: Sticker | None = None, - story: Story | None = None, - video: Video | None = None, - video_note: VideoNote | None = None, - voice: Voice | None = None, - has_media_spoiler: bool | None = None, - contact: "Contact | None" = None, - dice: Dice | None = None, - game: Game | None = None, - giveaway: Giveaway | None = None, - giveaway_winners: GiveawayWinners | None = None, - invoice: Invoice | None = None, - location: "Location | None" = None, - poll: Poll | None = None, - venue: Venue | None = None, - paid_media: PaidMediaInfo | None = None, - checklist: Checklist | None = None, - live_photo: LivePhoto | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.origin: MessageOrigin = origin - self.chat: Chat | None = chat - self.message_id: int | None = message_id - self.link_preview_options: LinkPreviewOptions | None = link_preview_options - self.animation: Animation | None = animation - self.audio: Audio | None = audio - self.document: Document | None = document - self.photo: tuple[PhotoSize, ...] | None = parse_sequence_arg(photo) - self.sticker: Sticker | None = sticker - self.story: Story | None = story - self.video: Video | None = video - self.video_note: VideoNote | None = video_note - self.voice: Voice | None = voice - self.has_media_spoiler: bool | None = has_media_spoiler - self.checklist: Checklist | None = checklist - self.contact: Contact | None = contact - self.dice: Dice | None = dice - self.game: Game | None = game - self.giveaway: Giveaway | None = giveaway - self.giveaway_winners: GiveawayWinners | None = giveaway_winners - self.invoice: Invoice | None = invoice - self.location: Location | None = location - self.poll: Poll | None = poll - self.venue: Venue | None = venue - self.paid_media: PaidMediaInfo | None = paid_media - self.live_photo: LivePhoto | None = live_photo - - self._id_attrs = (self.origin,) - - self._freeze() - - + # Required + origin: MessageOrigin = tg_field(compare=True) + # Optionals + chat: Chat | None = tg_field(default=None) + message_id: int | None = tg_field(default=None) + link_preview_options: LinkPreviewOptions | None = tg_field(default=None) + animation: Animation | None = tg_field(default=None) + audio: Audio | None = tg_field(default=None) + document: Document | None = tg_field(default=None) + photo: tuple[PhotoSize, ...] = tg_field(default=None, converter=parse_sequence_arg) + sticker: Sticker | None = tg_field(default=None) + story: Story | None = tg_field(default=None) + video: Video | None = tg_field(default=None) + video_note: VideoNote | None = tg_field(default=None) + voice: Voice | None = tg_field(default=None) + has_media_spoiler: bool | None = tg_field(default=None) + contact: "Contact | None" = tg_field(default=None) + dice: Dice | None = tg_field(default=None) + game: Game | None = tg_field(default=None) + giveaway: Giveaway | None = tg_field(default=None) + giveaway_winners: GiveawayWinners | None = tg_field(default=None) + invoice: Invoice | None = tg_field(default=None) + location: "Location | None" = tg_field(default=None) + poll: Poll | None = tg_field(default=None) + venue: Venue | None = tg_field(default=None) + paid_media: PaidMediaInfo | None = tg_field(default=None) + checklist: Checklist | None = tg_field(default=None) + live_photo: LivePhoto | None = tg_field(default=None) + + +@tg_dataclass() class TextQuote(TelegramObject): """ This object contains information about the quoted part of a message that is replied to @@ -305,37 +243,15 @@ class TextQuote(TelegramObject): message sender. Otherwise, the quote was added automatically by the server. """ - __slots__ = ( - "entities", - "is_manual", - "position", - "text", - ) - - def __init__( - self, - text: str, - position: int, - entities: Sequence[MessageEntity] | None = None, - is_manual: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.text: str = text - self.position: int = position - self.entities: tuple[MessageEntity, ...] | None = parse_sequence_arg(entities) - self.is_manual: bool | None = is_manual - - self._id_attrs = ( - self.text, - self.position, - ) - - self._freeze() + # Required + text: str = tg_field(compare=True) + position: int = tg_field(compare=True) + # Optional + entities: tuple[MessageEntity, ...] = tg_field(default=None, converter=parse_sequence_arg) + is_manual: bool | None = tg_field(default=None) +@tg_dataclass() class ReplyParameters(TelegramObject): """ Describes reply parameters for the message that is being sent. @@ -415,44 +331,16 @@ class ReplyParameters(TelegramObject): .. versionadded:: 22.8 """ - __slots__ = ( - "allow_sending_without_reply", - "chat_id", - "checklist_task_id", - "message_id", - "poll_option_id", - "quote", - "quote_entities", - "quote_parse_mode", - "quote_position", + # Required + message_id: int = tg_field(compare=True) + # Optional + chat_id: int | str | None = tg_field(default=None) + allow_sending_without_reply: ODVInput[bool] = tg_field(default=DEFAULT_NONE) + quote: str | None = tg_field(default=None) + quote_parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + quote_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - message_id: int, - chat_id: int | str | None = None, - allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, - quote: str | None = None, - quote_parse_mode: ODVInput[str] = DEFAULT_NONE, - quote_entities: Sequence[MessageEntity] | None = None, - quote_position: int | None = None, - checklist_task_id: int | None = None, - poll_option_id: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.message_id: int = message_id - self.chat_id: int | str | None = chat_id - self.allow_sending_without_reply: ODVInput[bool] = allow_sending_without_reply - self.quote: str | None = quote - self.quote_parse_mode: ODVInput[str] = quote_parse_mode - self.quote_entities: tuple[MessageEntity, ...] | None = parse_sequence_arg(quote_entities) - self.quote_position: int | None = quote_position - self.checklist_task_id: int | None = checklist_task_id - self.poll_option_id: str | None = poll_option_id - - self._id_attrs = (self.message_id,) - - self._freeze() + quote_position: int | None = tg_field(default=None) + checklist_task_id: int | None = tg_field(default=None) + poll_option_id: str | None = tg_field(default=None) diff --git a/src/telegram/_replykeyboardmarkup.py b/src/telegram/_replykeyboardmarkup.py index 99ac0de3b00..27f54d488d8 100644 --- a/src/telegram/_replykeyboardmarkup.py +++ b/src/telegram/_replykeyboardmarkup.py @@ -19,15 +19,16 @@ """This module contains an object that represents a Telegram ReplyKeyboardMarkup.""" from collections.abc import Sequence -from typing import Final +from typing import ClassVar from telegram import constants from telegram._keyboardbutton import KeyboardButton from telegram._telegramobject import TelegramObject +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.markup import check_keyboard_type -from telegram._utils.types import JSONDict +@tg_dataclass() class ReplyKeyboardMarkup(TelegramObject): """This object represents a custom keyboard with reply options. Not supported in channels and for messages sent on behalf of a Telegram Business account. @@ -121,49 +122,30 @@ class ReplyKeyboardMarkup(TelegramObject): """ - __slots__ = ( - "input_field_placeholder", - "is_persistent", - "keyboard", - "one_time_keyboard", - "resize_keyboard", - "selective", - ) - - def __init__( - self, - keyboard: Sequence[Sequence[str | KeyboardButton]], - resize_keyboard: bool | None = None, - one_time_keyboard: bool | None = None, - selective: bool | None = None, - input_field_placeholder: str | None = None, - is_persistent: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - if not check_keyboard_type(keyboard): + @staticmethod + def _keyboard_converter( + value: Sequence[Sequence[str | KeyboardButton]], + ) -> tuple[tuple[KeyboardButton, ...], ...]: + if not check_keyboard_type(value): raise ValueError( "The parameter `keyboard` should be a sequence of sequences of " "strings or KeyboardButtons" ) - - # Required - self.keyboard: tuple[tuple[KeyboardButton, ...], ...] = tuple( + return tuple( tuple(KeyboardButton(button) if isinstance(button, str) else button for button in row) - for row in keyboard + for row in value ) - # Optionals - self.resize_keyboard: bool | None = resize_keyboard - self.one_time_keyboard: bool | None = one_time_keyboard - self.selective: bool | None = selective - self.input_field_placeholder: str | None = input_field_placeholder - self.is_persistent: bool | None = is_persistent - - self._id_attrs = (self.keyboard,) - - self._freeze() + # Required + keyboard: tuple[tuple[KeyboardButton, ...], ...] = tg_field( + compare=True, converter=_keyboard_converter + ) + # Optional + resize_keyboard: bool | None = tg_field(default=None) + one_time_keyboard: bool | None = tg_field(default=None) + selective: bool | None = tg_field(default=None) + input_field_placeholder: str | None = tg_field(default=None) + is_persistent: bool | None = tg_field(default=None) @classmethod def from_button( @@ -348,12 +330,12 @@ def from_column( **kwargs, # type: ignore[arg-type] ) - MIN_INPUT_FIELD_PLACEHOLDER: Final[int] = constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER + MIN_INPUT_FIELD_PLACEHOLDER: ClassVar[int] = constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER """:const:`telegram.constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER` .. versionadded:: 20.0 """ - MAX_INPUT_FIELD_PLACEHOLDER: Final[int] = constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER + MAX_INPUT_FIELD_PLACEHOLDER: ClassVar[int] = constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER """:const:`telegram.constants.ReplyLimit.MAX_INPUT_FIELD_PLACEHOLDER` .. versionadded:: 20.0 diff --git a/src/telegram/_replykeyboardremove.py b/src/telegram/_replykeyboardremove.py index 05a5839595a..b249b221db1 100644 --- a/src/telegram/_replykeyboardremove.py +++ b/src/telegram/_replykeyboardremove.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram ReplyKeyboardRemove.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ReplyKeyboardRemove(TelegramObject): """ Upon receiving a message with this object, Telegram clients will remove the current custom @@ -60,13 +61,7 @@ class ReplyKeyboardRemove(TelegramObject): """ - __slots__ = ("remove_keyboard", "selective") + # Attribute only (init=False) + remove_keyboard: bool = tg_field(compare=True, init=False, default=True) - def __init__(self, selective: bool | None = None, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - # Required - self.remove_keyboard: bool = True - # Optionals - self.selective: bool | None = selective - - self._freeze() + selective: bool | None = tg_field(default=None) diff --git a/src/telegram/_sentguestmessage.py b/src/telegram/_sentguestmessage.py index daea15ebdee..3938eeec36f 100644 --- a/src/telegram/_sentguestmessage.py +++ b/src/telegram/_sentguestmessage.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram Sent Guest Message.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class SentGuestMessage(TelegramObject): """Describes an inline message sent by a guest bot. @@ -37,18 +38,5 @@ class SentGuestMessage(TelegramObject): inline_message_id (:obj:`str`): Identifier of the sent inline message. """ - __slots__ = ("inline_message_id",) - - def __init__( - self, - inline_message_id: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.inline_message_id: str = inline_message_id - - self._id_attrs = (self.inline_message_id,) - - self._freeze() + # Required + inline_message_id: str = tg_field(compare=True) diff --git a/src/telegram/_sentwebappmessage.py b/src/telegram/_sentwebappmessage.py index 1d26e231b9e..2d0563d6a0c 100644 --- a/src/telegram/_sentwebappmessage.py +++ b/src/telegram/_sentwebappmessage.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram Sent Web App Message.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class SentWebAppMessage(TelegramObject): """Contains information about an inline message sent by a Web App on behalf of a user. @@ -41,15 +42,5 @@ class SentWebAppMessage(TelegramObject): the message. """ - __slots__ = ("inline_message_id",) - - def __init__( - self, inline_message_id: str | None = None, *, api_kwargs: JSONDict | None = None - ): - super().__init__(api_kwargs=api_kwargs) - # Optionals - self.inline_message_id: str | None = inline_message_id - - self._id_attrs = (self.inline_message_id,) - - self._freeze() + # Optionals + inline_message_id: str | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_shared.py b/src/telegram/_shared.py index 66c095b11aa..30d232e95a5 100644 --- a/src/telegram/_shared.py +++ b/src/telegram/_shared.py @@ -18,15 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains two objects used for request chats/users service messages.""" -from collections.abc import Sequence - from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.usernames import get_full_name, get_link, get_name +@tg_dataclass() class UsersShared(TelegramObject): """ This object contains information about the user whose identifier was shared with the bot @@ -64,30 +63,17 @@ class UsersShared(TelegramObject): .. versionadded:: 21.1 """ - __slots__ = ("request_id", "users") - __REMOVED_API_FIELDS__ = frozenset( { "user_ids", } ) - def __init__( - self, - request_id: int, - users: Sequence["SharedUser"], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.request_id: int = request_id - self.users: tuple[SharedUser, ...] = parse_sequence_arg(users) - - self._id_attrs = (self.request_id, self.users) - - self._freeze() + request_id: int = tg_field(compare=True) + users: tuple["SharedUser", ...] = tg_field(compare=True, converter=parse_sequence_arg) +@tg_dataclass() class ChatShared(TelegramObject): """ This object contains information about the chat whose identifier was shared with the bot @@ -135,28 +121,13 @@ class ChatShared(TelegramObject): .. versionadded:: 21.1 """ - __slots__ = ("chat_id", "photo", "request_id", "title", "username") - - def __init__( - self, - request_id: int, - chat_id: int, - title: str | None = None, - username: str | None = None, - photo: Sequence[PhotoSize] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.request_id: int = request_id - self.chat_id: int = chat_id - self.title: str | None = title - self.username: str | None = username - self.photo: tuple[PhotoSize, ...] | None = parse_sequence_arg(photo) - - self._id_attrs = (self.request_id, self.chat_id) - - self._freeze() + # Required + request_id: int = tg_field(compare=True) + chat_id: int = tg_field(compare=True) + # Optional + title: str | None = tg_field(default=None) + username: str | None = tg_field(default=None) + photo: tuple[PhotoSize, ...] = tg_field(default=None, converter=parse_sequence_arg) @property def link(self) -> str | None: @@ -168,6 +139,7 @@ def link(self) -> str | None: return get_link(self) +@tg_dataclass() class SharedUser(TelegramObject): """ This object contains information about a user that was shared with the bot using a @@ -211,28 +183,13 @@ class SharedUser(TelegramObject): the photo was requested by the bot. This list is empty if the photo was not requsted. """ - __slots__ = ("first_name", "last_name", "photo", "user_id", "username") - - def __init__( - self, - user_id: int, - first_name: str | None = None, - last_name: str | None = None, - username: str | None = None, - photo: Sequence[PhotoSize] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.user_id: int = user_id - self.first_name: str | None = first_name - self.last_name: str | None = last_name - self.username: str | None = username - self.photo: tuple[PhotoSize, ...] | None = parse_sequence_arg(photo) - - self._id_attrs = (self.user_id,) - - self._freeze() + # Required + user_id: int = tg_field(compare=True) + # Optional + first_name: str | None = tg_field(default=None) + last_name: str | None = tg_field(default=None) + username: str | None = tg_field(default=None) + photo: tuple[PhotoSize, ...] = tg_field(default=None, converter=parse_sequence_arg) @property def name(self) -> str | None: diff --git a/src/telegram/_story.py b/src/telegram/_story.py index 85d17f8935d..7f653ca19f4 100644 --- a/src/telegram/_story.py +++ b/src/telegram/_story.py @@ -20,10 +20,12 @@ from telegram._chat import Chat from telegram._telegramobject import TelegramObject +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput, TimePeriod +@tg_dataclass() class Story(TelegramObject): """ This object represents a story. @@ -46,25 +48,9 @@ class Story(TelegramObject): """ - __slots__ = ( - "chat", - "id", - ) - - def __init__( - self, - chat: Chat, - id: int, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.chat: Chat = chat - self.id: int = id - - self._id_attrs = (self.chat, self.id) - - self._freeze() + # Required + chat: Chat = tg_field(compare=True) + id: int = tg_field(compare=True) async def repost( self, diff --git a/src/telegram/_storyarea.py b/src/telegram/_storyarea.py index 27d8f168cf2..a336f3a09af 100644 --- a/src/telegram/_storyarea.py +++ b/src/telegram/_storyarea.py @@ -18,15 +18,16 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects that represent story areas.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._reaction import ReactionType from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class StoryAreaPosition(TelegramObject): """Describes the position of a clickable area within a story. @@ -65,45 +66,15 @@ class StoryAreaPosition(TelegramObject): """ - __slots__ = ( - "corner_radius_percentage", - "height_percentage", - "rotation_angle", - "width_percentage", - "x_percentage", - "y_percentage", - ) - - def __init__( - self, - x_percentage: float, - y_percentage: float, - width_percentage: float, - height_percentage: float, - rotation_angle: float, - corner_radius_percentage: float, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.x_percentage: float = x_percentage - self.y_percentage: float = y_percentage - self.width_percentage: float = width_percentage - self.height_percentage: float = height_percentage - self.rotation_angle: float = rotation_angle - self.corner_radius_percentage: float = corner_radius_percentage - - self._id_attrs = ( - self.x_percentage, - self.y_percentage, - self.width_percentage, - self.height_percentage, - self.rotation_angle, - self.corner_radius_percentage, - ) - self._freeze() + x_percentage: float = tg_field(compare=True) + y_percentage: float = tg_field(compare=True) + width_percentage: float = tg_field(compare=True) + height_percentage: float = tg_field(compare=True) + rotation_angle: float = tg_field(compare=True) + corner_radius_percentage: float = tg_field(compare=True) +@tg_dataclass() class LocationAddress(TelegramObject): """Describes the physical address of a location. @@ -129,27 +100,13 @@ class LocationAddress(TelegramObject): """ - __slots__ = ("city", "country_code", "state", "street") - - def __init__( - self, - country_code: str, - state: str | None = None, - city: str | None = None, - street: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.country_code: str = country_code - self.state: str | None = state - self.city: str | None = city - self.street: str | None = street - - self._id_attrs = (self.country_code, self.state, self.city, self.street) - self._freeze() + country_code: str = tg_field(compare=True) + state: str | None = tg_field(compare=True, default=None) + city: str | None = tg_field(compare=True, default=None) + street: str | None = tg_field(compare=True, default=None) +@tg_dataclass() class StoryAreaType(TelegramObject): """Describes the type of a clickable area on a story. Currently, it can be one of: @@ -172,32 +129,25 @@ class StoryAreaType(TelegramObject): """ - __slots__ = ("type",) - - LOCATION: Final[str] = constants.StoryAreaTypeType.LOCATION + LOCATION: ClassVar[str] = constants.StoryAreaTypeType.LOCATION """:const:`telegram.constants.StoryAreaTypeType.LOCATION`""" - SUGGESTED_REACTION: Final[str] = constants.StoryAreaTypeType.SUGGESTED_REACTION + SUGGESTED_REACTION: ClassVar[str] = constants.StoryAreaTypeType.SUGGESTED_REACTION """:const:`telegram.constants.StoryAreaTypeType.SUGGESTED_REACTION`""" - LINK: Final[str] = constants.StoryAreaTypeType.LINK + LINK: ClassVar[str] = constants.StoryAreaTypeType.LINK """:const:`telegram.constants.StoryAreaTypeType.LINK`""" - WEATHER: Final[str] = constants.StoryAreaTypeType.WEATHER + WEATHER: ClassVar[str] = constants.StoryAreaTypeType.WEATHER """:const:`telegram.constants.StoryAreaTypeType.WEATHER`""" - UNIQUE_GIFT: Final[str] = constants.StoryAreaTypeType.UNIQUE_GIFT + UNIQUE_GIFT: ClassVar[str] = constants.StoryAreaTypeType.UNIQUE_GIFT """:const:`telegram.constants.StoryAreaTypeType.UNIQUE_GIFT`""" - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.StoryAreaTypeType, type, type) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.StoryAreaTypeType, value, value) - self._id_attrs = (self.type,) - self._freeze() + type: str = tg_field(compare=True, converter=_type_converter) +@tg_dataclass() class StoryAreaTypeLocation(StoryAreaType): """Describes a story area pointing to a location. Currently, a story can have up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_LOCATION_AREAS` location areas. @@ -220,26 +170,16 @@ class StoryAreaTypeLocation(StoryAreaType): """ - __slots__ = ("address", "latitude", "longitude") - - def __init__( - self, - latitude: float, - longitude: float, - address: LocationAddress | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=StoryAreaType.LOCATION, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.latitude: float = latitude - self.longitude: float = longitude - self.address: LocationAddress | None = address - - self._id_attrs = (self.type, self.latitude, self.longitude) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=StoryAreaType.LOCATION) + # Required + latitude: float = tg_field(compare=True) + longitude: float = tg_field(compare=True) + # Optional + address: LocationAddress | None = tg_field(default=None) +@tg_dataclass() class StoryAreaTypeSuggestedReaction(StoryAreaType): """ Describes a story area pointing to a suggested reaction. Currently, a story can have up to @@ -268,26 +208,16 @@ class StoryAreaTypeSuggestedReaction(StoryAreaType): """ - __slots__ = ("is_dark", "is_flipped", "reaction_type") - - def __init__( - self, - reaction_type: ReactionType, - is_dark: bool | None = None, - is_flipped: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=StoryAreaType.SUGGESTED_REACTION, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.reaction_type: ReactionType = reaction_type - self.is_dark: bool | None = is_dark - self.is_flipped: bool | None = is_flipped - - self._id_attrs = (self.type, self.reaction_type, self.is_dark, self.is_flipped) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=StoryAreaType.SUGGESTED_REACTION) + # Required + reaction_type: ReactionType = tg_field(compare=True) + # Optional + is_dark: bool | None = tg_field(compare=True, default=None) + is_flipped: bool | None = tg_field(compare=True, default=None) +@tg_dataclass() class StoryAreaTypeLink(StoryAreaType): """Describes a story area pointing to an ``HTTP`` or ``tg://`` link. Currently, a story can have up to :tg-const:`~telegram.constants.StoryAreaTypeLimit.MAX_LINK_AREAS` link areas. @@ -306,22 +236,13 @@ class StoryAreaTypeLink(StoryAreaType): """ - __slots__ = ("url",) - - def __init__( - self, - url: str, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=StoryAreaType.LINK, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.url: str = url - - self._id_attrs = (self.type, self.url) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=StoryAreaType.LINK) + # Required + url: str = tg_field(compare=True) +@tg_dataclass() class StoryAreaTypeWeather(StoryAreaType): """ Describes a story area containing weather information. Currently, a story can have up to @@ -347,26 +268,15 @@ class StoryAreaTypeWeather(StoryAreaType): """ - __slots__ = ("background_color", "emoji", "temperature") - - def __init__( - self, - temperature: float, - emoji: str, - background_color: int, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=StoryAreaType.WEATHER, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.temperature: float = temperature - self.emoji: str = emoji - self.background_color: int = background_color - - self._id_attrs = (self.type, self.temperature, self.emoji, self.background_color) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=StoryAreaType.WEATHER) + # Required + temperature: float = tg_field(compare=True) + emoji: str = tg_field(compare=True) + background_color: int = tg_field(compare=True) +@tg_dataclass() class StoryAreaTypeUniqueGift(StoryAreaType): """ Describes a story area pointing to a unique gift. Currently, a story can have at most @@ -387,22 +297,13 @@ class StoryAreaTypeUniqueGift(StoryAreaType): """ - __slots__ = ("name",) - - def __init__( - self, - name: str, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=StoryAreaType.UNIQUE_GIFT, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.name: str = name - - self._id_attrs = (self.type, self.name) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=StoryAreaType.UNIQUE_GIFT) + # Required + name: str = tg_field(compare=True) +@tg_dataclass() class StoryArea(TelegramObject): """Describes a clickable area on a story media. @@ -421,18 +322,5 @@ class StoryArea(TelegramObject): """ - __slots__ = ("position", "type") - - def __init__( - self, - position: StoryAreaPosition, - type: StoryAreaType, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.position: StoryAreaPosition = position - self.type: StoryAreaType = type - self._id_attrs = (self.position, self.type) - - self._freeze() + position: StoryAreaPosition = tg_field(compare=True) + type: StoryAreaType = tg_field(compare=True) diff --git a/src/telegram/_suggestedpost.py b/src/telegram/_suggestedpost.py index 19e66bf92eb..eebec613eb6 100644 --- a/src/telegram/_suggestedpost.py +++ b/src/telegram/_suggestedpost.py @@ -19,16 +19,17 @@ """This module contains objects related to Telegram suggested posts.""" import datetime as dtm -from typing import Final +from typing import ClassVar from telegram import constants from telegram._message import Message from telegram._payment.stars.staramount import StarAmount from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class SuggestedPostPrice(TelegramObject): """ Desribes the price of a suggested post. @@ -65,24 +66,11 @@ class SuggestedPostPrice(TelegramObject): and :tg-const:`telegram.constants.SuggestedPost.MAX_PRICE_NANOTONCOINS`. """ - __slots__ = ("amount", "currency") - - def __init__( - self, - currency: str, - amount: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.currency: str = currency - self.amount: int = amount - - self._id_attrs = (self.currency, self.amount) - - self._freeze() + currency: str = tg_field(compare=True) + amount: int = tg_field(compare=True) +@tg_dataclass() class SuggestedPostParameters(TelegramObject): """ Contains parameters of a post that is being suggested by the bot. @@ -119,24 +107,12 @@ class SuggestedPostParameters(TelegramObject): """ - __slots__ = ("price", "send_date") - - def __init__( - self, - price: SuggestedPostPrice | None = None, - send_date: dtm.datetime | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.price: SuggestedPostPrice | None = price - self.send_date: dtm.datetime | None = send_date - - self._id_attrs = (self.price, self.send_date) - - self._freeze() + # Optional + price: SuggestedPostPrice | None = tg_field(compare=True, default=None) + send_date: dtm.datetime | None = tg_field(compare=True, default=None) +@tg_dataclass() class SuggestedPostInfo(TelegramObject): """ Contains information about a suggested post. @@ -176,35 +152,25 @@ class SuggestedPostInfo(TelegramObject): """ - __slots__ = ("price", "send_date", "state") - - PENDING: Final[str] = constants.SuggestedPostInfoState.PENDING + PENDING: ClassVar[str] = constants.SuggestedPostInfoState.PENDING """:const:`telegram.constants.SuggestedPostInfoState.PENDING`""" - APPROVED: Final[str] = constants.SuggestedPostInfoState.APPROVED + APPROVED: ClassVar[str] = constants.SuggestedPostInfoState.APPROVED """:const:`telegram.constants.SuggestedPostInfoState.APPROVED`""" - DECLINED: Final[str] = constants.SuggestedPostInfoState.DECLINED + DECLINED: ClassVar[str] = constants.SuggestedPostInfoState.DECLINED """:const:`telegram.constants.SuggestedPostInfoState.DECLINED`""" - def __init__( - self, - state: str, - price: SuggestedPostPrice | None = None, - send_date: dtm.datetime | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.state: str = enum.get_member(constants.SuggestedPostInfoState, state, state) - # Optionals - self.price: SuggestedPostPrice | None = price - self.send_date: dtm.datetime | None = send_date - - self._id_attrs = (self.state, self.price) + @staticmethod + def _state_converter(value: str) -> str: + return enum.get_member(constants.SuggestedPostInfoState, value, value) - self._freeze() + # Required + state: str = tg_field(compare=True, converter=_state_converter) + # Optional + price: SuggestedPostPrice | None = tg_field(compare=True, default=None) + send_date: dtm.datetime | None = tg_field(default=None) +@tg_dataclass() class SuggestedPostDeclined(TelegramObject): """ Describes a service message about the rejection of a suggested post. @@ -232,24 +198,11 @@ class SuggestedPostDeclined(TelegramObject): """ - __slots__ = ("comment", "suggested_post_message") - - def __init__( - self, - suggested_post_message: Message | None = None, - comment: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.suggested_post_message: Message | None = suggested_post_message - self.comment: str | None = comment - - self._id_attrs = (self.suggested_post_message, self.comment) - - self._freeze() + suggested_post_message: Message | None = tg_field(compare=True, default=None) + comment: str | None = tg_field(compare=True, default=None) +@tg_dataclass() class SuggestedPostPaid(TelegramObject): """ Describes a service message about a successful payment for a suggested post. @@ -292,35 +245,15 @@ class SuggestedPostPaid(TelegramObject): """ - __slots__ = ("amount", "currency", "star_amount", "suggested_post_message") - - def __init__( - self, - currency: str, - suggested_post_message: Message | None = None, - amount: int | None = None, - star_amount: StarAmount | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.currency: str = currency - # Optionals - self.suggested_post_message: Message | None = suggested_post_message - self.amount: int | None = amount - self.star_amount: StarAmount | None = star_amount - - self._id_attrs = ( - self.currency, - self.suggested_post_message, - self.amount, - self.star_amount, - ) - - self._freeze() + # Required + currency: str = tg_field(compare=True) + # Optional + suggested_post_message: Message | None = tg_field(compare=True, default=None) + amount: int | None = tg_field(compare=True, default=None) + star_amount: StarAmount | None = tg_field(compare=True, default=None) +@tg_dataclass() class SuggestedPostRefunded(TelegramObject): """ Describes a service message about a payment refund for a suggested post. @@ -356,26 +289,13 @@ class SuggestedPostRefunded(TelegramObject): """ - __slots__ = ("reason", "suggested_post_message") - - def __init__( - self, - reason: str, - suggested_post_message: Message | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.reason: str = reason - # Optionals - self.suggested_post_message: Message | None = suggested_post_message - - self._id_attrs = (self.reason, self.suggested_post_message) - - self._freeze() + # Required + reason: str = tg_field(compare=True) + # Optional + suggested_post_message: Message | None = tg_field(compare=True, default=None) +@tg_dataclass() class SuggestedPostApproved(TelegramObject): """ Describes a service message about the approval of a suggested post. @@ -409,28 +329,14 @@ class SuggestedPostApproved(TelegramObject): """ - __slots__ = ("price", "send_date", "suggested_post_message") - - def __init__( - self, - send_date: dtm.datetime, - suggested_post_message: Message | None = None, - price: SuggestedPostPrice | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.send_date: dtm.datetime = send_date - # Optionals - self.suggested_post_message: Message | None = suggested_post_message - self.price: SuggestedPostPrice | None = price - - self._id_attrs = (self.send_date, self.suggested_post_message, self.price) - - self._freeze() + # Rrequired + send_date: dtm.datetime = tg_field(compare=True) + # Optional + suggested_post_message: Message | None = tg_field(compare=True, default=None) + price: SuggestedPostPrice | None = tg_field(compare=True, default=None) +@tg_dataclass() class SuggestedPostApprovalFailed(TelegramObject): """ Describes a service message about the failed approval of a suggested post. Currently, only @@ -459,21 +365,7 @@ class SuggestedPostApprovalFailed(TelegramObject): """ - __slots__ = ("price", "suggested_post_message") - - def __init__( - self, - price: SuggestedPostPrice, - suggested_post_message: Message | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.price: SuggestedPostPrice = price - # Optionals - self.suggested_post_message: Message | None = suggested_post_message - - self._id_attrs = (self.price, self.suggested_post_message) - - self._freeze() + # Required + price: SuggestedPostPrice = tg_field(compare=True) + # Optional + suggested_post_message: Message | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_switchinlinequerychosenchat.py b/src/telegram/_switchinlinequerychosenchat.py index 30e721321e3..27bc2895746 100644 --- a/src/telegram/_switchinlinequerychosenchat.py +++ b/src/telegram/_switchinlinequerychosenchat.py @@ -18,9 +18,10 @@ """This module contains a class that represents a Telegram SwitchInlineQueryChosenChat.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class SwitchInlineQueryChosenChat(TelegramObject): """ This object represents an inline button that switches the current user to inline mode in a @@ -62,38 +63,9 @@ class SwitchInlineQueryChosenChat(TelegramObject): """ - __slots__ = ( - "allow_bot_chats", - "allow_channel_chats", - "allow_group_chats", - "allow_user_chats", - "query", - ) - - def __init__( - self, - query: str | None = None, - allow_user_chats: bool | None = None, - allow_bot_chats: bool | None = None, - allow_group_chats: bool | None = None, - allow_channel_chats: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Optional - self.query: str | None = query - self.allow_user_chats: bool | None = allow_user_chats - self.allow_bot_chats: bool | None = allow_bot_chats - self.allow_group_chats: bool | None = allow_group_chats - self.allow_channel_chats: bool | None = allow_channel_chats - - self._id_attrs = ( - self.query, - self.allow_user_chats, - self.allow_bot_chats, - self.allow_group_chats, - self.allow_channel_chats, - ) - - self._freeze() + # Optional + query: str | None = tg_field(compare=True, default=None) + allow_user_chats: bool | None = tg_field(compare=True, default=None) + allow_bot_chats: bool | None = tg_field(compare=True, default=None) + allow_group_chats: bool | None = tg_field(compare=True, default=None) + allow_channel_chats: bool | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 5822026e2da..487ce1f6951 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -734,11 +734,14 @@ def get_bot(self) -> "Bot": Raises: RuntimeError: If no :class:`telegram.Bot` instance was set for this object. """ - if self._bot is None: + # FIXME: This is a pylint hack without it pylint thinks the return type + # of get_bot() (i.e type of self._bot) is dataclasses.Field + bot = cast("Bot | None", object.__getattribute__(self, "_bot")) + if bot is None: raise RuntimeError( "This object has no bot associated with it. Shortcuts cannot be used." ) - return self._bot + return bot def set_bot(self, bot: "Bot | None") -> None: """Sets the :class:`telegram.Bot` instance associated with this object. diff --git a/src/telegram/_uniquegift.py b/src/telegram/_uniquegift.py index 56a3a7ed90f..95a3518e651 100644 --- a/src/telegram/_uniquegift.py +++ b/src/telegram/_uniquegift.py @@ -20,8 +20,7 @@ """This module contains classes related to unique gifs.""" import datetime as dtm -from collections.abc import Sequence -from typing import Final +from typing import ClassVar from telegram import constants from telegram._chat import Chat @@ -29,9 +28,10 @@ from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class UniqueGiftColors(TelegramObject): """This object contains information about the color scheme for a user's name, message replies and link previews based on a unique gift. @@ -64,48 +64,17 @@ class UniqueGiftColors(TelegramObject): themes; RGB format. """ - __slots__ = ( - "dark_theme_main_color", - "dark_theme_other_colors", - "light_theme_main_color", - "light_theme_other_colors", - "model_custom_emoji_id", - "symbol_custom_emoji_id", + model_custom_emoji_id: str = tg_field(compare=True) + symbol_custom_emoji_id: str = tg_field(compare=True) + light_theme_main_color: int = tg_field(compare=True) + light_theme_other_colors: tuple[int, ...] = tg_field( + compare=True, converter=parse_sequence_arg ) - - def __init__( - self, - model_custom_emoji_id: str, - symbol_custom_emoji_id: str, - light_theme_main_color: int, - light_theme_other_colors: Sequence[int], - dark_theme_main_color: int, - dark_theme_other_colors: Sequence[int], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.model_custom_emoji_id: str = model_custom_emoji_id - self.symbol_custom_emoji_id: str = symbol_custom_emoji_id - self.light_theme_main_color: int = light_theme_main_color - self.light_theme_other_colors: tuple[int, ...] = parse_sequence_arg( - light_theme_other_colors - ) - self.dark_theme_main_color: int = dark_theme_main_color - self.dark_theme_other_colors: tuple[int, ...] = parse_sequence_arg(dark_theme_other_colors) - - self._id_attrs = ( - self.model_custom_emoji_id, - self.symbol_custom_emoji_id, - self.light_theme_main_color, - self.light_theme_other_colors, - self.dark_theme_main_color, - self.dark_theme_other_colors, - ) - - self._freeze() + dark_theme_main_color: int = tg_field(compare=True) + dark_theme_other_colors: tuple[int, ...] = tg_field(compare=True, converter=parse_sequence_arg) +@tg_dataclass() class UniqueGiftModel(TelegramObject): """This object describes the model of a unique gift. @@ -141,35 +110,19 @@ class UniqueGiftModel(TelegramObject): .. versionadded:: 22.7 """ - __slots__ = ( - "name", - "rarity", - "rarity_per_mille", - "sticker", - ) - - def __init__( - self, - name: str, - sticker: Sticker, - rarity_per_mille: int, - rarity: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.name: str = name - self.sticker: Sticker = sticker - self.rarity_per_mille: int = rarity_per_mille - # Optional - self.rarity: str | None = enum.get_member(constants.UniqueGiftModelRarity, rarity, rarity) + @staticmethod + def _rarity_converter(value: str | None) -> str | None: + return enum.get_member(constants.UniqueGiftModelRarity, value, value) - self._id_attrs = (self.name, self.sticker, self.rarity_per_mille) - - self._freeze() + # Required + name: str = tg_field(compare=True) + sticker: Sticker = tg_field(compare=True) + rarity_per_mille: int = tg_field(compare=True) + # Optional + rarity: str | None = tg_field(default=None, converter=_rarity_converter) +@tg_dataclass() class UniqueGiftSymbol(TelegramObject): """This object describes the symbol shown on the pattern of a unique gift. @@ -192,30 +145,12 @@ class UniqueGiftSymbol(TelegramObject): """ - __slots__ = ( - "name", - "rarity_per_mille", - "sticker", - ) - - def __init__( - self, - name: str, - sticker: Sticker, - rarity_per_mille: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.name: str = name - self.sticker: Sticker = sticker - self.rarity_per_mille: int = rarity_per_mille - - self._id_attrs = (self.name, self.sticker, self.rarity_per_mille) - - self._freeze() + name: str = tg_field(compare=True) + sticker: Sticker = tg_field(compare=True) + rarity_per_mille: int = tg_field(compare=True) +@tg_dataclass() class UniqueGiftBackdropColors(TelegramObject): """This object describes the colors of the backdrop of a unique gift. @@ -239,33 +174,13 @@ class UniqueGiftBackdropColors(TelegramObject): """ - __slots__ = ( - "center_color", - "edge_color", - "symbol_color", - "text_color", - ) - - def __init__( - self, - center_color: int, - edge_color: int, - symbol_color: int, - text_color: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.center_color: int = center_color - self.edge_color: int = edge_color - self.symbol_color: int = symbol_color - self.text_color: int = text_color - - self._id_attrs = (self.center_color, self.edge_color, self.symbol_color, self.text_color) - - self._freeze() + center_color: int = tg_field(compare=True) + edge_color: int = tg_field(compare=True) + symbol_color: int = tg_field(compare=True) + text_color: int = tg_field(compare=True) +@tg_dataclass() class UniqueGiftBackdrop(TelegramObject): """This object describes the backdrop of a unique gift. @@ -288,30 +203,12 @@ class UniqueGiftBackdrop(TelegramObject): """ - __slots__ = ( - "colors", - "name", - "rarity_per_mille", - ) - - def __init__( - self, - name: str, - colors: UniqueGiftBackdropColors, - rarity_per_mille: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.name: str = name - self.colors: UniqueGiftBackdropColors = colors - self.rarity_per_mille: int = rarity_per_mille - - self._id_attrs = (self.name, self.colors, self.rarity_per_mille) - - self._freeze() + name: str = tg_field(compare=True) + colors: UniqueGiftBackdropColors = tg_field(compare=True) + rarity_per_mille: int = tg_field(compare=True) +@tg_dataclass() class UniqueGift(TelegramObject): """This object describes a unique gift that was upgraded from a regular gift. @@ -395,64 +292,23 @@ class UniqueGift(TelegramObject): .. versionadded:: 22.7 """ - __slots__ = ( - "backdrop", - "base_name", - "colors", - "gift_id", - "is_burned", - "is_from_blockchain", - "is_premium", - "model", - "name", - "number", - "publisher_chat", - "symbol", - ) - - def __init__( - self, - gift_id: str, - base_name: str, - name: str, - number: int, - model: UniqueGiftModel, - symbol: UniqueGiftSymbol, - backdrop: UniqueGiftBackdrop, - publisher_chat: Chat | None = None, - is_from_blockchain: bool | None = None, - is_premium: bool | None = None, - colors: UniqueGiftColors | None = None, - is_burned: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.gift_id: str = gift_id - self.base_name: str = base_name - self.name: str = name - self.number: int = number - self.model: UniqueGiftModel = model - self.symbol: UniqueGiftSymbol = symbol - self.backdrop: UniqueGiftBackdrop = backdrop - self.publisher_chat: Chat | None = publisher_chat - self.is_from_blockchain: bool | None = is_from_blockchain - self.is_premium: bool | None = is_premium - self.colors: UniqueGiftColors | None = colors - self.is_burned: bool | None = is_burned - - self._id_attrs = ( - self.base_name, - self.name, - self.number, - self.model, - self.symbol, - self.backdrop, - ) - - self._freeze() - - + # Required + gift_id: str = tg_field() + base_name: str = tg_field(compare=True) + name: str = tg_field(compare=True) + number: int = tg_field(compare=True) + model: UniqueGiftModel = tg_field(compare=True) + symbol: UniqueGiftSymbol = tg_field(compare=True) + backdrop: UniqueGiftBackdrop = tg_field(compare=True) + # Optional + publisher_chat: Chat | None = tg_field(default=None) + is_from_blockchain: bool | None = tg_field(default=None) + is_premium: bool | None = tg_field(default=None) + colors: UniqueGiftColors | None = tg_field(default=None) + is_burned: bool | None = tg_field(default=None) + + +@tg_dataclass() class UniqueGiftInfo(TelegramObject): """Describes a service message about a unique gift that was sent or received. @@ -527,59 +383,36 @@ class UniqueGiftInfo(TelegramObject): .. versionadded:: 22.3 """ - GIFTED_UPGRADE: Final[str] = constants.UniqueGiftInfoOrigin.GIFTED_UPGRADE + GIFTED_UPGRADE: ClassVar[str] = constants.UniqueGiftInfoOrigin.GIFTED_UPGRADE """:const:`telegram.constants.UniqueGiftInfoOrigin.GIFTED_UPGRADE` .. versionadded:: 22.6 """ - OFFER: Final[str] = constants.UniqueGiftInfoOrigin.OFFER + OFFER: ClassVar[str] = constants.UniqueGiftInfoOrigin.OFFER """:const:`telegram.constants.UniqueGiftInfoOrigin.OFFER` .. versionadded:: 22.6 """ - RESALE: Final[str] = constants.UniqueGiftInfoOrigin.RESALE + RESALE: ClassVar[str] = constants.UniqueGiftInfoOrigin.RESALE """:const:`telegram.constants.UniqueGiftInfoOrigin.RESALE` .. versionadded:: 22.3 """ - TRANSFER: Final[str] = constants.UniqueGiftInfoOrigin.TRANSFER + TRANSFER: ClassVar[str] = constants.UniqueGiftInfoOrigin.TRANSFER """:const:`telegram.constants.UniqueGiftInfoOrigin.TRANSFER`""" - UPGRADE: Final[str] = constants.UniqueGiftInfoOrigin.UPGRADE + UPGRADE: ClassVar[str] = constants.UniqueGiftInfoOrigin.UPGRADE """:const:`telegram.constants.UniqueGiftInfoOrigin.UPGRADE`""" - __slots__ = ( - "gift", - "last_resale_amount", - "last_resale_currency", - "next_transfer_date", - "origin", - "owned_gift_id", - "transfer_star_count", - ) - - def __init__( - self, - gift: UniqueGift, - origin: str, - owned_gift_id: str | None = None, - transfer_star_count: int | None = None, - next_transfer_date: dtm.datetime | None = None, - last_resale_currency: str | None = None, - last_resale_amount: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.gift: UniqueGift = gift - self.origin: str = enum.get_member(constants.UniqueGiftInfoOrigin, origin, origin) - # Optional - self.owned_gift_id: str | None = owned_gift_id - self.transfer_star_count: int | None = transfer_star_count - self.next_transfer_date: dtm.datetime | None = next_transfer_date - self.last_resale_currency: str | None = last_resale_currency - self.last_resale_amount: int | None = last_resale_amount - - self._id_attrs = (self.gift, self.origin) - - self._freeze() + @staticmethod + def _origin_converter(value: str) -> str: + return enum.get_member(constants.UniqueGiftInfoOrigin, value, value) + + # Required + gift: UniqueGift = tg_field(compare=True) + origin: str = tg_field(compare=True, converter=_origin_converter) + # Optional + owned_gift_id: str | None = tg_field(default=None) + transfer_star_count: int | None = tg_field(default=None) + next_transfer_date: dtm.datetime | None = tg_field(default=None) + last_resale_currency: str | None = tg_field(default=None) + last_resale_amount: int | None = tg_field(default=None) diff --git a/src/telegram/_update.py b/src/telegram/_update.py index 1a309e6ae63..0411aee340f 100644 --- a/src/telegram/_update.py +++ b/src/telegram/_update.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Update.""" -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._business import BusinessConnection, BusinessMessagesDeleted @@ -36,13 +36,14 @@ from telegram._payment.shippingquery import ShippingQuery from telegram._poll import Poll, PollAnswer from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.warnings import warn if TYPE_CHECKING: from telegram import Chat, User +@tg_dataclass() class Update(TelegramObject): """This object represents an incoming update. @@ -296,219 +297,150 @@ class Update(TelegramObject): .. versionadded:: 22.8 """ - __slots__ = ( - "_effective_chat", - "_effective_message", - "_effective_sender", - "_effective_user", - "business_connection", - "business_message", - "callback_query", - "channel_post", - "chat_boost", - "chat_join_request", - "chat_member", - "chosen_inline_result", - "deleted_business_messages", - "edited_business_message", - "edited_channel_post", - "edited_message", - "guest_message", - "inline_query", - "managed_bot", - "message", - "message_reaction", - "message_reaction_count", - "my_chat_member", - "poll", - "poll_answer", - "pre_checkout_query", - "purchased_paid_media", - "removed_chat_boost", - "shipping_query", - "update_id", - ) - - MESSAGE: Final[str] = constants.UpdateType.MESSAGE + MESSAGE: ClassVar[str] = constants.UpdateType.MESSAGE """:const:`telegram.constants.UpdateType.MESSAGE` .. versionadded:: 13.5""" - EDITED_MESSAGE: Final[str] = constants.UpdateType.EDITED_MESSAGE + EDITED_MESSAGE: ClassVar[str] = constants.UpdateType.EDITED_MESSAGE """:const:`telegram.constants.UpdateType.EDITED_MESSAGE` .. versionadded:: 13.5""" - CHANNEL_POST: Final[str] = constants.UpdateType.CHANNEL_POST + CHANNEL_POST: ClassVar[str] = constants.UpdateType.CHANNEL_POST """:const:`telegram.constants.UpdateType.CHANNEL_POST` .. versionadded:: 13.5""" - EDITED_CHANNEL_POST: Final[str] = constants.UpdateType.EDITED_CHANNEL_POST + EDITED_CHANNEL_POST: ClassVar[str] = constants.UpdateType.EDITED_CHANNEL_POST """:const:`telegram.constants.UpdateType.EDITED_CHANNEL_POST` .. versionadded:: 13.5""" - INLINE_QUERY: Final[str] = constants.UpdateType.INLINE_QUERY + INLINE_QUERY: ClassVar[str] = constants.UpdateType.INLINE_QUERY """:const:`telegram.constants.UpdateType.INLINE_QUERY` .. versionadded:: 13.5""" - CHOSEN_INLINE_RESULT: Final[str] = constants.UpdateType.CHOSEN_INLINE_RESULT + CHOSEN_INLINE_RESULT: ClassVar[str] = constants.UpdateType.CHOSEN_INLINE_RESULT """:const:`telegram.constants.UpdateType.CHOSEN_INLINE_RESULT` .. versionadded:: 13.5""" - CALLBACK_QUERY: Final[str] = constants.UpdateType.CALLBACK_QUERY + CALLBACK_QUERY: ClassVar[str] = constants.UpdateType.CALLBACK_QUERY """:const:`telegram.constants.UpdateType.CALLBACK_QUERY` .. versionadded:: 13.5""" - SHIPPING_QUERY: Final[str] = constants.UpdateType.SHIPPING_QUERY + SHIPPING_QUERY: ClassVar[str] = constants.UpdateType.SHIPPING_QUERY """:const:`telegram.constants.UpdateType.SHIPPING_QUERY` .. versionadded:: 13.5""" - PRE_CHECKOUT_QUERY: Final[str] = constants.UpdateType.PRE_CHECKOUT_QUERY + PRE_CHECKOUT_QUERY: ClassVar[str] = constants.UpdateType.PRE_CHECKOUT_QUERY """:const:`telegram.constants.UpdateType.PRE_CHECKOUT_QUERY` .. versionadded:: 13.5""" - POLL: Final[str] = constants.UpdateType.POLL + POLL: ClassVar[str] = constants.UpdateType.POLL """:const:`telegram.constants.UpdateType.POLL` .. versionadded:: 13.5""" - POLL_ANSWER: Final[str] = constants.UpdateType.POLL_ANSWER + POLL_ANSWER: ClassVar[str] = constants.UpdateType.POLL_ANSWER """:const:`telegram.constants.UpdateType.POLL_ANSWER` .. versionadded:: 13.5""" - MY_CHAT_MEMBER: Final[str] = constants.UpdateType.MY_CHAT_MEMBER + MY_CHAT_MEMBER: ClassVar[str] = constants.UpdateType.MY_CHAT_MEMBER """:const:`telegram.constants.UpdateType.MY_CHAT_MEMBER` .. versionadded:: 13.5""" - CHAT_MEMBER: Final[str] = constants.UpdateType.CHAT_MEMBER + CHAT_MEMBER: ClassVar[str] = constants.UpdateType.CHAT_MEMBER """:const:`telegram.constants.UpdateType.CHAT_MEMBER` .. versionadded:: 13.5""" - CHAT_JOIN_REQUEST: Final[str] = constants.UpdateType.CHAT_JOIN_REQUEST + CHAT_JOIN_REQUEST: ClassVar[str] = constants.UpdateType.CHAT_JOIN_REQUEST """:const:`telegram.constants.UpdateType.CHAT_JOIN_REQUEST` .. versionadded:: 13.8""" - CHAT_BOOST: Final[str] = constants.UpdateType.CHAT_BOOST + CHAT_BOOST: ClassVar[str] = constants.UpdateType.CHAT_BOOST """:const:`telegram.constants.UpdateType.CHAT_BOOST` .. versionadded:: 20.8""" - REMOVED_CHAT_BOOST: Final[str] = constants.UpdateType.REMOVED_CHAT_BOOST + REMOVED_CHAT_BOOST: ClassVar[str] = constants.UpdateType.REMOVED_CHAT_BOOST """:const:`telegram.constants.UpdateType.REMOVED_CHAT_BOOST` .. versionadded:: 20.8""" - MESSAGE_REACTION: Final[str] = constants.UpdateType.MESSAGE_REACTION + MESSAGE_REACTION: ClassVar[str] = constants.UpdateType.MESSAGE_REACTION """:const:`telegram.constants.UpdateType.MESSAGE_REACTION` .. versionadded:: 20.8""" - MESSAGE_REACTION_COUNT: Final[str] = constants.UpdateType.MESSAGE_REACTION_COUNT + MESSAGE_REACTION_COUNT: ClassVar[str] = constants.UpdateType.MESSAGE_REACTION_COUNT """:const:`telegram.constants.UpdateType.MESSAGE_REACTION_COUNT` .. versionadded:: 20.8""" - BUSINESS_CONNECTION: Final[str] = constants.UpdateType.BUSINESS_CONNECTION + BUSINESS_CONNECTION: ClassVar[str] = constants.UpdateType.BUSINESS_CONNECTION """:const:`telegram.constants.UpdateType.BUSINESS_CONNECTION` .. versionadded:: 21.1""" - BUSINESS_MESSAGE: Final[str] = constants.UpdateType.BUSINESS_MESSAGE + BUSINESS_MESSAGE: ClassVar[str] = constants.UpdateType.BUSINESS_MESSAGE """:const:`telegram.constants.UpdateType.BUSINESS_MESSAGE` .. versionadded:: 21.1""" - EDITED_BUSINESS_MESSAGE: Final[str] = constants.UpdateType.EDITED_BUSINESS_MESSAGE + EDITED_BUSINESS_MESSAGE: ClassVar[str] = constants.UpdateType.EDITED_BUSINESS_MESSAGE """:const:`telegram.constants.UpdateType.EDITED_BUSINESS_MESSAGE` .. versionadded:: 21.1""" - DELETED_BUSINESS_MESSAGES: Final[str] = constants.UpdateType.DELETED_BUSINESS_MESSAGES + DELETED_BUSINESS_MESSAGES: ClassVar[str] = constants.UpdateType.DELETED_BUSINESS_MESSAGES """:const:`telegram.constants.UpdateType.DELETED_BUSINESS_MESSAGES` .. versionadded:: 21.1""" - PURCHASED_PAID_MEDIA: Final[str] = constants.UpdateType.PURCHASED_PAID_MEDIA + PURCHASED_PAID_MEDIA: ClassVar[str] = constants.UpdateType.PURCHASED_PAID_MEDIA """:const:`telegram.constants.UpdateType.PURCHASED_PAID_MEDIA` .. versionadded:: 21.6 """ - MANAGED_BOT: Final[str] = constants.UpdateType.MANAGED_BOT + MANAGED_BOT: ClassVar[str] = constants.UpdateType.MANAGED_BOT """:const:`telegram.constants.UpdateType.MANAGED_BOT` .. versionadded:: 22.8 """ - GUEST_MESSAGE: Final[str] = constants.UpdateType.GUEST_MESSAGE + GUEST_MESSAGE: ClassVar[str] = constants.UpdateType.GUEST_MESSAGE """:const:`telegram.constants.UpdateType.GUEST_MESSAGE` .. versionadded:: 22.8 """ - ALL_TYPES: Final[list[str]] = list(constants.UpdateType) + ALL_TYPES: ClassVar[list[str]] = list(constants.UpdateType) """list[:obj:`str`]: A list of all available update types. .. versionadded:: 13.5""" - def __init__( - self, - update_id: int, - message: Message | None = None, - edited_message: Message | None = None, - channel_post: Message | None = None, - edited_channel_post: Message | None = None, - inline_query: InlineQuery | None = None, - chosen_inline_result: ChosenInlineResult | None = None, - callback_query: CallbackQuery | None = None, - shipping_query: ShippingQuery | None = None, - pre_checkout_query: PreCheckoutQuery | None = None, - poll: Poll | None = None, - poll_answer: PollAnswer | None = None, - my_chat_member: ChatMemberUpdated | None = None, - chat_member: ChatMemberUpdated | None = None, - chat_join_request: ChatJoinRequest | None = None, - chat_boost: ChatBoostUpdated | None = None, - removed_chat_boost: ChatBoostRemoved | None = None, - message_reaction: MessageReactionUpdated | None = None, - message_reaction_count: MessageReactionCountUpdated | None = None, - business_connection: BusinessConnection | None = None, - business_message: Message | None = None, - edited_business_message: Message | None = None, - deleted_business_messages: BusinessMessagesDeleted | None = None, - purchased_paid_media: PaidMediaPurchased | None = None, - managed_bot: ManagedBotUpdated | None = None, - guest_message: Message | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.update_id: int = update_id - # Optionals - self.message: Message | None = message - self.edited_message: Message | None = edited_message - self.inline_query: InlineQuery | None = inline_query - self.chosen_inline_result: ChosenInlineResult | None = chosen_inline_result - self.callback_query: CallbackQuery | None = callback_query - self.shipping_query: ShippingQuery | None = shipping_query - self.pre_checkout_query: PreCheckoutQuery | None = pre_checkout_query - self.channel_post: Message | None = channel_post - self.edited_channel_post: Message | None = edited_channel_post - self.poll: Poll | None = poll - self.poll_answer: PollAnswer | None = poll_answer - self.my_chat_member: ChatMemberUpdated | None = my_chat_member - self.chat_member: ChatMemberUpdated | None = chat_member - self.chat_join_request: ChatJoinRequest | None = chat_join_request - self.chat_boost: ChatBoostUpdated | None = chat_boost - self.removed_chat_boost: ChatBoostRemoved | None = removed_chat_boost - self.message_reaction: MessageReactionUpdated | None = message_reaction - self.message_reaction_count: MessageReactionCountUpdated | None = message_reaction_count - self.business_connection: BusinessConnection | None = business_connection - self.business_message: Message | None = business_message - self.edited_business_message: Message | None = edited_business_message - self.deleted_business_messages: BusinessMessagesDeleted | None = deleted_business_messages - self.purchased_paid_media: PaidMediaPurchased | None = purchased_paid_media - self.managed_bot: ManagedBotUpdated | None = managed_bot - self.guest_message: Message | None = guest_message - - self._effective_user: User | None = None - self._effective_sender: User | Chat | None = None - self._effective_chat: Chat | None = None - self._effective_message: Message | None = None - - self._id_attrs = (self.update_id,) - - self._freeze() + # Required + update_id: int = tg_field(compare=True) + # Optional + message: Message | None = tg_field(default=None) + edited_message: Message | None = tg_field(default=None) + channel_post: Message | None = tg_field(default=None) + edited_channel_post: Message | None = tg_field(default=None) + inline_query: InlineQuery | None = tg_field(default=None) + chosen_inline_result: ChosenInlineResult | None = tg_field(default=None) + callback_query: CallbackQuery | None = tg_field(default=None) + shipping_query: ShippingQuery | None = tg_field(default=None) + pre_checkout_query: PreCheckoutQuery | None = tg_field(default=None) + poll: Poll | None = tg_field(default=None) + poll_answer: PollAnswer | None = tg_field(default=None) + my_chat_member: ChatMemberUpdated | None = tg_field(default=None) + chat_member: ChatMemberUpdated | None = tg_field(default=None) + chat_join_request: ChatJoinRequest | None = tg_field(default=None) + chat_boost: ChatBoostUpdated | None = tg_field(default=None) + removed_chat_boost: ChatBoostRemoved | None = tg_field(default=None) + message_reaction: MessageReactionUpdated | None = tg_field(default=None) + message_reaction_count: MessageReactionCountUpdated | None = tg_field(default=None) + business_connection: BusinessConnection | None = tg_field(default=None) + business_message: Message | None = tg_field(default=None) + edited_business_message: Message | None = tg_field(default=None) + deleted_business_messages: BusinessMessagesDeleted | None = tg_field(default=None) + purchased_paid_media: PaidMediaPurchased | None = tg_field(default=None) + managed_bot: ManagedBotUpdated | None = tg_field(default=None) + guest_message: Message | None = tg_field(default=None) + + _effective_user: "User | None" = tg_field(init=False, default=None) + _effective_sender: "User | Chat | None" = tg_field(init=False, default=None) + _effective_chat: "Chat | None" = tg_field(init=False, default=None) + _effective_message: Message | None = tg_field(init=False, default=None) @property def effective_user(self) -> "User | None": @@ -601,7 +533,7 @@ def effective_user(self) -> "User | None": elif self.managed_bot: user = self.managed_bot.user - self._effective_user = user + object.__setattr__(self, "_effective_user", user) return user @property @@ -663,7 +595,7 @@ def effective_sender(self) -> "User | Chat | None": if sender is None: sender = self.effective_user - self._effective_sender = sender + object.__setattr__(self, "_effective_sender", sender) return sender @property @@ -730,7 +662,7 @@ def effective_chat(self) -> "Chat | None": elif self.message_reaction_count: chat = self.message_reaction_count.chat - self._effective_chat = chat + object.__setattr__(self, "_effective_chat", chat) return chat @property @@ -800,5 +732,5 @@ def effective_message(self) -> Message | None: elif self.guest_message: message = self.guest_message - self._effective_message = message + object.__setattr__(self, "_effective_message", message) return message diff --git a/src/telegram/_user.py b/src/telegram/_user.py index e8d4aae0a62..d7a17ec9fa0 100644 --- a/src/telegram/_user.py +++ b/src/telegram/_user.py @@ -26,6 +26,7 @@ from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton from telegram._menubutton import MenuButton from telegram._telegramobject import TelegramObject +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import ( CorrectOptionIds, @@ -77,6 +78,7 @@ from telegram._utils.types import FileInput, ReplyMarkup +@tg_dataclass() class User(TelegramObject): """This object represents a Telegram user or bot. @@ -192,72 +194,25 @@ class User(TelegramObject): case so far, but Telegram does not guarantee that this stays this way. """ - __slots__ = ( - "added_to_attachment_menu", - "allows_users_to_create_topics", - "can_connect_to_business", - "can_join_groups", - "can_manage_bots", - "can_read_all_group_messages", - "first_name", - "has_main_web_app", - "has_topics_enabled", - "id", - "is_bot", - "is_premium", - "language_code", - "last_name", - "supports_guest_queries", - "supports_inline_queries", - "username", - ) - - def __init__( - self, - id: int, - first_name: str, - is_bot: bool, - last_name: str | None = None, - username: str | None = None, - language_code: str | None = None, - can_join_groups: bool | None = None, - can_read_all_group_messages: bool | None = None, - supports_inline_queries: bool | None = None, - is_premium: bool | None = None, - added_to_attachment_menu: bool | None = None, - can_connect_to_business: bool | None = None, - has_main_web_app: bool | None = None, - has_topics_enabled: bool | None = None, - allows_users_to_create_topics: bool | None = None, - can_manage_bots: bool | None = None, - supports_guest_queries: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.id: int = id - self.first_name: str = first_name - self.is_bot: bool = is_bot - # Optionals - self.last_name: str | None = last_name - self.username: str | None = username - self.language_code: str | None = language_code - self.can_join_groups: bool | None = can_join_groups - self.can_read_all_group_messages: bool | None = can_read_all_group_messages - self.supports_inline_queries: bool | None = supports_inline_queries - self.is_premium: bool | None = is_premium - self.added_to_attachment_menu: bool | None = added_to_attachment_menu - self.can_connect_to_business: bool | None = can_connect_to_business - self.has_main_web_app: bool | None = has_main_web_app - self.has_topics_enabled: bool | None = has_topics_enabled - self.allows_users_to_create_topics: bool | None = allows_users_to_create_topics - self.can_manage_bots: bool | None = can_manage_bots - self.supports_guest_queries: bool | None = supports_guest_queries - - self._id_attrs = (self.id,) - - self._freeze() + # Required + id: int = tg_field(compare=True) + first_name: str = tg_field() + is_bot: bool = tg_field() + # Optional + last_name: str | None = tg_field(default=None) + username: str | None = tg_field(default=None) + language_code: str | None = tg_field(default=None) + can_join_groups: bool | None = tg_field(default=None) + can_read_all_group_messages: bool | None = tg_field(default=None) + supports_inline_queries: bool | None = tg_field(default=None) + is_premium: bool | None = tg_field(default=None) + added_to_attachment_menu: bool | None = tg_field(default=None) + can_connect_to_business: bool | None = tg_field(default=None) + has_main_web_app: bool | None = tg_field(default=None) + has_topics_enabled: bool | None = tg_field(default=None) + allows_users_to_create_topics: bool | None = tg_field(default=None) + can_manage_bots: bool | None = tg_field(default=None) + supports_guest_queries: bool | None = tg_field(default=None) @property def name(self) -> str: diff --git a/src/telegram/_userprofileaudios.py b/src/telegram/_userprofileaudios.py index dfe1362fb4e..f840781c402 100644 --- a/src/telegram/_userprofileaudios.py +++ b/src/telegram/_userprofileaudios.py @@ -18,13 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram UserProfileAudios.""" -from collections.abc import Sequence - from telegram._files.audio import Audio from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class UserProfileAudios(TelegramObject): """ This object represents the audios displayed on a user's profile. @@ -44,20 +44,5 @@ class UserProfileAudios(TelegramObject): """ - __slots__ = ("audios", "total_count") - - def __init__( - self, - total_count: int, - audios: Sequence[Audio], - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - # Required - self.total_count: int = total_count - self.audios: tuple[Audio, ...] = tuple(audios) - - self._id_attrs = (self.total_count, self.audios) - - self._freeze() + total_count: int = tg_field(compare=True) + audios: tuple[Audio, ...] = tg_field(compare=True, converter=parse_sequence_arg) diff --git a/src/telegram/_userprofilephotos.py b/src/telegram/_userprofilephotos.py index e5518a2081a..29d1b6dc4ea 100644 --- a/src/telegram/_userprofilephotos.py +++ b/src/telegram/_userprofilephotos.py @@ -22,9 +22,10 @@ from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class UserProfilePhotos(TelegramObject): """This object represents a user's profile pictures. @@ -49,20 +50,12 @@ class UserProfilePhotos(TelegramObject): """ - __slots__ = ("photos", "total_count") + @staticmethod + def _photos_converter( + value: Sequence[Sequence[PhotoSize]], + ) -> tuple[tuple[PhotoSize, ...], ...]: + return tuple(tuple(sizes) for sizes in value) - def __init__( - self, - total_count: int, - photos: Sequence[Sequence[PhotoSize]], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.total_count: int = total_count - self.photos: tuple[tuple[PhotoSize, ...], ...] = tuple(tuple(sizes) for sizes in photos) - - self._id_attrs = (self.total_count, self.photos) - - self._freeze() + # Required + total_count: int = tg_field(compare=True) + photos: tuple[tuple[PhotoSize, ...], ...] = tg_field(compare=True, converter=_photos_converter) diff --git a/src/telegram/_userrating.py b/src/telegram/_userrating.py index f8045eed9ec..6a68c1b5060 100644 --- a/src/telegram/_userrating.py +++ b/src/telegram/_userrating.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram user rating.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class UserRating(TelegramObject): """ This object describes the rating of a user based on their Telegram Star spendings. @@ -53,23 +54,9 @@ class UserRating(TelegramObject): """ - __slots__ = ("current_level_rating", "level", "next_level_rating", "rating") - - def __init__( - self, - level: int, - rating: int, - current_level_rating: int, - next_level_rating: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.level: int = level - self.rating: int = rating - self.current_level_rating: int = current_level_rating - self.next_level_rating: int | None = next_level_rating - - self._id_attrs = (self.level, self.rating) - - self._freeze() + # Required + level: int = tg_field(compare=True) + rating: int = tg_field(compare=True) + current_level_rating: int = tg_field() + # Optional + next_level_rating: int | None = tg_field(default=None) diff --git a/src/telegram/_utils/usernames.py b/src/telegram/_utils/usernames.py index 5ecb2e4699f..fc05f04de0c 100644 --- a/src/telegram/_utils/usernames.py +++ b/src/telegram/_utils/usernames.py @@ -33,17 +33,29 @@ if TYPE_CHECKING: from typing import type_check_only + # Below fields are defined as properties to satisfy frozeness of TelegramObjects + # disallowing writes @type_check_only class UserLike(Protocol): - first_name: str - last_name: str | None - username: str | None + @property + def first_name(self) -> str: ... + + @property + def last_name(self) -> str | None: ... + + @property + def username(self) -> str | None: ... @type_check_only class UserLikeOptional(Protocol): - first_name: str | None - last_name: str | None - username: str | None + @property + def first_name(self) -> str | None: ... + + @property + def last_name(self) -> str | None: ... + + @property + def username(self) -> str | None: ... @overload @@ -52,7 +64,8 @@ def get_name(userlike: TeleUserLike) -> str: ... def get_name(userlike: TeleUserLikeOptional) -> str | None: ... -def get_name(userlike: TeleUserLike | TeleUserLikeOptional) -> str | None: +# The body handles the broadest supported input +def get_name(userlike: TeleUserLikeOptional) -> str | None: """Returns ``username`` prefixed with "@". If ``username`` is not available, calls :func:`get_full_name` below`. """ @@ -67,7 +80,8 @@ def get_full_name(userlike: TeleUserLike) -> str: ... def get_full_name(userlike: TeleUserLikeOptional) -> str | None: ... -def get_full_name(userlike: TeleUserLike | TeleUserLikeOptional) -> str | None: +# The body handles the broadest supported input +def get_full_name(userlike: TeleUserLikeOptional) -> str | None: """ If parameter ``first_name`` is not :obj:`None`, gives ``first_name`` followed by (if available) `UserLike.last_name`. Otherwise, @@ -86,14 +100,17 @@ def get_full_name(userlike: TeleUserLike | TeleUserLikeOptional) -> str | None: TeleLinkableOptional = TypeVar("TeleLinkableOptional", bound="LinkableOptional") if TYPE_CHECKING: - + # Below fields are defined as properties to satisfy frozeness of TelegramObjects + # disallowing writes @type_check_only class Linkable(Protocol): - username: str + @property + def username(self) -> str: ... @type_check_only class LinkableOptional(Protocol): - username: str | None + @property + def username(self) -> str | None: ... @overload @@ -102,7 +119,8 @@ def get_link(linkable: TeleLinkable) -> str: ... def get_link(linkable: TeleLinkableOptional) -> str | None: ... -def get_link(linkable: TeleLinkable | TeleLinkableOptional) -> str | None: +# The body handles the broadest supported input +def get_link(linkable: TeleLinkableOptional) -> str | None: """If ``username`` is available, returns a t.me link of the user/chat.""" if linkable.username: return f"https://t.me/{linkable.username}" diff --git a/src/telegram/_videochat.py b/src/telegram/_videochat.py index c355fe607b9..1f81afd578e 100644 --- a/src/telegram/_videochat.py +++ b/src/telegram/_videochat.py @@ -19,15 +19,15 @@ """This module contains objects related to Telegram video chats.""" import datetime as dtm -from collections.abc import Sequence from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod +@tg_dataclass() class VideoChatStarted(TelegramObject): """ This object represents a service message about a video @@ -38,14 +38,8 @@ class VideoChatStarted(TelegramObject): This class was renamed from ``VoiceChatStarted`` in accordance to Bot API 6.0. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(api_kwargs=api_kwargs) - - self._freeze() - +@tg_dataclass() class VideoChatEnded(TelegramObject): """ This object represents a service message about a @@ -78,19 +72,7 @@ class VideoChatEnded(TelegramObject): """ - __slots__ = ("_duration",) - - def __init__( - self, - duration: TimePeriod, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self._duration: dtm.timedelta = to_timedelta(duration) - self._id_attrs = (self._duration,) - - self._freeze() + _duration: dtm.timedelta = tg_field(compare=True, alias="duration", converter=to_timedelta) @property def duration(self) -> int | dtm.timedelta: @@ -99,6 +81,7 @@ def duration(self) -> int | dtm.timedelta: ) +@tg_dataclass() class VideoChatParticipantsInvited(TelegramObject): """ This object represents a service message about new members invited to a video chat. @@ -124,21 +107,10 @@ class VideoChatParticipantsInvited(TelegramObject): """ - __slots__ = ("users",) - - def __init__( - self, - users: Sequence[User], - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.users: tuple[User, ...] = parse_sequence_arg(users) - self._id_attrs = (self.users,) - - self._freeze() + users: tuple[User, ...] = tg_field(compare=True, converter=parse_sequence_arg) +@tg_dataclass() class VideoChatScheduled(TelegramObject): """This object represents a service message about a video chat scheduled in the chat. @@ -163,17 +135,4 @@ class VideoChatScheduled(TelegramObject): """ - __slots__ = ("start_date",) - - def __init__( - self, - start_date: dtm.datetime, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.start_date: dtm.datetime = start_date - - self._id_attrs = (self.start_date,) - - self._freeze() + start_date: dtm.datetime = tg_field(compare=True) diff --git a/src/telegram/_webappdata.py b/src/telegram/_webappdata.py index 04583cf99f5..8682132c3f4 100644 --- a/src/telegram/_webappdata.py +++ b/src/telegram/_webappdata.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram WebAppData.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class WebAppData(TelegramObject): """Contains data sent from a `Web App `_ to the bot. @@ -49,14 +50,5 @@ class WebAppData(TelegramObject): Be aware that a bad client can send arbitrary data in this field. """ - __slots__ = ("button_text", "data") - - def __init__(self, data: str, button_text: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - # Required - self.data: str = data - self.button_text: str = button_text - - self._id_attrs = (self.data, self.button_text) - - self._freeze() + data: str = tg_field(compare=True) + button_text: str = tg_field(compare=True) diff --git a/src/telegram/_webappinfo.py b/src/telegram/_webappinfo.py index 2d29cef7df5..e2c93839ebf 100644 --- a/src/telegram/_webappinfo.py +++ b/src/telegram/_webappinfo.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram Web App Info.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class WebAppInfo(TelegramObject): """ This object contains information about a `Web App `_. @@ -45,13 +46,5 @@ class WebAppInfo(TelegramObject): `_. """ - __slots__ = ("url",) - - def __init__(self, url: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - # Required - self.url: str = url - - self._id_attrs = (self.url,) - - self._freeze() + # Required + url: str = tg_field(compare=True) diff --git a/src/telegram/_webhookinfo.py b/src/telegram/_webhookinfo.py index ad076cbb4b5..6178c31ffb7 100644 --- a/src/telegram/_webhookinfo.py +++ b/src/telegram/_webhookinfo.py @@ -19,13 +19,13 @@ """This module contains an object that represents a Telegram WebhookInfo.""" import datetime as dtm -from collections.abc import Sequence from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class WebhookInfo(TelegramObject): """This object represents a Telegram WebhookInfo. @@ -106,56 +106,16 @@ class WebhookInfo(TelegramObject): |datetime_localization| """ - __slots__ = ( - "allowed_updates", - "has_custom_certificate", - "ip_address", - "last_error_date", - "last_error_message", - "last_synchronization_error_date", - "max_connections", - "pending_update_count", - "url", + # Required + url: str = tg_field(compare=True) + has_custom_certificate: bool = tg_field(compare=True) + pending_update_count: int = tg_field(compare=True) + # Optional + last_error_date: dtm.datetime | None = tg_field(compare=True, default=None) + last_error_message: str | None = tg_field(compare=True, default=None) + max_connections: int | None = tg_field(compare=True, default=None) + allowed_updates: tuple[str, ...] = tg_field( + compare=True, default=None, converter=parse_sequence_arg ) - - def __init__( - self, - url: str, - has_custom_certificate: bool, - pending_update_count: int, - last_error_date: dtm.datetime | None = None, - last_error_message: str | None = None, - max_connections: int | None = None, - allowed_updates: Sequence[str] | None = None, - ip_address: str | None = None, - last_synchronization_error_date: dtm.datetime | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.url: str = url - self.has_custom_certificate: bool = has_custom_certificate - self.pending_update_count: int = pending_update_count - - # Optional - self.ip_address: str | None = ip_address - self.last_error_date: dtm.datetime | None = last_error_date - self.last_error_message: str | None = last_error_message - self.max_connections: int | None = max_connections - self.allowed_updates: tuple[str, ...] = parse_sequence_arg(allowed_updates) - self.last_synchronization_error_date: dtm.datetime | None = last_synchronization_error_date - - self._id_attrs = ( - self.url, - self.has_custom_certificate, - self.pending_update_count, - self.ip_address, - self.last_error_date, - self.last_error_message, - self.max_connections, - self.allowed_updates, - self.last_synchronization_error_date, - ) - - self._freeze() + ip_address: str | None = tg_field(compare=True, default=None) + last_synchronization_error_date: dtm.datetime | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_writeaccessallowed.py b/src/telegram/_writeaccessallowed.py index c0ff1322fc4..9cf2f731247 100644 --- a/src/telegram/_writeaccessallowed.py +++ b/src/telegram/_writeaccessallowed.py @@ -19,9 +19,10 @@ """This module contains objects related to the write access allowed service message.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class WriteAccessAllowed(TelegramObject): """ This object represents a service message about a user allowing a bot to write messages after @@ -67,21 +68,6 @@ class WriteAccessAllowed(TelegramObject): """ - __slots__ = ("from_attachment_menu", "from_request", "web_app_name") - - def __init__( - self, - web_app_name: str | None = None, - from_request: bool | None = None, - from_attachment_menu: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.web_app_name: str | None = web_app_name - self.from_request: bool | None = from_request - self.from_attachment_menu: bool | None = from_attachment_menu - - self._id_attrs = (self.web_app_name,) - - self._freeze() + web_app_name: str | None = tg_field(compare=True, default=None) + from_request: bool | None = tg_field(default=None) + from_attachment_menu: bool | None = tg_field(default=None) From d19ccdc4bd28f64a7ffffbfa8f046b95e2ce41ae Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:54:03 +0000 Subject: [PATCH 08/31] Add chango fragment for PR #5334 --- changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml diff --git a/changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml b/changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml new file mode 100644 index 00000000000..e2954017da6 --- /dev/null +++ b/changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml @@ -0,0 +1,6 @@ +breaking = "Dataclasses" +internal = "Dataclasses" +[[pull_requests]] +uid = "5334" +author_uids = ["aelkheir"] +closes_threads = ["5279"] From ea65eeaad906f7ac2876cfe940f737938794fb9e Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:33:56 +0300 Subject: [PATCH 09/31] Finish up classes under `telegram/**/` Except `inputmedia.py` classes and `Bot` --- docs/auxil/sphinx_hooks.py | 1 - docs/source/telegram.animation.rst | 2 +- docs/source/telegram.audio.rst | 2 +- docs/source/telegram.document.rst | 2 +- docs/source/telegram.sticker.rst | 2 +- docs/source/telegram.video.rst | 2 +- docs/source/telegram.videonote.rst | 2 +- docs/source/telegram.voice.rst | 2 +- src/telegram/_files/_basemedium.py | 26 +- src/telegram/_files/_basethumbedmedium.py | 84 ------- src/telegram/_files/animation.py | 47 +--- src/telegram/_files/audio.py | 47 +--- src/telegram/_files/chatphoto.py | 40 +-- src/telegram/_files/contact.py | 34 +-- src/telegram/_files/document.py | 35 +-- src/telegram/_files/file.py | 45 +--- src/telegram/_files/inputprofilephoto.py | 76 +++--- src/telegram/_files/inputsticker.py | 37 ++- src/telegram/_files/inputstorycontent.py | 71 ++---- src/telegram/_files/livephoto.py | 56 +---- src/telegram/_files/location.py | 55 ++--- src/telegram/_files/photosize.py | 30 +-- src/telegram/_files/sticker.py | 159 ++++-------- src/telegram/_files/venue.py | 49 +--- src/telegram/_files/video.py | 66 ++--- src/telegram/_files/videonote.py | 37 +-- src/telegram/_files/videoquality.py | 33 +-- src/telegram/_files/voice.py | 31 +-- src/telegram/_games/callbackgame.py | 10 +- src/telegram/_games/game.py | 46 +--- src/telegram/_games/gamehighscore.py | 19 +- src/telegram/_inline/inlinekeyboardbutton.py | 101 ++------ src/telegram/_inline/inlinekeyboardmarkup.py | 30 +-- src/telegram/_inline/inlinequery.py | 45 ++-- src/telegram/_inline/inlinequeryresult.py | 27 +- .../_inline/inlinequeryresultarticle.py | 53 ++-- .../_inline/inlinequeryresultaudio.py | 61 ++--- .../_inline/inlinequeryresultcachedaudio.py | 47 ++-- .../inlinequeryresultcacheddocument.py | 55 ++--- .../_inline/inlinequeryresultcachedgif.py | 55 ++--- .../inlinequeryresultcachedmpeg4gif.py | 55 ++--- .../_inline/inlinequeryresultcachedphoto.py | 59 ++--- .../_inline/inlinequeryresultcachedsticker.py | 29 +-- .../_inline/inlinequeryresultcachedvideo.py | 59 ++--- .../_inline/inlinequeryresultcachedvoice.py | 51 ++-- .../_inline/inlinequeryresultcontact.py | 57 ++--- .../_inline/inlinequeryresultdocument.py | 71 ++---- src/telegram/_inline/inlinequeryresultgame.py | 26 +- src/telegram/_inline/inlinequeryresultgif.py | 77 ++---- .../_inline/inlinequeryresultlocation.py | 87 ++----- .../_inline/inlinequeryresultmpeg4gif.py | 77 ++---- .../_inline/inlinequeryresultphoto.py | 71 ++---- .../_inline/inlinequeryresultsbutton.py | 36 +-- .../_inline/inlinequeryresultvenue.py | 73 ++---- .../_inline/inlinequeryresultvideo.py | 81 ++---- .../_inline/inlinequeryresultvoice.py | 57 ++--- .../_inline/inputcontactmessagecontent.py | 30 +-- .../_inline/inputinvoicemessagecontent.py | 107 ++------ .../_inline/inputlocationmessagecontent.py | 64 ++--- src/telegram/_inline/inputmessagecontent.py | 10 +- .../_inline/inputtextmessagecontent.py | 49 ++-- .../_inline/inputvenuemessagecontent.py | 55 +---- src/telegram/_inline/preparedinlinemessage.py | 22 +- src/telegram/_message.py | 66 +++-- src/telegram/_messageentity.py | 8 +- src/telegram/_passport/credentials.py | 206 +++++----------- src/telegram/_passport/data.py | 116 ++------- .../_passport/encryptedpassportelement.py | 73 ++---- src/telegram/_passport/passportdata.py | 48 ++-- .../_passport/passportelementerrors.py | 230 +++++++++++------- src/telegram/_passport/passportfile.py | 43 +--- src/telegram/_payment/invoice.py | 60 ++--- src/telegram/_payment/labeledprice.py | 15 +- src/telegram/_payment/orderinfo.py | 27 +- src/telegram/_payment/precheckoutquery.py | 43 +--- src/telegram/_payment/refundedpayment.py | 39 +-- src/telegram/_payment/shippingaddress.py | 47 +--- src/telegram/_payment/shippingoption.py | 26 +- src/telegram/_payment/shippingquery.py | 26 +- src/telegram/_payment/stars/affiliateinfo.py | 41 +--- .../_payment/stars/revenuewithdrawalstate.py | 61 ++--- src/telegram/_payment/stars/staramount.py | 21 +- .../_payment/stars/startransactions.py | 54 ++-- .../_payment/stars/transactionpartner.py | 193 +++++---------- src/telegram/_payment/successfulpayment.py | 58 ++--- src/telegram/ext/_callbackdatacache.py | 3 +- 86 files changed, 1325 insertions(+), 3104 deletions(-) delete mode 100644 src/telegram/_files/_basethumbedmedium.py diff --git a/docs/auxil/sphinx_hooks.py b/docs/auxil/sphinx_hooks.py index a8a3e5271e5..8963a9c0cc1 100644 --- a/docs/auxil/sphinx_hooks.py +++ b/docs/auxil/sphinx_hooks.py @@ -50,7 +50,6 @@ PRIVATE_BASE_CLASSES = { "_ChatUserBaseFilter": "MessageFilter", "_Dice": "MessageFilter", - "_BaseThumbedMedium": "TelegramObject", "_BaseMedium": "TelegramObject", "_CredentialsBase": "TelegramObject", "_ChatBase": "TelegramObject", diff --git a/docs/source/telegram.animation.rst b/docs/source/telegram.animation.rst index 4e654fad49c..e56cba7a3b4 100644 --- a/docs/source/telegram.animation.rst +++ b/docs/source/telegram.animation.rst @@ -1,7 +1,7 @@ Animation ========= -.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject +.. Also lists methods of _BaseMedium, but not the ones of TelegramObject .. autoclass:: telegram.Animation :members: diff --git a/docs/source/telegram.audio.rst b/docs/source/telegram.audio.rst index 563de6c0289..9d1983495e3 100644 --- a/docs/source/telegram.audio.rst +++ b/docs/source/telegram.audio.rst @@ -1,7 +1,7 @@ Audio ===== -.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject +.. Also lists methods of _BaseMedium, but not the ones of TelegramObject .. autoclass:: telegram.Audio :members: diff --git a/docs/source/telegram.document.rst b/docs/source/telegram.document.rst index 1a337077069..5c5c157e962 100644 --- a/docs/source/telegram.document.rst +++ b/docs/source/telegram.document.rst @@ -1,6 +1,6 @@ Document ======== -.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject +.. Also lists methods of _BaseMedium, but not the ones of TelegramObject .. autoclass:: telegram.Document :members: diff --git a/docs/source/telegram.sticker.rst b/docs/source/telegram.sticker.rst index 459629b7ecc..b88bf74a15e 100644 --- a/docs/source/telegram.sticker.rst +++ b/docs/source/telegram.sticker.rst @@ -1,7 +1,7 @@ Sticker ======= -.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject +.. Also lists methods of _BaseMedium, but not the ones of TelegramObject .. autoclass:: telegram.Sticker :members: diff --git a/docs/source/telegram.video.rst b/docs/source/telegram.video.rst index bcda1026431..75d09f9d542 100644 --- a/docs/source/telegram.video.rst +++ b/docs/source/telegram.video.rst @@ -1,7 +1,7 @@ Video ===== -.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject +.. Also lists methods of _BaseMedium, but not the ones of TelegramObject .. autoclass:: telegram.Video :members: diff --git a/docs/source/telegram.videonote.rst b/docs/source/telegram.videonote.rst index e7b504fb515..6c147cdffeb 100644 --- a/docs/source/telegram.videonote.rst +++ b/docs/source/telegram.videonote.rst @@ -1,7 +1,7 @@ VideoNote ========= -.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject +.. Also lists methods of _BaseMedium, but not the ones of TelegramObject .. autoclass:: telegram.VideoNote :members: diff --git a/docs/source/telegram.voice.rst b/docs/source/telegram.voice.rst index b1530967bd2..d74ab9cdb5d 100644 --- a/docs/source/telegram.voice.rst +++ b/docs/source/telegram.voice.rst @@ -1,7 +1,7 @@ Voice ===== -.. Also lists methods of _BaseThumbedMedium, but not the ones of TelegramObject +.. Also lists methods of _BaseMedium, but not the ones of TelegramObject .. autoclass:: telegram.Voice :members: diff --git a/src/telegram/_files/_basemedium.py b/src/telegram/_files/_basemedium.py index 5d9091d7248..22f57b125f1 100644 --- a/src/telegram/_files/_basemedium.py +++ b/src/telegram/_files/_basemedium.py @@ -21,6 +21,7 @@ from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput @@ -28,6 +29,7 @@ from telegram import File +@tg_dataclass() class _BaseMedium(TelegramObject): """Base class for objects representing the various media file types. Objects of this class are comparable in terms of equality. Two objects of this class are @@ -39,36 +41,18 @@ class _BaseMedium(TelegramObject): file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - file_size (:obj:`int`, optional): File size. Attributes: file_id (:obj:`str`): File identifier. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - file_size (:obj:`int`): Optional. File size. """ - __slots__ = ("file_id", "file_size", "file_unique_id") - - def __init__( - self, - file_id: str, - file_unique_id: str, - file_size: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Required - self.file_id: str = str(file_id) - self.file_unique_id: str = str(file_unique_id) - # Optionals - self.file_size: int | None = file_size - - self._id_attrs = (self.file_unique_id,) + # Required + file_id: str = tg_field() + file_unique_id: str = tg_field(compare=True) async def get_file( self, diff --git a/src/telegram/_files/_basethumbedmedium.py b/src/telegram/_files/_basethumbedmedium.py deleted file mode 100644 index 96d17a8079c..00000000000 --- a/src/telegram/_files/_basethumbedmedium.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python -# -# A library that provides a Python interface to the Telegram Bot API -# Copyright (C) 2015-2026 -# Leandro Toledo de Souza -# -# 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/]. -"""Common base class for media objects with thumbnails""" - -from typing import TypeVar - -from telegram._files._basemedium import _BaseMedium -from telegram._files.photosize import PhotoSize -from telegram._utils.types import JSONDict - -# pylint: disable=invalid-name -ThumbedMT_co = TypeVar("ThumbedMT_co", bound="_BaseThumbedMedium", covariant=True) - - -class _BaseThumbedMedium(_BaseMedium): - """ - Base class for objects representing the various media file types that may include a thumbnail. - - Objects of this class are comparable in terms of equality. Two objects of this class are - considered equal, if their :attr:`file_unique_id` is equal. - - Args: - file_id (:obj:`str`): Identifier for this file, which can be used to download - or reuse the file. - file_unique_id (:obj:`str`): Unique identifier for this file, which - is supposed to be the same over time and for different bots. - Can't be used to download or reuse the file. - file_size (:obj:`int`, optional): File size. - thumbnail (:class:`telegram.PhotoSize`, optional): Thumbnail as defined by the sender. - - .. versionadded:: 20.2 - - Attributes: - file_id (:obj:`str`): File identifier. - file_unique_id (:obj:`str`): Unique identifier for this file, which - is supposed to be the same over time and for different bots. - Can't be used to download or reuse the file. - file_size (:obj:`int`): Optional. File size. - thumbnail (:class:`telegram.PhotoSize`): Optional. Thumbnail as defined by the sender. - - .. versionadded:: 20.2 - - """ - - __slots__ = ("thumbnail",) - __REMOVED_API_FIELDS__ = frozenset( - { - "thumb", - } - ) - - def __init__( - self, - file_id: str, - file_unique_id: str, - file_size: int | None = None, - thumbnail: PhotoSize | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - api_kwargs=api_kwargs, - ) - - self.thumbnail: PhotoSize | None = thumbnail diff --git a/src/telegram/_files/animation.py b/src/telegram/_files/animation.py index ff7fc4d9432..1369944efb6 100644 --- a/src/telegram/_files/animation.py +++ b/src/telegram/_files/animation.py @@ -20,14 +20,15 @@ import datetime as dtm -from telegram._files._basethumbedmedium import _BaseThumbedMedium +from telegram._files._basemedium import _BaseMedium from telegram._files.photosize import PhotoSize from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod -class Animation(_BaseThumbedMedium): +@tg_dataclass() +class Animation(_BaseMedium): """This object represents an animation file (GIF or H.264/MPEG-4 AVC video without sound). Objects of this class are comparable in terms of equality. Two objects of this class are @@ -80,37 +81,15 @@ class Animation(_BaseThumbedMedium): """ - __slots__ = ("_duration", "file_name", "height", "mime_type", "width") - - def __init__( - self, - file_id: str, - file_unique_id: str, - width: int, - height: int, - duration: TimePeriod, - file_name: str | None = None, - mime_type: str | None = None, - file_size: int | None = None, - thumbnail: PhotoSize | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - api_kwargs=api_kwargs, - thumbnail=thumbnail, - ) - with self._unfrozen(): - # Required - self.width: int = width - self.height: int = height - self._duration: dtm.timedelta = to_timedelta(duration) - # Optional - self.mime_type: str | None = mime_type - self.file_name: str | None = file_name + # Required + width: int = tg_field() + height: int = tg_field() + _duration: dtm.timedelta = tg_field(alias="duration", converter=to_timedelta) + # Optional + file_name: str | None = tg_field(default=None) + mime_type: str | None = tg_field(default=None) + file_size: int | None = tg_field(default=None) + thumbnail: PhotoSize | None = tg_field(default=None) @property def duration(self) -> int | dtm.timedelta: diff --git a/src/telegram/_files/audio.py b/src/telegram/_files/audio.py index 6d1db7d67ab..adbeccb49d6 100644 --- a/src/telegram/_files/audio.py +++ b/src/telegram/_files/audio.py @@ -20,14 +20,15 @@ import datetime as dtm -from telegram._files._basethumbedmedium import _BaseThumbedMedium +from telegram._files._basemedium import _BaseMedium from telegram._files.photosize import PhotoSize from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod -class Audio(_BaseThumbedMedium): +@tg_dataclass() +class Audio(_BaseMedium): """This object represents an audio file to be treated as music by the Telegram clients. Objects of this class are comparable in terms of equality. Two objects of this class are @@ -82,37 +83,15 @@ class Audio(_BaseThumbedMedium): """ - __slots__ = ("_duration", "file_name", "mime_type", "performer", "title") - - def __init__( - self, - file_id: str, - file_unique_id: str, - duration: TimePeriod, - performer: str | None = None, - title: str | None = None, - mime_type: str | None = None, - file_size: int | None = None, - file_name: str | None = None, - thumbnail: PhotoSize | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - thumbnail=thumbnail, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - # Required - self._duration: dtm.timedelta = to_timedelta(duration) - # Optional - self.performer: str | None = performer - self.title: str | None = title - self.mime_type: str | None = mime_type - self.file_name: str | None = file_name + # Required + _duration: dtm.timedelta = tg_field(alias="duration", converter=to_timedelta) + # Optional + performer: str | None = tg_field(default=None) + title: str | None = tg_field(default=None) + mime_type: str | None = tg_field(default=None) + file_size: int | None = tg_field(default=None) + file_name: str | None = tg_field(default=None) + thumbnail: PhotoSize | None = tg_field(default=None) @property def duration(self) -> int | dtm.timedelta: diff --git a/src/telegram/_files/chatphoto.py b/src/telegram/_files/chatphoto.py index 6fe3eca726c..273dcd9827c 100644 --- a/src/telegram/_files/chatphoto.py +++ b/src/telegram/_files/chatphoto.py @@ -18,10 +18,11 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ChatPhoto.""" -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._telegramobject import TelegramObject +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput @@ -29,6 +30,7 @@ from telegram import File +@tg_dataclass() class ChatPhoto(TelegramObject): """This object represents a chat photo. @@ -74,34 +76,10 @@ class ChatPhoto(TelegramObject): """ - __slots__ = ( - "big_file_id", - "big_file_unique_id", - "small_file_id", - "small_file_unique_id", - ) - - def __init__( - self, - small_file_id: str, - small_file_unique_id: str, - big_file_id: str, - big_file_unique_id: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.small_file_id: str = small_file_id - self.small_file_unique_id: str = small_file_unique_id - self.big_file_id: str = big_file_id - self.big_file_unique_id: str = big_file_unique_id - - self._id_attrs = ( - self.small_file_unique_id, - self.big_file_unique_id, - ) - - self._freeze() + small_file_id: str = tg_field() + small_file_unique_id: str = tg_field(compare=True) + big_file_id: str = tg_field() + big_file_unique_id: str = tg_field(compare=True) async def get_small_file( self, @@ -165,12 +143,12 @@ async def get_big_file( api_kwargs=api_kwargs, ) - SIZE_SMALL: Final[int] = constants.ChatPhotoSize.SMALL + SIZE_SMALL: ClassVar[int] = constants.ChatPhotoSize.SMALL """:const:`telegram.constants.ChatPhotoSize.SMALL` .. versionadded:: 20.0 """ - SIZE_BIG: Final[int] = constants.ChatPhotoSize.BIG + SIZE_BIG: ClassVar[int] = constants.ChatPhotoSize.BIG """:const:`telegram.constants.ChatPhotoSize.BIG` .. versionadded:: 20.0 diff --git a/src/telegram/_files/contact.py b/src/telegram/_files/contact.py index c3951c31b8b..e349c11c6d5 100644 --- a/src/telegram/_files/contact.py +++ b/src/telegram/_files/contact.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram Contact.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class Contact(TelegramObject): """This object represents a phone contact. @@ -44,27 +45,10 @@ class Contact(TelegramObject): """ - __slots__ = ("first_name", "last_name", "phone_number", "user_id", "vcard") - - def __init__( - self, - phone_number: str, - first_name: str, - last_name: str | None = None, - user_id: int | None = None, - vcard: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.phone_number: str = str(phone_number) - self.first_name: str = first_name - # Optionals - self.last_name: str | None = last_name - self.user_id: int | None = user_id - self.vcard: str | None = vcard - - self._id_attrs = (self.phone_number,) - - self._freeze() + # Required + phone_number: str = tg_field(compare=True) + first_name: str = tg_field() + # Optional + last_name: str | None = tg_field(default=None) + user_id: int | None = tg_field(default=None) + vcard: str | None = tg_field(default=None) diff --git a/src/telegram/_files/document.py b/src/telegram/_files/document.py index f58fbee95da..2e5bba1f168 100644 --- a/src/telegram/_files/document.py +++ b/src/telegram/_files/document.py @@ -18,12 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Document.""" -from telegram._files._basethumbedmedium import _BaseThumbedMedium +from telegram._files._basemedium import _BaseMedium from telegram._files.photosize import PhotoSize -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field -class Document(_BaseThumbedMedium): +@tg_dataclass() +class Document(_BaseMedium): """This object represents a general file (as opposed to photos, voice messages and audio files). @@ -61,27 +62,7 @@ class Document(_BaseThumbedMedium): """ - __slots__ = ("file_name", "mime_type") - - def __init__( - self, - file_id: str, - file_unique_id: str, - file_name: str | None = None, - mime_type: str | None = None, - file_size: int | None = None, - thumbnail: PhotoSize | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - thumbnail=thumbnail, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - # Optional - self.mime_type: str | None = mime_type - self.file_name: str | None = file_name + file_name: str | None = tg_field(default=None) + mime_type: str | None = tg_field(default=None) + file_size: int | None = tg_field(default=None) + thumbnail: PhotoSize | None = tg_field(default=None) diff --git a/src/telegram/_files/file.py b/src/telegram/_files/file.py index 3e9d162fd88..ad9e217dc2e 100644 --- a/src/telegram/_files/file.py +++ b/src/telegram/_files/file.py @@ -26,14 +26,16 @@ from telegram._passport.credentials import decrypt from telegram._telegramobject import TelegramObject +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.files import is_local_file -from telegram._utils.types import FilePathInput, JSONDict, ODVInput +from telegram._utils.types import FilePathInput, ODVInput if TYPE_CHECKING: from telegram import FileCredentials +@tg_dataclass() class File(TelegramObject): """ This object represents a file ready to be downloaded. The file can be e.g. downloaded with @@ -74,37 +76,14 @@ class File(TelegramObject): file. """ - __slots__ = ( - "_credentials", - "file_id", - "file_path", - "file_size", - "file_unique_id", - ) - - def __init__( - self, - file_id: str, - file_unique_id: str, - file_size: int | None = None, - file_path: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Required - self.file_id: str = str(file_id) - self.file_unique_id: str = str(file_unique_id) - # Optionals - self.file_size: int | None = file_size - self.file_path: str | None = file_path - - self._credentials: FileCredentials | None = None - - self._id_attrs = (self.file_unique_id,) - - self._freeze() + # Required + file_id: str = tg_field() + file_unique_id: str = tg_field(compare=True) + # Optional + file_size: int | None = tg_field(default=None) + file_path: str | None = tg_field(default=None) + # Attribute only (init=False) + _credentials: "FileCredentials | None" = tg_field(init=False, default=None) def _get_encoded_url(self) -> str: """Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string.""" @@ -369,4 +348,4 @@ def set_credentials(self, credentials: "FileCredentials") -> None: Args: credentials (:class:`telegram.FileCredentials`): The credentials. """ - self._credentials = credentials + object.__setattr__(self, "_credentials", credentials) diff --git a/src/telegram/_files/inputprofilephoto.py b/src/telegram/_files/inputprofilephoto.py index 130d061020f..a00ea094c36 100644 --- a/src/telegram/_files/inputprofilephoto.py +++ b/src/telegram/_files/inputprofilephoto.py @@ -19,19 +19,21 @@ """This module contains an objects that represents a InputProfilePhoto and subclasses.""" import datetime as dtm -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.files import parse_file_input -from telegram._utils.types import FileInput, JSONDict +from telegram._utils.types import FileInput if TYPE_CHECKING: from telegram import InputFile +@tg_dataclass() class InputProfilePhoto(TelegramObject): """This object describes a profile photo to set. Currently, it can be one of @@ -48,25 +50,29 @@ class InputProfilePhoto(TelegramObject): """ - STATIC = constants.InputProfilePhotoType.STATIC + STATIC: ClassVar[str] = constants.InputProfilePhotoType.STATIC """:obj:`str`: :tg-const:`telegram.constants.InputProfilePhotoType.STATIC`.""" - ANIMATED = constants.InputProfilePhotoType.ANIMATED + ANIMATED: ClassVar[str] = constants.InputProfilePhotoType.ANIMATED """:obj:`str`: :tg-const:`telegram.constants.InputProfilePhotoType.ANIMATED`.""" - __slots__ = ("type",) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.InputProfilePhotoType, value, value) - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.InputProfilePhotoType, type, type) + @staticmethod + def _parse_file_input(value: FileInput) -> "str | InputFile": + # We use local_mode=True because we don't have access to the actual setting and want + # things to work in local mode. + return parse_file_input( + value, + local_mode=True, + attach=True, + ) - self._freeze() + type: str = tg_field(converter=_type_converter) +@tg_dataclass() class InputProfilePhotoStatic(InputProfilePhoto): """A static profile photo in the .JPG format. @@ -82,21 +88,13 @@ class InputProfilePhotoStatic(InputProfilePhoto): """ - __slots__ = ("photo",) - - def __init__( - self, - photo: FileInput, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=constants.InputProfilePhotoType.STATIC, api_kwargs=api_kwargs) - with self._unfrozen(): - # We use local_mode=True because we don't have access to the actual setting and want - # things to work in local mode. - self.photo: str | InputFile = parse_file_input(photo, attach=True, local_mode=True) + # Attribute only (init=False) + type: str = tg_field(init=False, default=constants.InputProfilePhotoType.STATIC) + # Required + photo: "str | InputFile" = tg_field(converter=InputProfilePhoto._parse_file_input) +@tg_dataclass() class InputProfilePhotoAnimated(InputProfilePhoto): """An animated profile photo in the MPEG4 format. @@ -116,21 +114,9 @@ class InputProfilePhotoAnimated(InputProfilePhoto): frame that will be used as the static profile photo. Defaults to ``0.0``. """ - __slots__ = ("animation", "main_frame_timestamp") - - def __init__( - self, - animation: FileInput, - main_frame_timestamp: float | dtm.timedelta | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(type=constants.InputProfilePhotoType.ANIMATED, api_kwargs=api_kwargs) - with self._unfrozen(): - # We use local_mode=True because we don't have access to the actual setting and want - # things to work in local mode. - self.animation: str | InputFile = parse_file_input( - animation, attach=True, local_mode=True - ) - - self.main_frame_timestamp: dtm.timedelta | None = to_timedelta(main_frame_timestamp) + # Attribute only (init=False) + type: str = tg_field(init=False, default=constants.InputProfilePhotoType.ANIMATED) + # Required + animation: "str | InputFile" = tg_field(converter=InputProfilePhoto._parse_file_input) + # Optional + main_frame_timestamp: dtm.timedelta | None = tg_field(default=None, converter=to_timedelta) diff --git a/src/telegram/_files/inputsticker.py b/src/telegram/_files/inputsticker.py index 38fafc4de3d..c7b33848e55 100644 --- a/src/telegram/_files/inputsticker.py +++ b/src/telegram/_files/inputsticker.py @@ -18,19 +18,20 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram InputSticker.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._files.sticker import MaskPosition from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.files import parse_file_input -from telegram._utils.types import FileInput, JSONDict +from telegram._utils.types import FileInput if TYPE_CHECKING: from telegram._files.inputfile import InputFile +@tg_dataclass() class InputSticker(TelegramObject): """ This object describes a sticker to be added to a sticker set. @@ -90,30 +91,20 @@ class InputSticker(TelegramObject): .. versionadded:: 21.1 """ - __slots__ = ("emoji_list", "format", "keywords", "mask_position", "sticker") - - def __init__( - self, - sticker: FileInput, - emoji_list: Sequence[str], - format: str, # pylint: disable=redefined-builtin - mask_position: MaskPosition | None = None, - keywords: Sequence[str] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - + @staticmethod + def _sticker_converter(value: FileInput) -> "str | InputFile": # We use local_mode=True because we don't have access to the actual setting and want # things to work in local mode. - self.sticker: str | InputFile = parse_file_input( - sticker, + return parse_file_input( + value, local_mode=True, attach=True, ) - self.emoji_list: tuple[str, ...] = parse_sequence_arg(emoji_list) - self.format: str = format - self.mask_position: MaskPosition | None = mask_position - self.keywords: tuple[str, ...] = parse_sequence_arg(keywords) - self._freeze() + # Required + sticker: "str | InputFile" = tg_field(converter=_sticker_converter) + emoji_list: tuple[str, ...] = tg_field(converter=parse_sequence_arg) + format: str = tg_field() + # Optional + mask_position: MaskPosition | None = tg_field(default=None) + keywords: tuple[str, ...] = tg_field(default=None, converter=parse_sequence_arg) diff --git a/src/telegram/_files/inputstorycontent.py b/src/telegram/_files/inputstorycontent.py index d9edd2dfdbf..fddc498ec6d 100644 --- a/src/telegram/_files/inputstorycontent.py +++ b/src/telegram/_files/inputstorycontent.py @@ -19,17 +19,19 @@ """This module contains objects that represent paid media in Telegram.""" import datetime as dtm -from typing import Final +from typing import ClassVar from telegram import constants from telegram._files.inputfile import InputFile from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.files import parse_file_input -from telegram._utils.types import FileInput, JSONDict +from telegram._utils.types import FileInput +@tg_dataclass() class InputStoryContent(TelegramObject): """This object describes the content of a story to post. Currently, it can be one of: @@ -45,23 +47,14 @@ class InputStoryContent(TelegramObject): type (:obj:`str`): Type of the content. """ - __slots__ = ("type",) - - PHOTO: Final[str] = constants.InputStoryContentType.PHOTO + PHOTO: ClassVar[str] = constants.InputStoryContentType.PHOTO """:const:`telegram.constants.InputStoryContentType.PHOTO`""" - VIDEO: Final[str] = constants.InputStoryContentType.VIDEO + VIDEO: ClassVar[str] = constants.InputStoryContentType.VIDEO """:const:`telegram.constants.InputStoryContentType.VIDEO`""" - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.InputStoryContentType, type, type) - - self._freeze() + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.InputStoryContentType, value, value) @staticmethod def _parse_file_input(file_input: FileInput) -> str | InputFile: @@ -69,7 +62,10 @@ def _parse_file_input(file_input: FileInput) -> str | InputFile: # things to work in local mode. return parse_file_input(file_input, attach=True, local_mode=True) + type: str = tg_field(converter=_type_converter) + +@tg_dataclass() class InputStoryContentPhoto(InputStoryContent): """Describes a photo to post as a story. @@ -92,20 +88,13 @@ class InputStoryContentPhoto(InputStoryContent): """ - __slots__ = ("photo",) - - def __init__( - self, - photo: FileInput, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=InputStoryContent.PHOTO, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.photo: str | InputFile = self._parse_file_input(photo) + # Attribute only (init=False) + type: str = tg_field(init=False, default=InputStoryContent.PHOTO) + # Required + photo: str | InputFile = tg_field(converter=InputStoryContent._parse_file_input) +@tg_dataclass() class InputStoryContentVideo(InputStoryContent): """ Describes a video to post as a story. @@ -144,21 +133,11 @@ class InputStoryContentVideo(InputStoryContent): is_animation (:obj:`bool`): Optional. Pass :obj:`True` if the video has no sound """ - __slots__ = ("cover_frame_timestamp", "duration", "is_animation", "video") - - def __init__( - self, - video: FileInput, - duration: float | dtm.timedelta | None = None, - cover_frame_timestamp: float | dtm.timedelta | None = None, - is_animation: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=InputStoryContent.VIDEO, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.video: str | InputFile = self._parse_file_input(video) - self.duration: dtm.timedelta | None = to_timedelta(duration) - self.cover_frame_timestamp: dtm.timedelta | None = to_timedelta(cover_frame_timestamp) - self.is_animation: bool | None = is_animation + # Attribute only (init=False) + type: str = tg_field(init=False, default=InputStoryContent.VIDEO) + # Required + video: str | InputFile = tg_field(converter=InputStoryContent._parse_file_input) + # Optional + duration: dtm.timedelta | None = tg_field(default=None, converter=to_timedelta) + cover_frame_timestamp: dtm.timedelta | None = tg_field(default=None, converter=to_timedelta) + is_animation: bool | None = tg_field(default=None) diff --git a/src/telegram/_files/livephoto.py b/src/telegram/_files/livephoto.py index aeefd91e9db..3bcc441767a 100644 --- a/src/telegram/_files/livephoto.py +++ b/src/telegram/_files/livephoto.py @@ -18,18 +18,15 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram LivePhoto.""" -from collections.abc import Sequence -from typing import TYPE_CHECKING +import datetime as dtm from telegram._files._basemedium import _BaseMedium from telegram._files.photosize import PhotoSize from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta -from telegram._utils.types import JSONDict, TimePeriod - -if TYPE_CHECKING: - import datetime as dtm +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class LivePhoto(_BaseMedium): """ This object represents a live photo. @@ -40,7 +37,7 @@ class LivePhoto(_BaseMedium): .. versionadded:: 22.8 Args: - file_id (:obj:`str`): Identifier for the video file which can be used to download or reuse + file_id (:obj:`str`): Identifier for the video file which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. @@ -55,7 +52,7 @@ class LivePhoto(_BaseMedium): file_size (:obj:`int`, optional): File size in bytes. Attributes: - file_id (:obj:`str`): Identifier for the video file which can be used to download or reuse + file_id (:obj:`str`): Identifier for the video file which can be used to download or reuse the file. file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be the same over time and for different bots. @@ -71,38 +68,11 @@ class LivePhoto(_BaseMedium): """ - __slots__ = ( - "duration", - "height", - "mime_type", - "photo", - "width", - ) - - def __init__( - self, - file_id: str, - file_unique_id: str, - width: int, - height: int, - duration: TimePeriod, - photo: Sequence[PhotoSize] | None = None, - mime_type: str | None = None, - file_size: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - # Required - self.width: int = width - self.height: int = height - self.duration: dtm.timedelta = to_timedelta(duration) - # Optional - self.photo: Sequence[PhotoSize] | None = parse_sequence_arg(photo) - self.mime_type: str | None = mime_type + # Required + width: int = tg_field() + height: int = tg_field() + duration: dtm.timedelta = tg_field(converter=to_timedelta) + # Optional + photo: tuple[PhotoSize, ...] = tg_field(default=None, converter=parse_sequence_arg) + mime_type: str | None = tg_field(default=None) + file_size: int | None = tg_field(default=None) diff --git a/src/telegram/_files/location.py b/src/telegram/_files/location.py index 14e1b7afc0c..bd1fd21de4e 100644 --- a/src/telegram/_files/location.py +++ b/src/telegram/_files/location.py @@ -19,15 +19,16 @@ """This module contains an object that represents a Telegram Location.""" import datetime as dtm -from typing import Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod +@tg_dataclass() class Location(TelegramObject): """This object represents a point on the map. @@ -70,58 +71,32 @@ class Location(TelegramObject): """ - __slots__ = ( - "_live_period", - "heading", - "horizontal_accuracy", - "latitude", - "longitude", - "proximity_alert_radius", + # Required + longitude: float = tg_field(compare=True) + latitude: float = tg_field(compare=True) + # Optional + horizontal_accuracy: float | None = tg_field(default=None) + _live_period: dtm.timedelta | None = tg_field( + default=None, alias="live_period", converter=to_timedelta ) - - def __init__( - self, - longitude: float, - latitude: float, - horizontal_accuracy: float | None = None, - live_period: TimePeriod | None = None, - heading: int | None = None, - proximity_alert_radius: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.longitude: float = longitude - self.latitude: float = latitude - - # Optionals - self.horizontal_accuracy: float | None = horizontal_accuracy - self._live_period: dtm.timedelta | None = to_timedelta(live_period) - self.heading: int | None = heading - self.proximity_alert_radius: int | None = ( - int(proximity_alert_radius) if proximity_alert_radius else None - ) - - self._id_attrs = (self.longitude, self.latitude) - - self._freeze() + heading: int | None = tg_field(default=None) + proximity_alert_radius: int | None = tg_field(default=None) @property def live_period(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._live_period, attribute="live_period") - HORIZONTAL_ACCURACY: Final[int] = constants.LocationLimit.HORIZONTAL_ACCURACY + HORIZONTAL_ACCURACY: ClassVar[int] = constants.LocationLimit.HORIZONTAL_ACCURACY """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY` .. versionadded:: 20.0 """ - MIN_HEADING: Final[int] = constants.LocationLimit.MIN_HEADING + MIN_HEADING: ClassVar[int] = constants.LocationLimit.MIN_HEADING """:const:`telegram.constants.LocationLimit.MIN_HEADING` .. versionadded:: 20.0 """ - MAX_HEADING: Final[int] = constants.LocationLimit.MAX_HEADING + MAX_HEADING: ClassVar[int] = constants.LocationLimit.MAX_HEADING """:const:`telegram.constants.LocationLimit.MAX_HEADING` .. versionadded:: 20.0 diff --git a/src/telegram/_files/photosize.py b/src/telegram/_files/photosize.py index 95481031b09..31f1bad01de 100644 --- a/src/telegram/_files/photosize.py +++ b/src/telegram/_files/photosize.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram PhotoSize.""" from telegram._files._basemedium import _BaseMedium -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class PhotoSize(_BaseMedium): """This object represents one size of a photo or a file/sticker thumbnail. @@ -51,25 +52,8 @@ class PhotoSize(_BaseMedium): """ - __slots__ = ("height", "width") - - def __init__( - self, - file_id: str, - file_unique_id: str, - width: int, - height: int, - file_size: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - # Required - self.width: int = width - self.height: int = height + # Required + width: int = tg_field() + height: int = tg_field() + # Optional + file_size: int | None = tg_field(default=None) diff --git a/src/telegram/_files/sticker.py b/src/telegram/_files/sticker.py index 9a32eb34f1b..156fbf188b0 100644 --- a/src/telegram/_files/sticker.py +++ b/src/telegram/_files/sticker.py @@ -18,22 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects that represent stickers.""" -from collections.abc import Sequence -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants -from telegram._files._basethumbedmedium import _BaseThumbedMedium +from telegram._files._basemedium import _BaseMedium from telegram._files.photosize import PhotoSize from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field if TYPE_CHECKING: from telegram._files.file import File -class Sticker(_BaseThumbedMedium): +@tg_dataclass() +class Sticker(_BaseMedium): """This object represents a sticker. Objects of this class are comparable in terms of equality. Two objects of this class are @@ -130,75 +130,41 @@ class Sticker(_BaseThumbedMedium): .. versionadded:: 20.2 """ - __slots__ = ( - "custom_emoji_id", - "emoji", - "height", - "is_animated", - "is_video", - "mask_position", - "needs_repainting", - "premium_animation", - "set_name", - "type", - "width", - ) __REMOVED_API_FIELDS__ = frozenset( { "thumb", } ) - def __init__( - self, - file_id: str, - file_unique_id: str, - width: int, - height: int, - is_animated: bool, - is_video: bool, - type: str, # pylint: disable=redefined-builtin - emoji: str | None = None, - file_size: int | None = None, - set_name: str | None = None, - mask_position: "MaskPosition | None" = None, - premium_animation: "File | None" = None, - custom_emoji_id: str | None = None, - thumbnail: PhotoSize | None = None, - needs_repainting: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - thumbnail=thumbnail, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - # Required - self.width: int = width - self.height: int = height - self.is_animated: bool = is_animated - self.is_video: bool = is_video - self.type: str = enum.get_member(constants.StickerType, type, type) - # Optional - self.emoji: str | None = emoji - self.set_name: str | None = set_name - self.mask_position: MaskPosition | None = mask_position - self.premium_animation: File | None = premium_animation - self.custom_emoji_id: str | None = custom_emoji_id - self.needs_repainting: bool | None = needs_repainting - - REGULAR: Final[str] = constants.StickerType.REGULAR + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.StickerType, value, value) + + # Required + width: int = tg_field() + height: int = tg_field() + is_animated: bool = tg_field() + is_video: bool = tg_field() + type: str = tg_field(converter=_type_converter) + # Optional + emoji: str | None = tg_field(default=None) + file_size: int | None = tg_field(default=None) + set_name: str | None = tg_field(default=None) + mask_position: "MaskPosition | None" = tg_field(default=None) + premium_animation: "File | None" = tg_field(default=None) + custom_emoji_id: str | None = tg_field(default=None) + thumbnail: PhotoSize | None = tg_field(default=None) + needs_repainting: bool | None = tg_field(default=None) + + REGULAR: ClassVar[str] = constants.StickerType.REGULAR """:const:`telegram.constants.StickerType.REGULAR`""" - MASK: Final[str] = constants.StickerType.MASK + MASK: ClassVar[str] = constants.StickerType.MASK """:const:`telegram.constants.StickerType.MASK`""" - CUSTOM_EMOJI: Final[str] = constants.StickerType.CUSTOM_EMOJI + CUSTOM_EMOJI: ClassVar[str] = constants.StickerType.CUSTOM_EMOJI """:const:`telegram.constants.StickerType.CUSTOM_EMOJI`""" +@tg_dataclass() class StickerSet(TelegramObject): """This object represents a sticker set. @@ -261,37 +227,18 @@ class StickerSet(TelegramObject): .. versionadded:: 20.2 """ - __slots__ = ( - "name", - "sticker_type", - "stickers", - "thumbnail", - "title", - ) __REMOVED_API_FIELDS__ = frozenset({"contains_masks", "is_animated", "is_video", "thumb"}) - def __init__( - self, - name: str, - title: str, - stickers: Sequence[Sticker], - sticker_type: str, - thumbnail: PhotoSize | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.name: str = name - self.title: str = title - self.stickers: tuple[Sticker, ...] = parse_sequence_arg(stickers) - self.sticker_type: str = sticker_type - # Optional - self.thumbnail: PhotoSize | None = thumbnail - self._id_attrs = (self.name,) - - self._freeze() + # Required + name: str = tg_field(compare=True) + title: str = tg_field() + stickers: tuple[Sticker, ...] = tg_field(converter=parse_sequence_arg) + sticker_type: str = tg_field() + # Optional + thumbnail: PhotoSize | None = tg_field(default=None) +@tg_dataclass() class MaskPosition(TelegramObject): """This object describes the position on faces where a mask should be placed by default. @@ -323,32 +270,16 @@ class MaskPosition(TelegramObject): """ - __slots__ = ("point", "scale", "x_shift", "y_shift") - - FOREHEAD: Final[str] = constants.MaskPosition.FOREHEAD + FOREHEAD: ClassVar[str] = constants.MaskPosition.FOREHEAD """:const:`telegram.constants.MaskPosition.FOREHEAD`""" - EYES: Final[str] = constants.MaskPosition.EYES + EYES: ClassVar[str] = constants.MaskPosition.EYES """:const:`telegram.constants.MaskPosition.EYES`""" - MOUTH: Final[str] = constants.MaskPosition.MOUTH + MOUTH: ClassVar[str] = constants.MaskPosition.MOUTH """:const:`telegram.constants.MaskPosition.MOUTH`""" - CHIN: Final[str] = constants.MaskPosition.CHIN + CHIN: ClassVar[str] = constants.MaskPosition.CHIN """:const:`telegram.constants.MaskPosition.CHIN`""" - def __init__( - self, - point: str, - x_shift: float, - y_shift: float, - scale: float, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.point: str = point - self.x_shift: float = x_shift - self.y_shift: float = y_shift - self.scale: float = scale - - self._id_attrs = (self.point, self.x_shift, self.y_shift, self.scale) - - self._freeze() + point: str = tg_field(compare=True) + x_shift: float = tg_field(compare=True) + y_shift: float = tg_field(compare=True) + scale: float = tg_field(compare=True) diff --git a/src/telegram/_files/venue.py b/src/telegram/_files/venue.py index 5b2ab2ff557..d4ff5ef6946 100644 --- a/src/telegram/_files/venue.py +++ b/src/telegram/_files/venue.py @@ -20,9 +20,10 @@ from telegram._files.location import Location from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class Venue(TelegramObject): """This object represents a venue. @@ -59,40 +60,12 @@ class Venue(TelegramObject): """ - __slots__ = ( - "address", - "foursquare_id", - "foursquare_type", - "google_place_id", - "google_place_type", - "location", - "title", - ) - - def __init__( - self, - location: Location, - title: str, - address: str, - foursquare_id: str | None = None, - foursquare_type: str | None = None, - google_place_id: str | None = None, - google_place_type: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Required - self.location: Location = location - self.title: str = title - self.address: str = address - # Optionals - self.foursquare_id: str | None = foursquare_id - self.foursquare_type: str | None = foursquare_type - self.google_place_id: str | None = google_place_id - self.google_place_type: str | None = google_place_type - - self._id_attrs = (self.location, self.title) - - self._freeze() + # Required + location: Location = tg_field(compare=True) + title: str = tg_field(compare=True) + address: str = tg_field() + # Optional + foursquare_id: str | None = tg_field(default=None) + foursquare_type: str | None = tg_field(default=None) + google_place_id: str | None = tg_field(default=None) + google_place_type: str | None = tg_field(default=None) diff --git a/src/telegram/_files/video.py b/src/telegram/_files/video.py index 45f83153f5a..b7466cc1a64 100644 --- a/src/telegram/_files/video.py +++ b/src/telegram/_files/video.py @@ -19,17 +19,17 @@ """This module contains an object that represents a Telegram Video.""" import datetime as dtm -from collections.abc import Sequence -from telegram._files._basethumbedmedium import _BaseThumbedMedium +from telegram._files._basemedium import _BaseMedium from telegram._files.photosize import PhotoSize from telegram._files.videoquality import VideoQuality from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod -class Video(_BaseThumbedMedium): +@tg_dataclass() +class Video(_BaseMedium): """This object represents a video file. Objects of this class are comparable in terms of equality. Two objects of this class are @@ -107,52 +107,20 @@ class Video(_BaseThumbedMedium): .. versionadded:: 22.7 """ - __slots__ = ( - "_duration", - "_start_timestamp", - "cover", - "file_name", - "height", - "mime_type", - "qualities", - "width", + # Required + width: int = tg_field() + height: int = tg_field() + _duration: dtm.timedelta = tg_field(alias="duration", converter=to_timedelta) + # Optional + mime_type: str | None = tg_field(default=None) + file_size: int | None = tg_field(default=None) + file_name: str | None = tg_field(default=None) + thumbnail: PhotoSize | None = tg_field(default=None) + cover: tuple[PhotoSize, ...] = tg_field(default=None, converter=parse_sequence_arg) + _start_timestamp: dtm.timedelta | None = tg_field( + default=None, alias="start_timestamp", converter=to_timedelta ) - - def __init__( - self, - file_id: str, - file_unique_id: str, - width: int, - height: int, - duration: TimePeriod, - mime_type: str | None = None, - file_size: int | None = None, - file_name: str | None = None, - thumbnail: PhotoSize | None = None, - cover: Sequence[PhotoSize] | None = None, - start_timestamp: TimePeriod | None = None, - qualities: Sequence[VideoQuality] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - thumbnail=thumbnail, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - # Required - self.width: int = width - self.height: int = height - self._duration: dtm.timedelta = to_timedelta(duration) - # Optional - self.mime_type: str | None = mime_type - self.file_name: str | None = file_name - self.cover: Sequence[PhotoSize] | None = parse_sequence_arg(cover) - self._start_timestamp: dtm.timedelta | None = to_timedelta(start_timestamp) - self.qualities: Sequence[VideoQuality] | None = parse_sequence_arg(qualities) + qualities: tuple[VideoQuality, ...] = tg_field(default=None, converter=parse_sequence_arg) @property def duration(self) -> int | dtm.timedelta: diff --git a/src/telegram/_files/videonote.py b/src/telegram/_files/videonote.py index edd4edc5d2b..85595957809 100644 --- a/src/telegram/_files/videonote.py +++ b/src/telegram/_files/videonote.py @@ -20,14 +20,15 @@ import datetime as dtm -from telegram._files._basethumbedmedium import _BaseThumbedMedium +from telegram._files._basemedium import _BaseMedium from telegram._files.photosize import PhotoSize from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod -class VideoNote(_BaseThumbedMedium): +@tg_dataclass() +class VideoNote(_BaseMedium): """This object represents a video message (available in Telegram apps as of v.4.0). Objects of this class are comparable in terms of equality. Two objects of this class are @@ -74,30 +75,12 @@ class VideoNote(_BaseThumbedMedium): """ - __slots__ = ("_duration", "length") - - def __init__( - self, - file_id: str, - file_unique_id: str, - length: int, - duration: TimePeriod, - file_size: int | None = None, - thumbnail: PhotoSize | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - thumbnail=thumbnail, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - # Required - self.length: int = length - self._duration: dtm.timedelta = to_timedelta(duration) + # Required + length: int = tg_field() + _duration: dtm.timedelta = tg_field(alias="duration", converter=to_timedelta) + # Optional + file_size: int | None = tg_field(default=None) + thumbnail: PhotoSize | None = tg_field(default=None) @property def duration(self) -> int | dtm.timedelta: diff --git a/src/telegram/_files/videoquality.py b/src/telegram/_files/videoquality.py index 1a0a0c6fc38..f6cab99838b 100644 --- a/src/telegram/_files/videoquality.py +++ b/src/telegram/_files/videoquality.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram VideoQuality.""" from telegram._files._basemedium import _BaseMedium -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class VideoQuality(_BaseMedium): """This object represents a video file of a specific quality. @@ -56,27 +57,9 @@ class VideoQuality(_BaseMedium): """ - __slots__ = ("codec", "height", "width") - - def __init__( - self, - file_id: str, - file_unique_id: str, - width: int, - height: int, - codec: str, - file_size: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - # Required - self.width: int = width - self.height: int = height - self.codec: str = codec + # Required + width: int = tg_field() + height: int = tg_field() + codec: str = tg_field() + # Optional + file_size: int | None = tg_field(default=None) diff --git a/src/telegram/_files/voice.py b/src/telegram/_files/voice.py index fb7691fe2df..6adf202cb2c 100644 --- a/src/telegram/_files/voice.py +++ b/src/telegram/_files/voice.py @@ -22,10 +22,11 @@ from telegram._files._basemedium import _BaseMedium from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod +@tg_dataclass() class Voice(_BaseMedium): """This object represents a voice note. @@ -62,29 +63,11 @@ class Voice(_BaseMedium): """ - __slots__ = ("_duration", "mime_type") - - def __init__( - self, - file_id: str, - file_unique_id: str, - duration: TimePeriod, - mime_type: str | None = None, - file_size: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__( - file_id=file_id, - file_unique_id=file_unique_id, - file_size=file_size, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - # Required - self._duration: dtm.timedelta = to_timedelta(duration) - # Optional - self.mime_type: str | None = mime_type + # Required + _duration: dtm.timedelta = tg_field(alias="duration", converter=to_timedelta) + # Optional + mime_type: str | None = tg_field(default=None) + file_size: int | None = tg_field(default=None) @property def duration(self) -> int | dtm.timedelta: diff --git a/src/telegram/_games/callbackgame.py b/src/telegram/_games/callbackgame.py index 093a5449b81..19e46393e4f 100644 --- a/src/telegram/_games/callbackgame.py +++ b/src/telegram/_games/callbackgame.py @@ -19,15 +19,9 @@ """This module contains an object that represents a Telegram CallbackGame.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass +@tg_dataclass() class CallbackGame(TelegramObject): """A placeholder, currently holds no information. Use BotFather to set up your game.""" - - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(api_kwargs=api_kwargs) - - self._freeze() diff --git a/src/telegram/_games/game.py b/src/telegram/_games/game.py index 7135628f501..22675446565 100644 --- a/src/telegram/_games/game.py +++ b/src/telegram/_games/game.py @@ -18,17 +18,16 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Game.""" -from collections.abc import Sequence - from telegram._files.animation import Animation from telegram._files.photosize import PhotoSize from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.strings import TextEncoding -from telegram._utils.types import JSONDict +@tg_dataclass() class Game(TelegramObject): """ This object represents a game. Use `BotFather `_ to create and edit @@ -86,39 +85,14 @@ class Game(TelegramObject): """ - __slots__ = ( - "animation", - "description", - "photo", - "text", - "text_entities", - "title", - ) - - def __init__( - self, - title: str, - description: str, - photo: Sequence[PhotoSize], - text: str | None = None, - text_entities: Sequence[MessageEntity] | None = None, - animation: Animation | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.title: str = title - self.description: str = description - self.photo: tuple[PhotoSize, ...] = parse_sequence_arg(photo) - # Optionals - self.text: str | None = text - self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) - self.animation: Animation | None = animation - - self._id_attrs = (self.title, self.description, self.photo) - - self._freeze() + # Required + title: str = tg_field(compare=True) + description: str = tg_field(compare=True) + photo: tuple[PhotoSize, ...] = tg_field(compare=True, converter=parse_sequence_arg) + # Optional + text: str | None = tg_field(default=None) + text_entities: tuple[MessageEntity, ...] = tg_field(default=None, converter=parse_sequence_arg) + animation: Animation | None = tg_field(default=None) def parse_text_entity(self, entity: MessageEntity) -> str: """Returns the text from a given :class:`telegram.MessageEntity`. diff --git a/src/telegram/_games/gamehighscore.py b/src/telegram/_games/gamehighscore.py index 3b13a2ea887..353ada8f5d9 100644 --- a/src/telegram/_games/gamehighscore.py +++ b/src/telegram/_games/gamehighscore.py @@ -20,9 +20,10 @@ from telegram._telegramobject import TelegramObject from telegram._user import User -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class GameHighScore(TelegramObject): """This object represents one row of the high scores table for a game. @@ -41,16 +42,6 @@ class GameHighScore(TelegramObject): """ - __slots__ = ("position", "score", "user") - - def __init__( - self, position: int, user: User, score: int, *, api_kwargs: JSONDict | None = None - ): - super().__init__(api_kwargs=api_kwargs) - self.position: int = position - self.user: User = user - self.score: int = score - - self._id_attrs = (self.position, self.user, self.score) - - self._freeze() + position: int = tg_field(compare=True) + user: User = tg_field(compare=True) + score: int = tg_field(compare=True) diff --git a/src/telegram/_inline/inlinekeyboardbutton.py b/src/telegram/_inline/inlinekeyboardbutton.py index 9a3cf7e062b..db798c6e913 100644 --- a/src/telegram/_inline/inlinekeyboardbutton.py +++ b/src/telegram/_inline/inlinekeyboardbutton.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram InlineKeyboardButton.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._copytextbutton import CopyTextButton @@ -26,10 +26,11 @@ from telegram._loginurl import LoginUrl from telegram._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._webappinfo import WebAppInfo +@tg_dataclass() class InlineKeyboardButton(TelegramObject): """This object represents one button of an inline keyboard. @@ -267,78 +268,23 @@ class InlineKeyboardButton(TelegramObject): .. versionadded:: 22.7 """ - __slots__ = ( - "callback_data", - "callback_game", - "copy_text", - "icon_custom_emoji_id", - "login_url", - "pay", - "style", - "switch_inline_query", - "switch_inline_query_chosen_chat", - "switch_inline_query_current_chat", - "text", - "url", - "web_app", + # Required + text: str = tg_field( + compare=True, ) - - def __init__( - self, - text: str, - url: str | None = None, - callback_data: str | object | None = None, - switch_inline_query: str | None = None, - switch_inline_query_current_chat: str | None = None, - callback_game: CallbackGame | None = None, - pay: bool | None = None, - login_url: LoginUrl | None = None, - web_app: WebAppInfo | None = None, - switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat | None = None, - copy_text: CopyTextButton | None = None, - style: str | None = None, - icon_custom_emoji_id: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.text: str = text - - # Optionals - self.url: str | None = url - self.login_url: LoginUrl | None = login_url - self.callback_data: str | object | None = callback_data - self.switch_inline_query: str | None = switch_inline_query - self.switch_inline_query_current_chat: str | None = switch_inline_query_current_chat - self.callback_game: CallbackGame | None = callback_game - self.pay: bool | None = pay - self.web_app: WebAppInfo | None = web_app - self.switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat | None = ( - switch_inline_query_chosen_chat - ) - self.copy_text: CopyTextButton | None = copy_text - self.style: str | None = style - self.icon_custom_emoji_id: str | None = icon_custom_emoji_id - self._id_attrs = () - self._set_id_attrs() - - self._freeze() - - def _set_id_attrs(self) -> None: - self._id_attrs = ( - self.text, - self.url, - self.login_url, - self.callback_data, - self.web_app, - self.switch_inline_query, - self.switch_inline_query_current_chat, - self.callback_game, - self.pay, - self.style, - self.icon_custom_emoji_id, - ) + # Optional + url: str | None = tg_field(compare=True, default=None) + callback_data: str | object | None = tg_field(compare=True, default=None) + switch_inline_query: str | None = tg_field(compare=True, default=None) + switch_inline_query_current_chat: str | None = tg_field(compare=True, default=None) + callback_game: CallbackGame | None = tg_field(compare=True, default=None) + pay: bool | None = tg_field(compare=True, default=None) + login_url: LoginUrl | None = tg_field(compare=True, default=None) + web_app: WebAppInfo | None = tg_field(compare=True, default=None) + switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat | None = tg_field(default=None) + copy_text: CopyTextButton | None = tg_field(default=None) + style: str | None = tg_field(compare=True, default=None) + icon_custom_emoji_id: str | None = tg_field(compare=True, default=None) def update_callback_data(self, callback_data: str | object) -> None: """ @@ -350,16 +296,15 @@ def update_callback_data(self, callback_data: str | object) -> None: Args: callback_data (:class:`object`): The new callback data. """ - with self._unfrozen(): - self.callback_data = callback_data - self._set_id_attrs() - MIN_CALLBACK_DATA: Final[int] = constants.InlineKeyboardButtonLimit.MIN_CALLBACK_DATA + object.__setattr__(self, "callback_data", callback_data) + + MIN_CALLBACK_DATA: ClassVar[int] = constants.InlineKeyboardButtonLimit.MIN_CALLBACK_DATA """:const:`telegram.constants.InlineKeyboardButtonLimit.MIN_CALLBACK_DATA` .. versionadded:: 20.0 """ - MAX_CALLBACK_DATA: Final[int] = constants.InlineKeyboardButtonLimit.MAX_CALLBACK_DATA + MAX_CALLBACK_DATA: ClassVar[int] = constants.InlineKeyboardButtonLimit.MAX_CALLBACK_DATA """:const:`telegram.constants.InlineKeyboardButtonLimit.MAX_CALLBACK_DATA` .. versionadded:: 20.0 diff --git a/src/telegram/_inline/inlinekeyboardmarkup.py b/src/telegram/_inline/inlinekeyboardmarkup.py index a3162eae0b4..61caa6979ca 100644 --- a/src/telegram/_inline/inlinekeyboardmarkup.py +++ b/src/telegram/_inline/inlinekeyboardmarkup.py @@ -22,10 +22,11 @@ from telegram._inline.inlinekeyboardbutton import InlineKeyboardButton from telegram._telegramobject import TelegramObject +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.markup import check_keyboard_type -from telegram._utils.types import JSONDict +@tg_dataclass() class InlineKeyboardMarkup(TelegramObject): """ This object represents an inline keyboard that appears right next to the message it belongs to. @@ -64,28 +65,21 @@ class InlineKeyboardMarkup(TelegramObject): """ - __slots__ = ("inline_keyboard",) - - def __init__( - self, - inline_keyboard: Sequence[Sequence[InlineKeyboardButton]], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - if not check_keyboard_type(inline_keyboard): + @staticmethod + def _inline_keyboard_converter( + value: Sequence[Sequence[InlineKeyboardButton]], + ) -> tuple[tuple[InlineKeyboardButton, ...], ...]: + if not check_keyboard_type(value): raise ValueError( "The parameter `inline_keyboard` should be a sequence of sequences of " "InlineKeyboardButtons" ) - # Required - self.inline_keyboard: tuple[tuple[InlineKeyboardButton, ...], ...] = tuple( - tuple(row) for row in inline_keyboard - ) - - self._id_attrs = (self.inline_keyboard,) + return tuple(tuple(row) for row in value) - self._freeze() + # Required + inline_keyboard: tuple[tuple[InlineKeyboardButton, ...], ...] = tg_field( + compare=True, converter=_inline_keyboard_converter + ) @classmethod def from_button(cls, button: InlineKeyboardButton, **kwargs: object) -> "InlineKeyboardMarkup": diff --git a/src/telegram/_inline/inlinequery.py b/src/telegram/_inline/inlinequery.py index b825059d667..f8dbfd2c188 100644 --- a/src/telegram/_inline/inlinequery.py +++ b/src/telegram/_inline/inlinequery.py @@ -20,12 +20,13 @@ """This module contains an object that represents a Telegram InlineQuery.""" from collections.abc import Callable, Sequence -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton from telegram._telegramobject import TelegramObject from telegram._user import User +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput, TimePeriod @@ -34,6 +35,7 @@ from telegram._files.location import Location +@tg_dataclass() class InlineQuery(TelegramObject): """ This object represents an incoming inline query. When the user sends an empty query, your bot @@ -103,33 +105,14 @@ class InlineQuery(TelegramObject): """ - __slots__ = ("chat_type", "from_user", "id", "location", "offset", "query") - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - from_user: User, - query: str, - offset: str, - location: "Location | None" = None, - chat_type: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.id: str = id - self.from_user: User = from_user - self.query: str = query - self.offset: str = offset - - # Optional - self.location: Location | None = location - self.chat_type: str | None = chat_type - - self._id_attrs = (self.id,) - - self._freeze() + # Required + id: str = tg_field(compare=True) + from_user: User = tg_field() + query: str = tg_field() + offset: str = tg_field() + # Optional + location: "Location | None" = tg_field(default=None) + chat_type: str | None = tg_field(default=None) async def answer( self, @@ -192,17 +175,17 @@ async def answer( api_kwargs=api_kwargs, ) - MAX_RESULTS: Final[int] = constants.InlineQueryLimit.RESULTS + MAX_RESULTS: ClassVar[int] = constants.InlineQueryLimit.RESULTS """:const:`telegram.constants.InlineQueryLimit.RESULTS` .. versionadded:: 13.2 """ - MAX_OFFSET_LENGTH: Final[int] = constants.InlineQueryLimit.MAX_OFFSET_LENGTH + MAX_OFFSET_LENGTH: ClassVar[int] = constants.InlineQueryLimit.MAX_OFFSET_LENGTH """:const:`telegram.constants.InlineQueryLimit.MAX_OFFSET_LENGTH` .. versionadded:: 20.0 """ - MAX_QUERY_LENGTH: Final[int] = constants.InlineQueryLimit.MAX_QUERY_LENGTH + MAX_QUERY_LENGTH: ClassVar[int] = constants.InlineQueryLimit.MAX_QUERY_LENGTH """:const:`telegram.constants.InlineQueryLimit.MAX_QUERY_LENGTH` .. versionadded:: 20.0 diff --git a/src/telegram/_inline/inlinequeryresult.py b/src/telegram/_inline/inlinequeryresult.py index 7e708ebbfe2..87cf761a27f 100644 --- a/src/telegram/_inline/inlinequeryresult.py +++ b/src/telegram/_inline/inlinequeryresult.py @@ -16,17 +16,17 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=redefined-builtin """This module contains the classes that represent Telegram InlineQueryResult.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class InlineQueryResult(TelegramObject): """Baseclass for the InlineQueryResult* classes. @@ -54,25 +54,20 @@ class InlineQueryResult(TelegramObject): """ - __slots__ = ("id", "type") + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.InlineQueryResultType, value, value) - def __init__(self, type: str, id: str, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) + # Required + type: str = tg_field(converter=_type_converter) + id: str = tg_field(compare=True) - # Required - self.type: str = enum.get_member(constants.InlineQueryResultType, type, type) - self.id: str = str(id) - - self._id_attrs = (self.id,) - - self._freeze() - - MIN_ID_LENGTH: Final[int] = constants.InlineQueryResultLimit.MIN_ID_LENGTH + MIN_ID_LENGTH: ClassVar[int] = constants.InlineQueryResultLimit.MIN_ID_LENGTH """:const:`telegram.constants.InlineQueryResultLimit.MIN_ID_LENGTH` .. versionadded:: 20.0 """ - MAX_ID_LENGTH: Final[int] = constants.InlineQueryResultLimit.MAX_ID_LENGTH + MAX_ID_LENGTH: ClassVar[int] = constants.InlineQueryResultLimit.MAX_ID_LENGTH """:const:`telegram.constants.InlineQueryResultLimit.MAX_ID_LENGTH` .. versionadded:: 20.0 diff --git a/src/telegram/_inline/inlinequeryresultarticle.py b/src/telegram/_inline/inlinequeryresultarticle.py index cca861b76f7..8d52285af20 100644 --- a/src/telegram/_inline/inlinequeryresultarticle.py +++ b/src/telegram/_inline/inlinequeryresultarticle.py @@ -22,13 +22,14 @@ from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultArticle(InlineQueryResult): """This object represents a Telegram InlineQueryResultArticle. @@ -89,41 +90,15 @@ class InlineQueryResultArticle(InlineQueryResult): """ - __slots__ = ( - "description", - "input_message_content", - "reply_markup", - "thumbnail_height", - "thumbnail_url", - "thumbnail_width", - "title", - "url", - ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - title: str, - input_message_content: "InputMessageContent", - reply_markup: InlineKeyboardMarkup | None = None, - url: str | None = None, - description: str | None = None, - thumbnail_url: str | None = None, - thumbnail_width: int | None = None, - thumbnail_height: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.ARTICLE, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.title: str = title - self.input_message_content: InputMessageContent = input_message_content - - # Optional - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.url: str | None = url - self.description: str | None = description - self.thumbnail_url: str | None = thumbnail_url - self.thumbnail_width: int | None = thumbnail_width - self.thumbnail_height: int | None = thumbnail_height + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.ARTICLE) + # Required + title: str = tg_field() + input_message_content: "InputMessageContent" = tg_field() + # Optional + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + url: str | None = tg_field(default=None) + description: str | None = tg_field(default=None) + thumbnail_url: str | None = tg_field(default=None) + thumbnail_width: int | None = tg_field(default=None) + thumbnail_height: int | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultaudio.py b/src/telegram/_inline/inlinequeryresultaudio.py index 02ce1a1eea1..e78b0d88357 100644 --- a/src/telegram/_inline/inlinequeryresultaudio.py +++ b/src/telegram/_inline/inlinequeryresultaudio.py @@ -19,22 +19,23 @@ """This module contains the classes that represent Telegram InlineQueryResultAudio.""" import datetime as dtm -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput, TimePeriod +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultAudio(InlineQueryResult): """ Represents a link to an mp3 audio file. By default, this audio file will be sent by the user. @@ -98,47 +99,23 @@ class InlineQueryResultAudio(InlineQueryResult): """ - __slots__ = ( - "_audio_duration", - "audio_url", - "caption", - "caption_entities", - "input_message_content", - "parse_mode", - "performer", - "reply_markup", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.AUDIO) + # Required + audio_url: str = tg_field() + title: str = tg_field() + # Optional + performer: str | None = tg_field(default=None) + _audio_duration: dtm.timedelta | None = tg_field( + default=None, alias="audio_duration", converter=to_timedelta + ) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - audio_url: str, - title: str, - performer: str | None = None, - audio_duration: TimePeriod | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.AUDIO, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.audio_url: str = audio_url - self.title: str = title - - # Optionals - self.performer: str | None = performer - self._audio_duration: dtm.timedelta | None = to_timedelta(audio_duration) - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content @property def audio_duration(self) -> int | dtm.timedelta | None: diff --git a/src/telegram/_inline/inlinequeryresultcachedaudio.py b/src/telegram/_inline/inlinequeryresultcachedaudio.py index d925d92d33a..e1f52d3270e 100644 --- a/src/telegram/_inline/inlinequeryresultcachedaudio.py +++ b/src/telegram/_inline/inlinequeryresultcachedaudio.py @@ -18,21 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedAudio.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultCachedAudio(InlineQueryResult): """ Represents a link to an mp3 audio file stored on the Telegram servers. By default, this audio @@ -83,35 +84,15 @@ class InlineQueryResultCachedAudio(InlineQueryResult): """ - __slots__ = ( - "audio_file_id", - "caption", - "caption_entities", - "input_message_content", - "parse_mode", - "reply_markup", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.AUDIO) + # Required + audio_file_id: str = tg_field() + # Optional + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - audio_file_id: str, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.AUDIO, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.audio_file_id: str = audio_file_id - - # Optionals - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content diff --git a/src/telegram/_inline/inlinequeryresultcacheddocument.py b/src/telegram/_inline/inlinequeryresultcacheddocument.py index 0f7771ccba7..4beda76b3c6 100644 --- a/src/telegram/_inline/inlinequeryresultcacheddocument.py +++ b/src/telegram/_inline/inlinequeryresultcacheddocument.py @@ -18,21 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedDocument.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultCachedDocument(InlineQueryResult): """ Represents a link to a file stored on the Telegram servers. By default, this file will be sent @@ -87,41 +88,17 @@ class InlineQueryResultCachedDocument(InlineQueryResult): """ - __slots__ = ( - "caption", - "caption_entities", - "description", - "document_file_id", - "input_message_content", - "parse_mode", - "reply_markup", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.DOCUMENT) + # Required + title: str = tg_field() + document_file_id: str = tg_field() + # Optional + description: str | None = tg_field(default=None) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - title: str, - document_file_id: str, - description: str | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.DOCUMENT, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.title: str = title - self.document_file_id: str = document_file_id - - # Optionals - self.description: str | None = description - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content diff --git a/src/telegram/_inline/inlinequeryresultcachedgif.py b/src/telegram/_inline/inlinequeryresultcachedgif.py index e9fbd38a37c..79513be2ef1 100644 --- a/src/telegram/_inline/inlinequeryresultcachedgif.py +++ b/src/telegram/_inline/inlinequeryresultcachedgif.py @@ -18,21 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedGif.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultCachedGif(InlineQueryResult): """ Represents a link to an animated GIF file stored on the Telegram servers. By default, this @@ -92,41 +93,17 @@ class InlineQueryResultCachedGif(InlineQueryResult): """ - __slots__ = ( - "caption", - "caption_entities", - "gif_file_id", - "input_message_content", - "parse_mode", - "reply_markup", - "show_caption_above_media", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.GIF) + # Required + gif_file_id: str = tg_field() + # Optional + title: str | None = tg_field(default=None) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - gif_file_id: str, - title: str | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - show_caption_above_media: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.GIF, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.gif_file_id: str = gif_file_id - - # Optionals - self.title: str | None = title - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.show_caption_above_media: bool | None = show_caption_above_media + show_caption_above_media: bool | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultcachedmpeg4gif.py b/src/telegram/_inline/inlinequeryresultcachedmpeg4gif.py index 6832e7febdd..98e99552e0a 100644 --- a/src/telegram/_inline/inlinequeryresultcachedmpeg4gif.py +++ b/src/telegram/_inline/inlinequeryresultcachedmpeg4gif.py @@ -18,21 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): """ Represents a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the @@ -92,41 +93,17 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): """ - __slots__ = ( - "caption", - "caption_entities", - "input_message_content", - "mpeg4_file_id", - "parse_mode", - "reply_markup", - "show_caption_above_media", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.MPEG4GIF) + # Required + mpeg4_file_id: str = tg_field() + # Optional + title: str | None = tg_field(default=None) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - mpeg4_file_id: str, - title: str | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - show_caption_above_media: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.MPEG4GIF, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.mpeg4_file_id: str = mpeg4_file_id - - # Optionals - self.title: str | None = title - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.show_caption_above_media: bool | None = show_caption_above_media + show_caption_above_media: bool | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultcachedphoto.py b/src/telegram/_inline/inlinequeryresultcachedphoto.py index 774fe3257a4..4e64c8af1c3 100644 --- a/src/telegram/_inline/inlinequeryresultcachedphoto.py +++ b/src/telegram/_inline/inlinequeryresultcachedphoto.py @@ -18,21 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultPhoto""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultCachedPhoto(InlineQueryResult): """ Represents a link to a photo stored on the Telegram servers. By default, this photo will be @@ -94,44 +95,18 @@ class InlineQueryResultCachedPhoto(InlineQueryResult): """ - __slots__ = ( - "caption", - "caption_entities", - "description", - "input_message_content", - "parse_mode", - "photo_file_id", - "reply_markup", - "show_caption_above_media", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.PHOTO) + # Required + photo_file_id: str = tg_field() + # Optional + title: str | None = tg_field(default=None) + description: str | None = tg_field(default=None) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - photo_file_id: str, - title: str | None = None, - description: str | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - show_caption_above_media: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.PHOTO, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.photo_file_id: str = photo_file_id - - # Optionals - self.title: str | None = title - self.description: str | None = description - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.show_caption_above_media: bool | None = show_caption_above_media + show_caption_above_media: bool | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultcachedsticker.py b/src/telegram/_inline/inlinequeryresultcachedsticker.py index 437a9ceef69..83768cb9482 100644 --- a/src/telegram/_inline/inlinequeryresultcachedsticker.py +++ b/src/telegram/_inline/inlinequeryresultcachedsticker.py @@ -22,13 +22,14 @@ from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultCachedSticker(InlineQueryResult): """ Represents a link to a sticker stored on the Telegram servers. By default, this sticker will @@ -60,22 +61,10 @@ class InlineQueryResultCachedSticker(InlineQueryResult): """ - __slots__ = ("input_message_content", "reply_markup", "sticker_file_id") - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - sticker_file_id: str, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.STICKER, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.sticker_file_id: str = sticker_file_id - - # Optionals - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.STICKER) + # Required + sticker_file_id: str = tg_field() + # Optional + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultcachedvideo.py b/src/telegram/_inline/inlinequeryresultcachedvideo.py index 19b602da112..d3c01fdb6a7 100644 --- a/src/telegram/_inline/inlinequeryresultcachedvideo.py +++ b/src/telegram/_inline/inlinequeryresultcachedvideo.py @@ -18,21 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedVideo.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultCachedVideo(InlineQueryResult): """ Represents a link to a video file stored on the Telegram servers. By default, this video file @@ -90,44 +91,18 @@ class InlineQueryResultCachedVideo(InlineQueryResult): """ - __slots__ = ( - "caption", - "caption_entities", - "description", - "input_message_content", - "parse_mode", - "reply_markup", - "show_caption_above_media", - "title", - "video_file_id", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.VIDEO) + # Required + video_file_id: str = tg_field() + title: str = tg_field() + # Optional + description: str | None = tg_field(default=None) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - video_file_id: str, - title: str, - description: str | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - show_caption_above_media: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.VIDEO, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.video_file_id: str = video_file_id - self.title: str = title - - # Optionals - self.description: str | None = description - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.show_caption_above_media: bool | None = show_caption_above_media + show_caption_above_media: bool | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultcachedvoice.py b/src/telegram/_inline/inlinequeryresultcachedvoice.py index c2114cb8906..2e8c9ea0944 100644 --- a/src/telegram/_inline/inlinequeryresultcachedvoice.py +++ b/src/telegram/_inline/inlinequeryresultcachedvoice.py @@ -18,21 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultCachedVoice.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultCachedVoice(InlineQueryResult): """ Represents a link to a voice message stored on the Telegram servers. By default, this voice @@ -85,38 +86,16 @@ class InlineQueryResultCachedVoice(InlineQueryResult): """ - __slots__ = ( - "caption", - "caption_entities", - "input_message_content", - "parse_mode", - "reply_markup", - "title", - "voice_file_id", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.VOICE) + # Required + voice_file_id: str = tg_field() + title: str = tg_field() + # Optional + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - voice_file_id: str, - title: str, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.VOICE, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.voice_file_id: str = voice_file_id - self.title: str = title - - # Optionals - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content diff --git a/src/telegram/_inline/inlinequeryresultcontact.py b/src/telegram/_inline/inlinequeryresultcontact.py index 53c5670820b..8bc9b256032 100644 --- a/src/telegram/_inline/inlinequeryresultcontact.py +++ b/src/telegram/_inline/inlinequeryresultcontact.py @@ -22,13 +22,14 @@ from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultContact(InlineQueryResult): """ Represents a contact with a phone number. By default, this contact will be sent by the user. @@ -87,44 +88,16 @@ class InlineQueryResultContact(InlineQueryResult): """ - __slots__ = ( - "first_name", - "input_message_content", - "last_name", - "phone_number", - "reply_markup", - "thumbnail_height", - "thumbnail_url", - "thumbnail_width", - "vcard", - ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - phone_number: str, - first_name: str, - last_name: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - vcard: str | None = None, - thumbnail_url: str | None = None, - thumbnail_width: int | None = None, - thumbnail_height: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.CONTACT, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.phone_number: str = phone_number - self.first_name: str = first_name - - # Optionals - self.last_name: str | None = last_name - self.vcard: str | None = vcard - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.thumbnail_url: str | None = thumbnail_url - self.thumbnail_width: int | None = thumbnail_width - self.thumbnail_height: int | None = thumbnail_height + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.CONTACT) + # Required + phone_number: str = tg_field() + first_name: str = tg_field() + # Optional + last_name: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + vcard: str | None = tg_field(default=None) + thumbnail_url: str | None = tg_field(default=None) + thumbnail_width: int | None = tg_field(default=None) + thumbnail_height: int | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultdocument.py b/src/telegram/_inline/inlinequeryresultdocument.py index 1bc2d97a887..8860ef8ff1f 100644 --- a/src/telegram/_inline/inlinequeryresultdocument.py +++ b/src/telegram/_inline/inlinequeryresultdocument.py @@ -18,21 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultDocument""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultDocument(InlineQueryResult): """ Represents a link to a file. By default, this file will be sent by the user with an optional @@ -114,53 +115,21 @@ class InlineQueryResultDocument(InlineQueryResult): """ - __slots__ = ( - "caption", - "caption_entities", - "description", - "document_url", - "input_message_content", - "mime_type", - "parse_mode", - "reply_markup", - "thumbnail_height", - "thumbnail_url", - "thumbnail_width", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.DOCUMENT) + # Required + document_url: str = tg_field() + title: str = tg_field() + mime_type: str = tg_field() + # Optional + caption: str | None = tg_field(default=None) + description: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - document_url: str, - title: str, - mime_type: str, - caption: str | None = None, - description: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - thumbnail_url: str | None = None, - thumbnail_width: int | None = None, - thumbnail_height: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.DOCUMENT, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.document_url: str = document_url - self.title: str = title - self.mime_type: str = mime_type - - # Optionals - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.description: str | None = description - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.thumbnail_url: str | None = thumbnail_url - self.thumbnail_width: int | None = thumbnail_width - self.thumbnail_height: int | None = thumbnail_height + thumbnail_url: str | None = tg_field(default=None) + thumbnail_width: int | None = tg_field(default=None) + thumbnail_height: int | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultgame.py b/src/telegram/_inline/inlinequeryresultgame.py index 579721dcc45..b96940111a9 100644 --- a/src/telegram/_inline/inlinequeryresultgame.py +++ b/src/telegram/_inline/inlinequeryresultgame.py @@ -20,10 +20,11 @@ from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram.constants import InlineQueryResultType +@tg_dataclass() class InlineQueryResultGame(InlineQueryResult): """Represents a :class:`telegram.Game`. @@ -46,20 +47,9 @@ class InlineQueryResultGame(InlineQueryResult): """ - __slots__ = ("game_short_name", "reply_markup") - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - game_short_name: str, - reply_markup: InlineKeyboardMarkup | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.GAME, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.id: str = id - self.game_short_name: str = game_short_name - - self.reply_markup: InlineKeyboardMarkup | None = reply_markup + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.GAME) + # Required + game_short_name: str = tg_field() + # Optional + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultgif.py b/src/telegram/_inline/inlinequeryresultgif.py index 67d4cdf23e5..f9115dab76f 100644 --- a/src/telegram/_inline/inlinequeryresultgif.py +++ b/src/telegram/_inline/inlinequeryresultgif.py @@ -19,22 +19,23 @@ """This module contains the classes that represent Telegram InlineQueryResultGif.""" import datetime as dtm -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput, TimePeriod +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultGif(InlineQueryResult): """ Represents a link to an animated GIF file. By default, this animated GIF file will be sent by @@ -130,59 +131,27 @@ class InlineQueryResultGif(InlineQueryResult): """ - __slots__ = ( - "_gif_duration", - "caption", - "caption_entities", - "gif_height", - "gif_url", - "gif_width", - "input_message_content", - "parse_mode", - "reply_markup", - "show_caption_above_media", - "thumbnail_mime_type", - "thumbnail_url", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.GIF) + # Required + gif_url: str = tg_field() + thumbnail_url: str = tg_field() + # Optional + gif_width: int | None = tg_field(default=None) + gif_height: int | None = tg_field(default=None) + title: str | None = tg_field(default=None) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + _gif_duration: dtm.timedelta | None = tg_field( + default=None, alias="gif_duration", converter=to_timedelta ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - gif_url: str, - thumbnail_url: str, - gif_width: int | None = None, - gif_height: int | None = None, - title: str | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - gif_duration: TimePeriod | None = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - thumbnail_mime_type: str | None = None, - show_caption_above_media: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.GIF, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.gif_url: str = gif_url - self.thumbnail_url: str = thumbnail_url - - # Optionals - self.gif_width: int | None = gif_width - self.gif_height: int | None = gif_height - self._gif_duration: dtm.timedelta | None = to_timedelta(gif_duration) - self.title: str | None = title - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.thumbnail_mime_type: str | None = thumbnail_mime_type - self.show_caption_above_media: bool | None = show_caption_above_media + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + thumbnail_mime_type: str | None = tg_field(default=None) + show_caption_above_media: bool | None = tg_field(default=None) @property def gif_duration(self) -> int | dtm.timedelta | None: diff --git a/src/telegram/_inline/inlinequeryresultlocation.py b/src/telegram/_inline/inlinequeryresultlocation.py index c4ce7db7346..263fe4cc77d 100644 --- a/src/telegram/_inline/inlinequeryresultlocation.py +++ b/src/telegram/_inline/inlinequeryresultlocation.py @@ -19,19 +19,20 @@ """This module contains the classes that represent Telegram InlineQueryResultLocation.""" import datetime as dtm -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultLocation(InlineQueryResult): """ Represents a location on a map. By default, the location will be sent by the user. @@ -126,94 +127,60 @@ class InlineQueryResultLocation(InlineQueryResult): """ - __slots__ = ( - "_live_period", - "heading", - "horizontal_accuracy", - "input_message_content", - "latitude", - "longitude", - "proximity_alert_radius", - "reply_markup", - "thumbnail_height", - "thumbnail_url", - "thumbnail_width", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=constants.InlineQueryResultType.LOCATION) + # Required + latitude: float = tg_field() + longitude: float = tg_field() + title: str = tg_field() + # Optional + _live_period: dtm.timedelta | None = tg_field( + default=None, alias="live_period", converter=to_timedelta ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - latitude: float, - longitude: float, - title: str, - live_period: TimePeriod | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - horizontal_accuracy: float | None = None, - heading: int | None = None, - proximity_alert_radius: int | None = None, - thumbnail_url: str | None = None, - thumbnail_width: int | None = None, - thumbnail_height: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(constants.InlineQueryResultType.LOCATION, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.latitude: float = latitude - self.longitude: float = longitude - self.title: str = title - - # Optionals - self._live_period: dtm.timedelta | None = to_timedelta(live_period) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.thumbnail_url: str | None = thumbnail_url - self.thumbnail_width: int | None = thumbnail_width - self.thumbnail_height: int | None = thumbnail_height - self.horizontal_accuracy: float | None = horizontal_accuracy - self.heading: int | None = heading - self.proximity_alert_radius: int | None = ( - int(proximity_alert_radius) if proximity_alert_radius else None - ) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + horizontal_accuracy: float | None = tg_field(default=None) + heading: int | None = tg_field(default=None) + proximity_alert_radius: int | None = tg_field(default=None) + thumbnail_url: str | None = tg_field(default=None) + thumbnail_width: int | None = tg_field(default=None) + thumbnail_height: int | None = tg_field(default=None) @property def live_period(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._live_period, attribute="live_period") - HORIZONTAL_ACCURACY: Final[int] = constants.LocationLimit.HORIZONTAL_ACCURACY + HORIZONTAL_ACCURACY: ClassVar[int] = constants.LocationLimit.HORIZONTAL_ACCURACY """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY` .. versionadded:: 20.0 """ - MIN_HEADING: Final[int] = constants.LocationLimit.MIN_HEADING + MIN_HEADING: ClassVar[int] = constants.LocationLimit.MIN_HEADING """:const:`telegram.constants.LocationLimit.MIN_HEADING` .. versionadded:: 20.0 """ - MAX_HEADING: Final[int] = constants.LocationLimit.MAX_HEADING + MAX_HEADING: ClassVar[int] = constants.LocationLimit.MAX_HEADING """:const:`telegram.constants.LocationLimit.MAX_HEADING` .. versionadded:: 20.0 """ - MIN_LIVE_PERIOD: Final[int] = constants.LocationLimit.MIN_LIVE_PERIOD + MIN_LIVE_PERIOD: ClassVar[int] = constants.LocationLimit.MIN_LIVE_PERIOD """:const:`telegram.constants.LocationLimit.MIN_LIVE_PERIOD` .. versionadded:: 20.0 """ - MAX_LIVE_PERIOD: Final[int] = constants.LocationLimit.MAX_LIVE_PERIOD + MAX_LIVE_PERIOD: ClassVar[int] = constants.LocationLimit.MAX_LIVE_PERIOD """:const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD` .. versionadded:: 20.0 """ - MIN_PROXIMITY_ALERT_RADIUS: Final[int] = constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS + MIN_PROXIMITY_ALERT_RADIUS: ClassVar[int] = constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS """:const:`telegram.constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS` .. versionadded:: 20.0 """ - MAX_PROXIMITY_ALERT_RADIUS: Final[int] = constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS + MAX_PROXIMITY_ALERT_RADIUS: ClassVar[int] = constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS """:const:`telegram.constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS` .. versionadded:: 20.0 diff --git a/src/telegram/_inline/inlinequeryresultmpeg4gif.py b/src/telegram/_inline/inlinequeryresultmpeg4gif.py index 817eda536d7..6662af478b6 100644 --- a/src/telegram/_inline/inlinequeryresultmpeg4gif.py +++ b/src/telegram/_inline/inlinequeryresultmpeg4gif.py @@ -19,22 +19,23 @@ """This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif.""" import datetime as dtm -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput, TimePeriod +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultMpeg4Gif(InlineQueryResult): """ Represents a link to a video animation (H.264/MPEG-4 AVC video without sound). By default, this @@ -132,59 +133,27 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): .. versionadded:: 21.3 """ - __slots__ = ( - "_mpeg4_duration", - "caption", - "caption_entities", - "input_message_content", - "mpeg4_height", - "mpeg4_url", - "mpeg4_width", - "parse_mode", - "reply_markup", - "show_caption_above_media", - "thumbnail_mime_type", - "thumbnail_url", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.MPEG4GIF) + # Required + mpeg4_url: str = tg_field() + thumbnail_url: str = tg_field() + # Optional + mpeg4_width: int | None = tg_field(default=None) + mpeg4_height: int | None = tg_field(default=None) + title: str | None = tg_field(default=None) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + _mpeg4_duration: dtm.timedelta | None = tg_field( + default=None, alias="mpeg4_duration", converter=to_timedelta ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - mpeg4_url: str, - thumbnail_url: str, - mpeg4_width: int | None = None, - mpeg4_height: int | None = None, - title: str | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - mpeg4_duration: TimePeriod | None = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - thumbnail_mime_type: str | None = None, - show_caption_above_media: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.MPEG4GIF, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.mpeg4_url: str = mpeg4_url - self.thumbnail_url: str = thumbnail_url - - # Optional - self.mpeg4_width: int | None = mpeg4_width - self.mpeg4_height: int | None = mpeg4_height - self._mpeg4_duration: dtm.timedelta | None = to_timedelta(mpeg4_duration) - self.title: str | None = title - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.thumbnail_mime_type: str | None = thumbnail_mime_type - self.show_caption_above_media: bool | None = show_caption_above_media + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + thumbnail_mime_type: str | None = tg_field(default=None) + show_caption_above_media: bool | None = tg_field(default=None) @property def mpeg4_duration(self) -> int | dtm.timedelta | None: diff --git a/src/telegram/_inline/inlinequeryresultphoto.py b/src/telegram/_inline/inlinequeryresultphoto.py index 74ca99baa0d..85554929160 100644 --- a/src/telegram/_inline/inlinequeryresultphoto.py +++ b/src/telegram/_inline/inlinequeryresultphoto.py @@ -18,21 +18,22 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InlineQueryResultPhoto.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultPhoto(InlineQueryResult): """ Represents a link to a photo. By default, this photo will be sent by the user with optional @@ -110,53 +111,21 @@ class InlineQueryResultPhoto(InlineQueryResult): """ - __slots__ = ( - "caption", - "caption_entities", - "description", - "input_message_content", - "parse_mode", - "photo_height", - "photo_url", - "photo_width", - "reply_markup", - "show_caption_above_media", - "thumbnail_url", - "title", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.PHOTO) + # Required + photo_url: str = tg_field() + thumbnail_url: str = tg_field() + # Optional + photo_width: int | None = tg_field(default=None) + photo_height: int | None = tg_field(default=None) + title: str | None = tg_field(default=None) + description: str | None = tg_field(default=None) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - photo_url: str, - thumbnail_url: str, - photo_width: int | None = None, - photo_height: int | None = None, - title: str | None = None, - description: str | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - show_caption_above_media: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.PHOTO, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.photo_url: str = photo_url - self.thumbnail_url: str = thumbnail_url - - # Optionals - self.photo_width: int | None = photo_width - self.photo_height: int | None = photo_height - self.title: str | None = title - self.description: str | None = description - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.show_caption_above_media: bool | None = show_caption_above_media + show_caption_above_media: bool | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultsbutton.py b/src/telegram/_inline/inlinequeryresultsbutton.py index eaa065f8334..c2c1360797f 100644 --- a/src/telegram/_inline/inlinequeryresultsbutton.py +++ b/src/telegram/_inline/inlinequeryresultsbutton.py @@ -18,14 +18,15 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the class that represent a Telegram InlineQueryResultsButton.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._webappinfo import WebAppInfo +@tg_dataclass() class InlineQueryResultsButton(TelegramObject): """This object represents a button to be shown above inline query results. You **must** use exactly one of the optional fields. @@ -71,34 +72,17 @@ class InlineQueryResultsButton(TelegramObject): """ - __slots__ = ("start_parameter", "text", "web_app") + # Required + text: str = tg_field(compare=True) + # Optional + web_app: WebAppInfo | None = tg_field(compare=True, default=None) + start_parameter: str | None = tg_field(compare=True, default=None) - def __init__( - self, - text: str, - web_app: WebAppInfo | None = None, - start_parameter: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Required - self.text: str = text - - # Optional - self.web_app: WebAppInfo | None = web_app - self.start_parameter: str | None = start_parameter - - self._id_attrs = (self.text, self.web_app, self.start_parameter) - - self._freeze() - - MIN_START_PARAMETER_LENGTH: Final[int] = ( + MIN_START_PARAMETER_LENGTH: ClassVar[int] = ( constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH ) """:const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`""" - MAX_START_PARAMETER_LENGTH: Final[int] = ( + MAX_START_PARAMETER_LENGTH: ClassVar[int] = ( constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH ) """:const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`""" diff --git a/src/telegram/_inline/inlinequeryresultvenue.py b/src/telegram/_inline/inlinequeryresultvenue.py index 162ff4ed8c4..a7d20fb789a 100644 --- a/src/telegram/_inline/inlinequeryresultvenue.py +++ b/src/telegram/_inline/inlinequeryresultvenue.py @@ -22,13 +22,14 @@ from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultVenue(InlineQueryResult): """ Represents a venue. By default, the venue will be sent by the user. Alternatively, you can @@ -105,56 +106,20 @@ class InlineQueryResultVenue(InlineQueryResult): """ - __slots__ = ( - "address", - "foursquare_id", - "foursquare_type", - "google_place_id", - "google_place_type", - "input_message_content", - "latitude", - "longitude", - "reply_markup", - "thumbnail_height", - "thumbnail_url", - "thumbnail_width", - "title", - ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - latitude: float, - longitude: float, - title: str, - address: str, - foursquare_id: str | None = None, - foursquare_type: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - google_place_id: str | None = None, - google_place_type: str | None = None, - thumbnail_url: str | None = None, - thumbnail_width: int | None = None, - thumbnail_height: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.VENUE, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.latitude: float = latitude - self.longitude: float = longitude - self.title: str = title - self.address: str = address - - # Optional - self.foursquare_id: str | None = foursquare_id - self.foursquare_type: str | None = foursquare_type - self.google_place_id: str | None = google_place_id - self.google_place_type: str | None = google_place_type - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.thumbnail_url: str | None = thumbnail_url - self.thumbnail_width: int | None = thumbnail_width - self.thumbnail_height: int | None = thumbnail_height + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.VENUE) + # Required + latitude: float = tg_field() + longitude: float = tg_field() + title: str = tg_field() + address: str = tg_field() + # Optional + foursquare_id: str | None = tg_field(default=None) + foursquare_type: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + google_place_id: str | None = tg_field(default=None) + google_place_type: str | None = tg_field(default=None) + thumbnail_url: str | None = tg_field(default=None) + thumbnail_width: int | None = tg_field(default=None) + thumbnail_height: int | None = tg_field(default=None) diff --git a/src/telegram/_inline/inlinequeryresultvideo.py b/src/telegram/_inline/inlinequeryresultvideo.py index a9ab9bc5241..dbe9a616372 100644 --- a/src/telegram/_inline/inlinequeryresultvideo.py +++ b/src/telegram/_inline/inlinequeryresultvideo.py @@ -19,22 +19,23 @@ """This module contains the classes that represent Telegram InlineQueryResultVideo.""" import datetime as dtm -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput, TimePeriod +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultVideo(InlineQueryResult): """ Represents a link to a page containing an embedded video player or a video file. By default, @@ -135,62 +136,28 @@ class InlineQueryResultVideo(InlineQueryResult): """ - __slots__ = ( - "_video_duration", - "caption", - "caption_entities", - "description", - "input_message_content", - "mime_type", - "parse_mode", - "reply_markup", - "show_caption_above_media", - "thumbnail_url", - "title", - "video_height", - "video_url", - "video_width", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.VIDEO) + # Required + video_url: str = tg_field() + mime_type: str = tg_field() + thumbnail_url: str = tg_field() + title: str = tg_field() + # Optional + caption: str | None = tg_field(default=None) + video_width: int | None = tg_field(default=None) + video_height: int | None = tg_field(default=None) + _video_duration: dtm.timedelta | None = tg_field( + default=None, alias="video_duration", converter=to_timedelta ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - video_url: str, - mime_type: str, - thumbnail_url: str, - title: str, - caption: str | None = None, - video_width: int | None = None, - video_height: int | None = None, - video_duration: TimePeriod | None = None, - description: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - show_caption_above_media: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.VIDEO, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.video_url: str = video_url - self.mime_type: str = mime_type - self.thumbnail_url: str = thumbnail_url - self.title: str = title - - # Optional - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.video_width: int | None = video_width - self.video_height: int | None = video_height - self._video_duration: dtm.timedelta | None = to_timedelta(video_duration) - self.description: str | None = description - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content - self.show_caption_above_media: bool | None = show_caption_above_media + description: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + show_caption_above_media: bool | None = tg_field(default=None) @property def video_duration(self) -> int | dtm.timedelta | None: diff --git a/src/telegram/_inline/inlinequeryresultvoice.py b/src/telegram/_inline/inlinequeryresultvoice.py index 92119fe121d..f54965392a0 100644 --- a/src/telegram/_inline/inlinequeryresultvoice.py +++ b/src/telegram/_inline/inlinequeryresultvoice.py @@ -19,22 +19,23 @@ """This module contains the classes that represent Telegram InlineQueryResultVoice.""" import datetime as dtm -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup from telegram._inline.inlinequeryresult import InlineQueryResult from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput, TimePeriod +from telegram._utils.types import ODVInput from telegram.constants import InlineQueryResultType if TYPE_CHECKING: from telegram import InputMessageContent +@tg_dataclass() class InlineQueryResultVoice(InlineQueryResult): """ Represents a link to a voice recording in an .ogg container encoded with OPUS. By default, @@ -98,44 +99,22 @@ class InlineQueryResultVoice(InlineQueryResult): """ - __slots__ = ( - "_voice_duration", - "caption", - "caption_entities", - "input_message_content", - "parse_mode", - "reply_markup", - "title", - "voice_url", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InlineQueryResultType.VOICE) + # Required + voice_url: str = tg_field() + title: str = tg_field() + # Optional + _voice_duration: dtm.timedelta | None = tg_field( + default=None, alias="voice_duration", converter=to_timedelta + ) + caption: str | None = tg_field(default=None) + reply_markup: InlineKeyboardMarkup | None = tg_field(default=None) + input_message_content: "InputMessageContent | None" = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - voice_url: str, - title: str, - voice_duration: TimePeriod | None = None, - caption: str | None = None, - reply_markup: InlineKeyboardMarkup | None = None, - input_message_content: "InputMessageContent | None" = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__(InlineQueryResultType.VOICE, id, api_kwargs=api_kwargs) - with self._unfrozen(): - self.voice_url: str = voice_url - self.title: str = title - - # Optional - self._voice_duration: dtm.timedelta | None = to_timedelta(voice_duration) - self.caption: str | None = caption - self.parse_mode: ODVInput[str] = parse_mode - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.reply_markup: InlineKeyboardMarkup | None = reply_markup - self.input_message_content: InputMessageContent | None = input_message_content @property def voice_duration(self) -> int | dtm.timedelta | None: diff --git a/src/telegram/_inline/inputcontactmessagecontent.py b/src/telegram/_inline/inputcontactmessagecontent.py index cead1136dd0..cf4f91edded 100644 --- a/src/telegram/_inline/inputcontactmessagecontent.py +++ b/src/telegram/_inline/inputcontactmessagecontent.py @@ -19,9 +19,10 @@ """This module contains the classes that represent Telegram InputContactMessageContent.""" from telegram._inline.inputmessagecontent import InputMessageContent -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class InputContactMessageContent(InputMessageContent): """Represents the content of a contact message to be sent as the result of an inline query. @@ -44,24 +45,9 @@ class InputContactMessageContent(InputMessageContent): """ - __slots__ = ("first_name", "last_name", "phone_number", "vcard") - - def __init__( - self, - phone_number: str, - first_name: str, - last_name: str | None = None, - vcard: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - with self._unfrozen(): - # Required - self.phone_number: str = phone_number - self.first_name: str = first_name - # Optionals - self.last_name: str | None = last_name - self.vcard: str | None = vcard - - self._id_attrs = (self.phone_number,) + # Required + phone_number: str = tg_field(compare=True) + first_name: str = tg_field() + # Optional + last_name: str | None = tg_field(default=None) + vcard: str | None = tg_field(default=None) diff --git a/src/telegram/_inline/inputinvoicemessagecontent.py b/src/telegram/_inline/inputinvoicemessagecontent.py index 6eec6479d85..5dacaa7a397 100644 --- a/src/telegram/_inline/inputinvoicemessagecontent.py +++ b/src/telegram/_inline/inputinvoicemessagecontent.py @@ -18,14 +18,13 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains a class that represents a Telegram InputInvoiceMessageContent.""" -from collections.abc import Sequence - from telegram._inline.inputmessagecontent import InputMessageContent from telegram._payment.labeledprice import LabeledPrice from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class InputInvoiceMessageContent(InputMessageContent): """ Represents the content of a invoice message to be sent as the result of an inline query. @@ -170,83 +169,25 @@ class signature. """ - __slots__ = ( - "currency", - "description", - "is_flexible", - "max_tip_amount", - "need_email", - "need_name", - "need_phone_number", - "need_shipping_address", - "payload", - "photo_height", - "photo_size", - "photo_url", - "photo_width", - "prices", - "provider_data", - "provider_token", - "send_email_to_provider", - "send_phone_number_to_provider", - "suggested_tip_amounts", - "title", - ) - - def __init__( - self, - title: str, - description: str, - payload: str, - currency: str, - prices: Sequence[LabeledPrice], - provider_token: str | None = None, - max_tip_amount: int | None = None, - suggested_tip_amounts: Sequence[int] | None = None, - provider_data: str | None = None, - photo_url: str | None = None, - photo_size: int | None = None, - photo_width: int | None = None, - photo_height: int | None = None, - need_name: bool | None = None, - need_phone_number: bool | None = None, - need_email: bool | None = None, - need_shipping_address: bool | None = None, - send_phone_number_to_provider: bool | None = None, - send_email_to_provider: bool | None = None, - is_flexible: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - with self._unfrozen(): - # Required - self.title: str = title - self.description: str = description - self.payload: str = payload - self.currency: str = currency - self.prices: tuple[LabeledPrice, ...] = parse_sequence_arg(prices) - # Optionals - self.provider_token: str | None = provider_token - self.max_tip_amount: int | None = max_tip_amount - self.suggested_tip_amounts: tuple[int, ...] = parse_sequence_arg(suggested_tip_amounts) - self.provider_data: str | None = provider_data - self.photo_url: str | None = photo_url - self.photo_size: int | None = photo_size - self.photo_width: int | None = photo_width - self.photo_height: int | None = photo_height - self.need_name: bool | None = need_name - self.need_phone_number: bool | None = need_phone_number - self.need_email: bool | None = need_email - self.need_shipping_address: bool | None = need_shipping_address - self.send_phone_number_to_provider: bool | None = send_phone_number_to_provider - self.send_email_to_provider: bool | None = send_email_to_provider - self.is_flexible: bool | None = is_flexible - - self._id_attrs = ( - self.title, - self.description, - self.payload, - self.currency, - self.prices, - ) + # Required + title: str = tg_field(compare=True) + description: str = tg_field(compare=True) + payload: str = tg_field(compare=True) + currency: str = tg_field(compare=True) + prices: tuple[LabeledPrice, ...] = tg_field(compare=True, converter=parse_sequence_arg) + # Optional + provider_token: str | None = tg_field(default=None) + max_tip_amount: int | None = tg_field(default=None) + suggested_tip_amounts: tuple[int, ...] = tg_field(default=None, converter=parse_sequence_arg) + provider_data: str | None = tg_field(default=None) + photo_url: str | None = tg_field(default=None) + photo_size: int | None = tg_field(default=None) + photo_width: int | None = tg_field(default=None) + photo_height: int | None = tg_field(default=None) + need_name: bool | None = tg_field(default=None) + need_phone_number: bool | None = tg_field(default=None) + need_email: bool | None = tg_field(default=None) + need_shipping_address: bool | None = tg_field(default=None) + send_phone_number_to_provider: bool | None = tg_field(default=None) + send_email_to_provider: bool | None = tg_field(default=None) + is_flexible: bool | None = tg_field(default=None) diff --git a/src/telegram/_inline/inputlocationmessagecontent.py b/src/telegram/_inline/inputlocationmessagecontent.py index 6e2a77e8e1d..496658d36ac 100644 --- a/src/telegram/_inline/inputlocationmessagecontent.py +++ b/src/telegram/_inline/inputlocationmessagecontent.py @@ -19,15 +19,16 @@ """This module contains the classes that represent Telegram InputLocationMessageContent.""" import datetime as dtm -from typing import Final +from typing import ClassVar from telegram import constants from telegram._inline.inputmessagecontent import InputMessageContent from telegram._utils.argumentparsing import to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value -from telegram._utils.types import JSONDict, TimePeriod +@tg_dataclass() class InputLocationMessageContent(InputMessageContent): # fmt: off """ @@ -85,79 +86,54 @@ class InputLocationMessageContent(InputMessageContent): if specified. """ - - __slots__ = ( - "_live_period", - "heading", - "horizontal_accuracy", - "latitude", - "longitude", - "proximity_alert_radius", - ) # fmt: on - def __init__( - self, - latitude: float, - longitude: float, - live_period: TimePeriod | None = None, - horizontal_accuracy: float | None = None, - heading: int | None = None, - proximity_alert_radius: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - with self._unfrozen(): - # Required - self.latitude: float = latitude - self.longitude: float = longitude - - # Optionals - self._live_period: dtm.timedelta | None = to_timedelta(live_period) - self.horizontal_accuracy: float | None = horizontal_accuracy - self.heading: int | None = heading - self.proximity_alert_radius: int | None = ( - int(proximity_alert_radius) if proximity_alert_radius else None - ) - - self._id_attrs = (self.latitude, self.longitude) + # Required + latitude: float = tg_field(compare=True) + longitude: float = tg_field(compare=True) + # Optional + _live_period: dtm.timedelta | None = tg_field( + default=None, alias="live_period", converter=to_timedelta + ) + horizontal_accuracy: float | None = tg_field(default=None) + heading: int | None = tg_field(default=None) + proximity_alert_radius: int | None = tg_field(default=None) @property def live_period(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._live_period, attribute="live_period") - HORIZONTAL_ACCURACY: Final[int] = constants.LocationLimit.HORIZONTAL_ACCURACY + HORIZONTAL_ACCURACY: ClassVar[int] = constants.LocationLimit.HORIZONTAL_ACCURACY """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY` .. versionadded:: 20.0 """ - MIN_HEADING: Final[int] = constants.LocationLimit.MIN_HEADING + MIN_HEADING: ClassVar[int] = constants.LocationLimit.MIN_HEADING """:const:`telegram.constants.LocationLimit.MIN_HEADING` .. versionadded:: 20.0 """ - MAX_HEADING: Final[int] = constants.LocationLimit.MAX_HEADING + MAX_HEADING: ClassVar[int] = constants.LocationLimit.MAX_HEADING """:const:`telegram.constants.LocationLimit.MAX_HEADING` .. versionadded:: 20.0 """ - MIN_LIVE_PERIOD: Final[int] = constants.LocationLimit.MIN_LIVE_PERIOD + MIN_LIVE_PERIOD: ClassVar[int] = constants.LocationLimit.MIN_LIVE_PERIOD """:const:`telegram.constants.LocationLimit.MIN_LIVE_PERIOD` .. versionadded:: 20.0 """ - MAX_LIVE_PERIOD: Final[int] = constants.LocationLimit.MAX_LIVE_PERIOD + MAX_LIVE_PERIOD: ClassVar[int] = constants.LocationLimit.MAX_LIVE_PERIOD """:const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD` .. versionadded:: 20.0 """ - MIN_PROXIMITY_ALERT_RADIUS: Final[int] = constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS + MIN_PROXIMITY_ALERT_RADIUS: ClassVar[int] = constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS """:const:`telegram.constants.LocationLimit.MIN_PROXIMITY_ALERT_RADIUS` .. versionadded:: 20.0 """ - MAX_PROXIMITY_ALERT_RADIUS: Final[int] = constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS + MAX_PROXIMITY_ALERT_RADIUS: ClassVar[int] = constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS """:const:`telegram.constants.LocationLimit.MAX_PROXIMITY_ALERT_RADIUS` .. versionadded:: 20.0 diff --git a/src/telegram/_inline/inputmessagecontent.py b/src/telegram/_inline/inputmessagecontent.py index 6e56dc2e43c..f57dffd7147 100644 --- a/src/telegram/_inline/inputmessagecontent.py +++ b/src/telegram/_inline/inputmessagecontent.py @@ -19,9 +19,10 @@ """This module contains the classes that represent Telegram InputMessageContent.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass +@tg_dataclass() class InputMessageContent(TelegramObject): """Base class for Telegram InputMessageContent Objects. @@ -31,10 +32,3 @@ class InputMessageContent(TelegramObject): :class:`telegram.InputVenueMessageContent` for more details. """ - - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(api_kwargs=api_kwargs) - - self._freeze() diff --git a/src/telegram/_inline/inputtextmessagecontent.py b/src/telegram/_inline/inputtextmessagecontent.py index 6edde810673..0b929db6227 100644 --- a/src/telegram/_inline/inputtextmessagecontent.py +++ b/src/telegram/_inline/inputtextmessagecontent.py @@ -18,19 +18,21 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains the classes that represent Telegram InputTextMessageContent.""" -from collections.abc import Sequence +from dataclasses import InitVar from typing import TYPE_CHECKING from telegram._inline.inputmessagecontent import InputMessageContent from telegram._messageentity import MessageEntity from telegram._utils.argumentparsing import parse_lpo_and_dwpp, parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE -from telegram._utils.types import JSONDict, ODVInput +from telegram._utils.types import ODVInput if TYPE_CHECKING: from telegram._linkpreviewoptions import LinkPreviewOptions +@tg_dataclass() class InputTextMessageContent(InputMessageContent): """ Represents the content of a text message to be sent as the result of an inline query. @@ -90,28 +92,27 @@ class InputTextMessageContent(InputMessageContent): """ - __slots__ = ("entities", "link_preview_options", "message_text", "parse_mode") + # Required + message_text: str = tg_field(compare=True) + # Optional + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + entities: tuple[MessageEntity, ...] = tg_field(default=None, converter=parse_sequence_arg) + link_preview_options: ODVInput["LinkPreviewOptions"] = tg_field(default=DEFAULT_NONE) + # Keyword only + disable_web_page_preview: InitVar[bool | None] = tg_field(kw_only=True, default=None) - def __init__( + def __post_init__( # pylint: disable=arguments-differ self, - message_text: str, - parse_mode: ODVInput[str] = DEFAULT_NONE, - entities: Sequence[MessageEntity] | None = None, - link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE, - *, disable_web_page_preview: bool | None = None, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - with self._unfrozen(): - # Required - self.message_text: str = message_text - # Optionals - self.parse_mode: ODVInput[str] = parse_mode - self.entities: tuple[MessageEntity, ...] = parse_sequence_arg(entities) - self.link_preview_options: ODVInput[LinkPreviewOptions] = parse_lpo_and_dwpp( - disable_web_page_preview, link_preview_options - ) - - self._id_attrs = (self.message_text,) + /, + ) -> None: + object.__setattr__( + self, + "link_preview_options", + parse_lpo_and_dwpp( + disable_web_page_preview, + self.link_preview_options, + ), + ) + + InputMessageContent.__post_init__(self) diff --git a/src/telegram/_inline/inputvenuemessagecontent.py b/src/telegram/_inline/inputvenuemessagecontent.py index 28c039fb1a9..0800435eedf 100644 --- a/src/telegram/_inline/inputvenuemessagecontent.py +++ b/src/telegram/_inline/inputvenuemessagecontent.py @@ -19,9 +19,10 @@ """This module contains the classes that represent Telegram InputVenueMessageContent.""" from telegram._inline.inputmessagecontent import InputMessageContent -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class InputVenueMessageContent(InputMessageContent): """Represents the content of a venue message to be sent as the result of an inline query. @@ -63,45 +64,13 @@ class InputVenueMessageContent(InputMessageContent): """ - __slots__ = ( - "address", - "foursquare_id", - "foursquare_type", - "google_place_id", - "google_place_type", - "latitude", - "longitude", - "title", - ) - - def __init__( - self, - latitude: float, - longitude: float, - title: str, - address: str, - foursquare_id: str | None = None, - foursquare_type: str | None = None, - google_place_id: str | None = None, - google_place_type: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - with self._unfrozen(): - # Required - self.latitude: float = latitude - self.longitude: float = longitude - self.title: str = title - self.address: str = address - # Optionals - self.foursquare_id: str | None = foursquare_id - self.foursquare_type: str | None = foursquare_type - self.google_place_id: str | None = google_place_id - self.google_place_type: str | None = google_place_type - - self._id_attrs = ( - self.latitude, - self.longitude, - self.title, - ) + # Required + latitude: float = tg_field(compare=True) + longitude: float = tg_field(compare=True) + title: str = tg_field(compare=True) + address: str = tg_field() + # Optional + foursquare_id: str | None = tg_field(default=None) + foursquare_type: str | None = tg_field(default=None) + google_place_id: str | None = tg_field(default=None) + google_place_type: str | None = tg_field(default=None) diff --git a/src/telegram/_inline/preparedinlinemessage.py b/src/telegram/_inline/preparedinlinemessage.py index fb03082f1f7..0c76ec2af3a 100644 --- a/src/telegram/_inline/preparedinlinemessage.py +++ b/src/telegram/_inline/preparedinlinemessage.py @@ -21,9 +21,10 @@ import datetime as dtm from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class PreparedInlineMessage(TelegramObject): """Describes an inline message to be sent by a user of a Mini App. @@ -45,19 +46,6 @@ class PreparedInlineMessage(TelegramObject): |datetime_localization| """ - __slots__ = ("expiration_date", "id") - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - expiration_date: dtm.datetime, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.id: str = id - self.expiration_date: dtm.datetime = expiration_date - - self._id_attrs = (self.id,) - - self._freeze() + # Required + id: str = tg_field(compare=True) + expiration_date: dtm.datetime = tg_field() diff --git a/src/telegram/_message.py b/src/telegram/_message.py index 8ce66d880eb..def0dd29f8e 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -19,7 +19,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Message.""" -import dataclasses +import copy import datetime as dtm import re from collections.abc import Sequence @@ -176,9 +176,24 @@ class MaybeInaccessibleMessage(TelegramObject): chat (:class:`telegram.Chat`): Conversation the message belongs to. """ - chat: Chat = tg_field(compare=True) message_id: int = tg_field(compare=True) date: dtm.datetime = tg_field() + chat: Chat = tg_field(compare=True) + + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering + def __init__( + self, + chat: Chat, + message_id: int, + date: dtm.datetime, + *, + api_kwargs: JSONDict | None = None, + ) -> None: + object.__setattr__(self, "chat", chat) + object.__setattr__(self, "message_id", message_id) + object.__setattr__(self, "date", date) + TelegramObject.__init__(self, api_kwargs=api_kwargs) @property def is_accessible(self) -> bool: @@ -202,7 +217,7 @@ def de_json( if data.get("date") == 0: return InaccessibleMessage.de_json(data=data, bot=bot) return Message.de_json(data=data, bot=bot) - return super().de_json(data=data, bot=bot) + return super(MaybeInaccessibleMessage, cls).de_json(data=data, bot=bot) # noqa: UP008 @tg_dataclass() @@ -229,6 +244,23 @@ class InaccessibleMessage(MaybeInaccessibleMessage): date: dtm.datetime = tg_field(init=False, default=ZERO_DATE) + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering + def __init__( + self, + chat: Chat, + message_id: int, + *, + api_kwargs: JSONDict | None = None, + ) -> None: + MaybeInaccessibleMessage.__init__( + self, + chat, + message_id, + ZERO_DATE, + api_kwargs=api_kwargs, + ) + @tg_dataclass() class Message(MaybeInaccessibleMessage): @@ -1183,12 +1215,9 @@ class Message(MaybeInaccessibleMessage): # fmt: on - # tags: deprecated NEXT.VERSION - # Remove these fields since they're inherited from parent - # and are only here to perseve previous (pre-dataclasses) ordering - message_id: int = tg_field(compare=True) - date: dtm.datetime = tg_field() - chat: Chat = tg_field(compare=True) + @staticmethod + def _to_bool(value: bool | None) -> bool: + return bool(value) from_user: User | None = tg_field(default=None) reply_to_message: "Message | None" = tg_field(default=None) @@ -1214,10 +1243,10 @@ class Message(MaybeInaccessibleMessage): left_chat_member: User | None = tg_field(default=None) new_chat_title: str | None = tg_field(default=None) new_chat_photo: tuple[PhotoSize, ...] = tg_field(default=None, converter=parse_sequence_arg) - delete_chat_photo: bool | None = tg_field(default=None) - group_chat_created: bool | None = tg_field(default=None) - supergroup_chat_created: bool | None = tg_field(default=None) - channel_chat_created: bool | None = tg_field(default=None) + delete_chat_photo: bool | None = tg_field(default=None, converter=_to_bool) + group_chat_created: bool | None = tg_field(default=None, converter=_to_bool) + supergroup_chat_created: bool | None = tg_field(default=None, converter=_to_bool) + channel_chat_created: bool | None = tg_field(default=None, converter=_to_bool) migrate_to_chat_id: int | None = tg_field(default=None) migrate_from_chat_id: int | None = tg_field(default=None) pinned_message: MaybeInaccessibleMessage | None = tg_field(default=None) @@ -1521,13 +1550,10 @@ def compute_quote_position_and_entities( continue # create a new entity with the correct offset and length - entities.append( - dataclasses.replace( - entity, - offset=offset, - length=e_length, - ) - ) + new_entity = copy.copy(entity) + object.__setattr__(new_entity, "offset", offset) + object.__setattr__(new_entity, "length", e_length) + entities.append(new_entity) return position, tuple(entities) or None diff --git a/src/telegram/_messageentity.py b/src/telegram/_messageentity.py index 64016b61363..25964afef92 100644 --- a/src/telegram/_messageentity.py +++ b/src/telegram/_messageentity.py @@ -219,9 +219,8 @@ def adjust_message_entities_to_utf_16(text: str, entities: _SEM) -> _SEM: position_translation[entity.offset + entity.length] - translated_positions ) new_entity = copy.copy(entity) - with new_entity._unfrozen(): - new_entity.offset = translated_positions - new_entity.length = translated_length + object.__setattr__(new_entity, "offset", translated_positions) + object.__setattr__(new_entity, "length", translated_length) out.append(new_entity) return out @@ -283,8 +282,7 @@ def shift_entities(by: str | int, entities: _SEM) -> _SEM: out = [] for entity in entities: new_entity = copy.copy(entity) - with new_entity._unfrozen(): - new_entity.offset += effective_shift + object.__setattr__(new_entity, "offset", entity.offset + effective_shift) out.append(new_entity) return out diff --git a/src/telegram/_passport/credentials.py b/src/telegram/_passport/credentials.py index 6252ffba0a0..098becf4272 100644 --- a/src/telegram/_passport/credentials.py +++ b/src/telegram/_passport/credentials.py @@ -19,7 +19,6 @@ # pylint: disable=missing-module-docstring, redefined-builtin import json from base64 import b64decode -from collections.abc import Sequence from typing import no_type_check try: @@ -40,8 +39,8 @@ from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.strings import TextEncoding -from telegram._utils.types import JSONDict from telegram.error import PassportDecryptionError @@ -100,6 +99,7 @@ def decrypt_json(secret, hash, data): return json.loads(decrypt(secret, hash, data).decode(TextEncoding.UTF_8)) +@tg_dataclass() class EncryptedCredentials(TelegramObject): """Contains data required for decrypting and authenticating EncryptedPassportElement. See the Telegram Passport Documentation for a complete description of the data decryption and @@ -128,34 +128,14 @@ class EncryptedCredentials(TelegramObject): """ - __slots__ = ( - "_decrypted_data", - "_decrypted_secret", - "data", - "hash", - "secret", - ) - - def __init__( - self, - data: str, - hash: str, - secret: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.data: str = data - self.hash: str = hash - self.secret: str = secret - - self._id_attrs = (self.data, self.hash, self.secret) - - self._decrypted_secret: bytes | None = None - self._decrypted_data: Credentials | None = None + # Required + data: str = tg_field(compare=True) + hash: str = tg_field(compare=True) + secret: str = tg_field(compare=True) - self._freeze() + # Attribute only (init=False) + _decrypted_secret: bytes | None = tg_field(init=False, default=None) + _decrypted_data: "Credentials | None" = tg_field(init=False, default=None) @property def decrypted_secret(self) -> bytes: @@ -166,7 +146,8 @@ def decrypted_secret(self) -> bytes: telegram.error.PassportDecryptionError: Decryption failed. Usually due to bad private/public key but can also suggest malformed/tampered data. """ - if self._decrypted_secret is None: + secret: bytes | None = self._decrypted_secret + if secret is None: if not CRYPTO_INSTALLED: raise RuntimeError( "To use Telegram Passports, PTB must be installed via `pip install " @@ -179,14 +160,16 @@ def decrypted_secret(self) -> bytes: # is the default for OAEP, the algorithm is the default for PHP which is what # Telegram's backend servers run. try: - self._decrypted_secret = self.get_bot().private_key.decrypt( # type: ignore + secret = self.get_bot().private_key.decrypt( # type: ignore b64decode(self.secret), OAEP(mgf=MGF1(algorithm=SHA1()), algorithm=SHA1(), label=None), # skipcq ) + + object.__setattr__(self, "_decrypted_secret", secret) except ValueError as exception: # If decryption fails raise exception raise PassportDecryptionError(exception) from exception - return self._decrypted_secret + return secret @property def decrypted_data(self) -> "Credentials": @@ -199,14 +182,17 @@ def decrypted_data(self) -> "Credentials": telegram.error.PassportDecryptionError: Decryption failed. Usually due to bad private/public key but can also suggest malformed/tampered data. """ - if self._decrypted_data is None: - self._decrypted_data = Credentials.de_json( + data: Credentials | None = self._decrypted_data + if data is None: + data = Credentials.de_json( decrypt_json(self.decrypted_secret, b64decode(self.hash), b64decode(self.data)), self.get_bot(), ) - return self._decrypted_data + object.__setattr__(self, "_decrypted_data", data) + return data +@tg_dataclass() class Credentials(TelegramObject): """ Attributes: @@ -214,23 +200,11 @@ class Credentials(TelegramObject): nonce (:obj:`str`): Bot-specified nonce """ - __slots__ = ("nonce", "secure_data") - - def __init__( - self, - secure_data: "SecureData", - nonce: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.secure_data: SecureData = secure_data - self.nonce: str = nonce - - self._freeze() + secure_data: "SecureData" = tg_field() + nonce: str = tg_field() +@tg_dataclass() class SecureData(TelegramObject): """ This object represents the credentials that were used to decrypt the encrypted data. @@ -281,54 +255,21 @@ class SecureData(TelegramObject): temporary registration. """ - __slots__ = ( - "address", - "bank_statement", - "driver_license", - "identity_card", - "internal_passport", - "passport", - "passport_registration", - "personal_details", - "rental_agreement", - "temporary_registration", - "utility_bill", - ) - - def __init__( - self, - personal_details: "SecureValue | None" = None, - passport: "SecureValue | None" = None, - internal_passport: "SecureValue | None" = None, - driver_license: "SecureValue | None" = None, - identity_card: "SecureValue | None" = None, - address: "SecureValue | None" = None, - utility_bill: "SecureValue | None" = None, - bank_statement: "SecureValue | None" = None, - rental_agreement: "SecureValue | None" = None, - passport_registration: "SecureValue | None" = None, - temporary_registration: "SecureValue | None" = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Optionals - self.temporary_registration: SecureValue | None = temporary_registration - self.passport_registration: SecureValue | None = passport_registration - self.rental_agreement: SecureValue | None = rental_agreement - self.bank_statement: SecureValue | None = bank_statement - self.utility_bill: SecureValue | None = utility_bill - self.address: SecureValue | None = address - self.identity_card: SecureValue | None = identity_card - self.driver_license: SecureValue | None = driver_license - self.internal_passport: SecureValue | None = internal_passport - self.passport: SecureValue | None = passport - self.personal_details: SecureValue | None = personal_details - - self._freeze() - - + # Optionals + personal_details: "SecureValue | None" = tg_field(default=None) + passport: "SecureValue | None" = tg_field(default=None) + internal_passport: "SecureValue | None" = tg_field(default=None) + driver_license: "SecureValue | None" = tg_field(default=None) + identity_card: "SecureValue | None" = tg_field(default=None) + address: "SecureValue | None" = tg_field(default=None) + utility_bill: "SecureValue | None" = tg_field(default=None) + bank_statement: "SecureValue | None" = tg_field(default=None) + rental_agreement: "SecureValue | None" = tg_field(default=None) + passport_registration: "SecureValue | None" = tg_field(default=None) + temporary_registration: "SecureValue | None" = tg_field(default=None) + + +@tg_dataclass() class SecureValue(TelegramObject): """ This object represents the credentials that were used to decrypt the encrypted value. @@ -385,52 +326,34 @@ class SecureValue(TelegramObject): """ - __slots__ = ("data", "files", "front_side", "reverse_side", "selfie", "translation") - - def __init__( - self, - data: "DataCredentials | None" = None, - front_side: "FileCredentials | None" = None, - reverse_side: "FileCredentials | None" = None, - selfie: "FileCredentials | None" = None, - files: Sequence["FileCredentials"] | None = None, - translation: Sequence["FileCredentials"] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.data: DataCredentials | None = data - self.front_side: FileCredentials | None = front_side - self.reverse_side: FileCredentials | None = reverse_side - self.selfie: FileCredentials | None = selfie - self.files: tuple[FileCredentials, ...] = parse_sequence_arg(files) - self.translation: tuple[FileCredentials, ...] = parse_sequence_arg(translation) - - self._freeze() + data: "DataCredentials | None" = tg_field(default=None) + front_side: "FileCredentials | None" = tg_field(default=None) + reverse_side: "FileCredentials | None" = tg_field(default=None) + selfie: "FileCredentials | None" = tg_field(default=None) + files: tuple["FileCredentials", ...] = tg_field(default=None, converter=parse_sequence_arg) + translation: tuple["FileCredentials", ...] = tg_field( + default=None, converter=parse_sequence_arg + ) +@tg_dataclass() class _CredentialsBase(TelegramObject): """Base class for DataCredentials and FileCredentials.""" - __slots__ = ("data_hash", "file_hash", "hash", "secret") + hash: str = tg_field() + secret: str = tg_field() - def __init__( - self, - hash: str, - secret: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - with self._unfrozen(): - self.hash: str = hash - self.secret: str = secret + # Aliases just to be sure + @property + def file_hash(self) -> str: + return self.hash - # Aliases just to be sure - self.file_hash: str = self.hash - self.data_hash: str = self.hash + @property + def data_hash(self) -> str: + return self.hash +@tg_dataclass() class DataCredentials(_CredentialsBase): """ These credentials can be used to decrypt encrypted data from the data field in @@ -445,13 +368,10 @@ class DataCredentials(_CredentialsBase): secret (:obj:`str`): Secret of encrypted data """ - __slots__ = () - - def __init__(self, data_hash: str, secret: str, *, api_kwargs: JSONDict | None = None): - super().__init__(hash=data_hash, secret=secret, api_kwargs=api_kwargs) - self._freeze() + hash: str = tg_field(alias="data_hash") +@tg_dataclass() class FileCredentials(_CredentialsBase): """ These credentials can be used to decrypt encrypted files from the front_side, @@ -466,8 +386,4 @@ class FileCredentials(_CredentialsBase): secret (:obj:`str`): Secret of encrypted file """ - __slots__ = () - - def __init__(self, file_hash: str, secret: str, *, api_kwargs: JSONDict | None = None): - super().__init__(hash=file_hash, secret=secret, api_kwargs=api_kwargs) - self._freeze() + hash: str = tg_field(alias="file_hash") diff --git a/src/telegram/_passport/data.py b/src/telegram/_passport/data.py index 57261496a8e..ea405195d9b 100644 --- a/src/telegram/_passport/data.py +++ b/src/telegram/_passport/data.py @@ -19,9 +19,10 @@ # pylint: disable=missing-module-docstring from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class PersonalDetails(TelegramObject): """ This object represents personal details. @@ -59,50 +60,21 @@ class PersonalDetails(TelegramObject): residence. """ - __slots__ = ( - "birth_date", - "country_code", - "first_name", - "first_name_native", - "gender", - "last_name", - "last_name_native", - "middle_name", - "middle_name_native", - "residence_country_code", - ) - - def __init__( - self, - first_name: str, - last_name: str, - birth_date: str, - gender: str, - country_code: str, - residence_country_code: str, - first_name_native: str | None = None, - last_name_native: str | None = None, - middle_name: str | None = None, - middle_name_native: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.first_name: str = first_name - self.last_name: str = last_name - self.middle_name: str | None = middle_name - self.birth_date: str = birth_date - self.gender: str = gender - self.country_code: str = country_code - self.residence_country_code: str = residence_country_code - self.first_name_native: str | None = first_name_native - self.last_name_native: str | None = last_name_native - self.middle_name_native: str | None = middle_name_native - - self._freeze() - - + # Required + first_name: str = tg_field() + last_name: str = tg_field() + birth_date: str = tg_field() + gender: str = tg_field() + country_code: str = tg_field() + residence_country_code: str = tg_field() + # Optional + first_name_native: str | None = tg_field(default=None) + last_name_native: str | None = tg_field(default=None) + middle_name: str | None = tg_field(default=None) + middle_name_native: str | None = tg_field(default=None) + + +@tg_dataclass() class ResidentialAddress(TelegramObject): """ This object represents a residential address. @@ -124,38 +96,16 @@ class ResidentialAddress(TelegramObject): post_code (:obj:`str`): Address post code. """ - __slots__ = ( - "city", - "country_code", - "post_code", - "state", - "street_line1", - "street_line2", - ) - - def __init__( - self, - street_line1: str, - street_line2: str, - city: str, - state: str, - country_code: str, - post_code: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.street_line1: str = street_line1 - self.street_line2: str = street_line2 - self.city: str = city - self.state: str = state - self.country_code: str = country_code - self.post_code: str = post_code - - self._freeze() + # Required + street_line1: str = tg_field() + street_line2: str = tg_field() + city: str = tg_field() + state: str = tg_field() + country_code: str = tg_field() + post_code: str = tg_field() +@tg_dataclass() class IdDocumentData(TelegramObject): """ This object represents the data of an identity document. @@ -169,17 +119,5 @@ class IdDocumentData(TelegramObject): expiry_date (:obj:`str`): Optional. Date of expiry, in DD.MM.YYYY format. """ - __slots__ = ("document_no", "expiry_date") - - def __init__( - self, - document_no: str, - expiry_date: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.document_no: str = document_no - self.expiry_date: str = expiry_date - - self._freeze() + document_no: str = tg_field() + expiry_date: str = tg_field() diff --git a/src/telegram/_passport/encryptedpassportelement.py b/src/telegram/_passport/encryptedpassportelement.py index 144586f777a..e8d159a5520 100644 --- a/src/telegram/_passport/encryptedpassportelement.py +++ b/src/telegram/_passport/encryptedpassportelement.py @@ -26,6 +26,7 @@ from telegram._passport.passportfile import PassportFile, with_file_credentials from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.types import JSONDict if TYPE_CHECKING: @@ -53,6 +54,7 @@ def _add_file_credentials_to_list( ] +@tg_dataclass() class EncryptedPassportElement(TelegramObject): """ Contains information about documents or other Telegram Passport elements shared with the bot @@ -154,61 +156,22 @@ class EncryptedPassportElement(TelegramObject): """ - __slots__ = ( - "data", - "email", - "files", - "front_side", - "hash", - "phone_number", - "reverse_side", - "selfie", - "translation", - "type", + # Required + type: str = tg_field(compare=True) + hash: str = tg_field() + # Optional + data: PersonalDetails | IdDocumentData | ResidentialAddress | None = tg_field( + compare=True, default=None ) - - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - hash: str, # pylint: disable=redefined-builtin - data: PersonalDetails | IdDocumentData | ResidentialAddress | None = None, - phone_number: str | None = None, - email: str | None = None, - files: Sequence[PassportFile] | None = None, - front_side: PassportFile | None = None, - reverse_side: PassportFile | None = None, - selfie: PassportFile | None = None, - translation: Sequence[PassportFile] | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Required - self.type: str = type - # Optionals - self.data: PersonalDetails | IdDocumentData | ResidentialAddress | None = data - self.phone_number: str | None = phone_number - self.email: str | None = email - self.files: tuple[PassportFile, ...] = parse_sequence_arg(files) - self.front_side: PassportFile | None = front_side - self.reverse_side: PassportFile | None = reverse_side - self.selfie: PassportFile | None = selfie - self.translation: tuple[PassportFile, ...] = parse_sequence_arg(translation) - self.hash: str = hash - - self._id_attrs = ( - self.type, - self.data, - self.phone_number, - self.email, - self.files, - self.front_side, - self.reverse_side, - self.selfie, - ) - - self._freeze() + phone_number: str | None = tg_field(compare=True, default=None) + email: str | None = tg_field(compare=True, default=None) + files: tuple[PassportFile, ...] = tg_field( + compare=True, default=None, converter=parse_sequence_arg + ) + front_side: PassportFile | None = tg_field(compare=True, default=None) + reverse_side: PassportFile | None = tg_field(compare=True, default=None) + selfie: PassportFile | None = tg_field(compare=True, default=None) + translation: tuple[PassportFile, ...] = tg_field(default=None, converter=parse_sequence_arg) @classmethod def de_json_decrypted( @@ -271,4 +234,4 @@ def de_json_decrypted( data.get("translation"), secure_data.translation ) - return super().de_json(data=data, bot=bot) + return super(EncryptedPassportElement, cls).de_json(data=data, bot=bot) # noqa: UP008 diff --git a/src/telegram/_passport/passportdata.py b/src/telegram/_passport/passportdata.py index 73bf54976ba..0ed374d7f6d 100644 --- a/src/telegram/_passport/passportdata.py +++ b/src/telegram/_passport/passportdata.py @@ -18,19 +18,19 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """Contains information about Telegram Passport data shared with the bot by the user.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._passport.credentials import EncryptedCredentials from telegram._passport.encryptedpassportelement import EncryptedPassportElement from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field if TYPE_CHECKING: from telegram import Credentials +@tg_dataclass() class PassportData(TelegramObject): """Contains information about Telegram Passport data shared with the bot by the user. @@ -63,24 +63,24 @@ class PassportData(TelegramObject): """ - __slots__ = ("_decrypted_data", "credentials", "data") - - def __init__( - self, - data: Sequence[EncryptedPassportElement], - credentials: EncryptedCredentials, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.data: tuple[EncryptedPassportElement, ...] = parse_sequence_arg(data) - self.credentials: EncryptedCredentials = credentials - - self._decrypted_data: tuple[EncryptedPassportElement] | None = None - self._id_attrs = tuple([x.type for x in data] + [credentials.hash]) - - self._freeze() + data: tuple[EncryptedPassportElement, ...] = tg_field( + compare=True, converter=parse_sequence_arg + ) + credentials: EncryptedCredentials = tg_field(compare=True) + + # Attribute only (init=False) + _decrypted_data: tuple[EncryptedPassportElement, ...] | None = tg_field( + default=None, init=False + ) + _comparison_key: tuple[str, ...] = tg_field(init=False, compare=True) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "_comparison_key", + tuple([x.type for x in self.data] + [self.credentials.hash]), + ) + TelegramObject.__post_init__(self) @property def decrypted_data(self) -> tuple[EncryptedPassportElement, ...]: @@ -95,14 +95,16 @@ def decrypted_data(self) -> tuple[EncryptedPassportElement, ...]: telegram.error.PassportDecryptionError: Decryption failed. Usually due to bad private/public key but can also suggest malformed/tampered data. """ - if self._decrypted_data is None: - self._decrypted_data = tuple( # type: ignore[assignment] + data = self._decrypted_data + if data is None: + data = tuple( EncryptedPassportElement.de_json_decrypted( element.to_dict(), self.get_bot(), self.decrypted_credentials ) for element in self.data ) - return self._decrypted_data # type: ignore[return-value] + object.__setattr__(self, "_decrypted_data", data) + return data @property def decrypted_credentials(self) -> "Credentials": diff --git a/src/telegram/_passport/passportelementerrors.py b/src/telegram/_passport/passportelementerrors.py index bba589ee2cb..aa7e1f431e8 100644 --- a/src/telegram/_passport/passportelementerrors.py +++ b/src/telegram/_passport/passportelementerrors.py @@ -23,9 +23,11 @@ from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.types import JSONDict +@tg_dataclass() class PassportElementError(TelegramObject): """Baseclass for the PassportElementError* classes. @@ -47,22 +49,25 @@ class PassportElementError(TelegramObject): """ - __slots__ = ("message", "source", "type") + # Required + source: str = tg_field(compare=True) + type: str = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Include in initalizer for next relase + message: str = tg_field(init=False) + # tags: deprecated NEXT.VERSION + # Remove manual __init__ def __init__( self, source: str, type: str, message: str, *, api_kwargs: JSONDict | None = None - ): - super().__init__(api_kwargs=api_kwargs) - # Required - self.source: str = str(source) - self.type: str = str(type) - self.message: str = str(message) - - self._id_attrs = (self.source, self.type) - - self._freeze() + ) -> None: + object.__setattr__(self, "source", str(source)) + object.__setattr__(self, "type", str(type)) + object.__setattr__(self, "message", str(message)) + TelegramObject.__init__(self, api_kwargs=api_kwargs) +@tg_dataclass() class PassportElementErrorDataField(PassportElementError): """ Represents an issue in one of the data fields that was provided by the user. The error is @@ -90,8 +95,17 @@ class PassportElementErrorDataField(PassportElementError): """ - __slots__ = ("data_hash", "field_name") - + # Attribute only (init=False) + source: str = tg_field(compare=True, init=False, default="data") + # Required + field_name: str = tg_field(compare=True) + data_hash: str = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Remove for next relase + message: str = tg_field(compare=True, init=False) + + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering def __init__( self, type: str, @@ -100,22 +114,13 @@ def __init__( message: str, *, api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__("data", type, message, api_kwargs=api_kwargs) - with self._unfrozen(): - self.field_name: str = field_name - self.data_hash: str = data_hash - - self._id_attrs = ( - self.source, - self.type, - self.field_name, - self.data_hash, - self.message, - ) + ) -> None: + PassportElementError.__init__(self, "data", type, message, api_kwargs=api_kwargs) + object.__setattr__(self, "field_name", field_name) + object.__setattr__(self, "data_hash", data_hash) +@tg_dataclass() class PassportElementErrorFile(PassportElementError): """ Represents an issue with a document scan. The error is considered resolved when the file with @@ -141,19 +146,24 @@ class PassportElementErrorFile(PassportElementError): """ - __slots__ = ("file_hash",) + # Attribute only (init=False) + source: str = tg_field(compare=True, init=False, default="file") + # Required + file_hash: str = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Remove for next relase + message: str = tg_field(compare=True, init=False) + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ): - # Required - super().__init__("file", type, message, api_kwargs=api_kwargs) - with self._unfrozen(): - self.file_hash: str = file_hash - - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + ) -> None: + PassportElementError.__init__(self, "file", type, message, api_kwargs=api_kwargs) + object.__setattr__(self, "file_hash", file_hash) +@tg_dataclass() class PassportElementErrorFiles(PassportElementError): """ Represents an issue with a list of scans. The error is considered resolved when the list of @@ -185,8 +195,16 @@ class PassportElementErrorFiles(PassportElementError): """ - __slots__ = ("file_hashes",) + # Attribute only (init=False) + source: str = tg_field(compare=True, init=False, default="files") + # Required + file_hashes: tuple[str, ...] = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Remove for next relase + message: str = tg_field(compare=True, init=False) + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering def __init__( self, type: str, @@ -194,15 +212,12 @@ def __init__( message: str, *, api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__("files", type, message, api_kwargs=api_kwargs) - with self._unfrozen(): - self.file_hashes: tuple[str, ...] = parse_sequence_arg(file_hashes) - - self._id_attrs = (self.source, self.type, self.message, self.file_hashes) + ) -> None: + PassportElementError.__init__(self, "files", type, message, api_kwargs=api_kwargs) + object.__setattr__(self, "file_hashes", parse_sequence_arg(file_hashes)) +@tg_dataclass() class PassportElementErrorFrontSide(PassportElementError): """ Represents an issue with the front side of a document. The error is considered resolved when @@ -228,19 +243,24 @@ class PassportElementErrorFrontSide(PassportElementError): """ - __slots__ = ("file_hash",) + # Attribute only (init=False) + source: str = tg_field(compare=True, init=False, default="front_side") + # Required + file_hash: str = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Remove for next relase + message: str = tg_field(compare=True, init=False) + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ): - # Required - super().__init__("front_side", type, message, api_kwargs=api_kwargs) - with self._unfrozen(): - self.file_hash: str = file_hash - - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + ) -> None: + PassportElementError.__init__(self, "front_side", type, message, api_kwargs=api_kwargs) + object.__setattr__(self, "file_hash", file_hash) +@tg_dataclass() class PassportElementErrorReverseSide(PassportElementError): """ Represents an issue with the reverse side of a document. The error is considered resolved when @@ -266,19 +286,24 @@ class PassportElementErrorReverseSide(PassportElementError): """ - __slots__ = ("file_hash",) + # Attribute only (init=False) + source: str = tg_field(compare=True, init=False, default="reverse_side") + # Required + file_hash: str = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Remove for next relase + message: str = tg_field(compare=True, init=False) + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ): - # Required - super().__init__("reverse_side", type, message, api_kwargs=api_kwargs) - with self._unfrozen(): - self.file_hash: str = file_hash - - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + ) -> None: + PassportElementError.__init__(self, "reverse_side", type, message, api_kwargs=api_kwargs) + object.__setattr__(self, "file_hash", file_hash) +@tg_dataclass() class PassportElementErrorSelfie(PassportElementError): """ Represents an issue with the selfie with a document. The error is considered resolved when @@ -302,19 +327,24 @@ class PassportElementErrorSelfie(PassportElementError): """ - __slots__ = ("file_hash",) + # Attribute only (init=False) + source: str = tg_field(compare=True, init=False, default="selfie") + # Required + file_hash: str = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Remove for next relase + message: str = tg_field(compare=True, init=False) + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ): - # Required - super().__init__("selfie", type, message, api_kwargs=api_kwargs) - with self._unfrozen(): - self.file_hash: str = file_hash - - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + ) -> None: + PassportElementError.__init__(self, "selfie", type, message, api_kwargs=api_kwargs) + object.__setattr__(self, "file_hash", file_hash) +@tg_dataclass() class PassportElementErrorTranslationFile(PassportElementError): """ Represents an issue with one of the files that constitute the translation of a document. @@ -342,19 +372,26 @@ class PassportElementErrorTranslationFile(PassportElementError): """ - __slots__ = ("file_hash",) + # Attribute only (init=False) + source: str = tg_field(compare=True, init=False, default="translation_file") + # Required + file_hash: str = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Remove for next relase + message: str = tg_field(compare=True, init=False) + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering def __init__( self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ): - # Required - super().__init__("translation_file", type, message, api_kwargs=api_kwargs) - with self._unfrozen(): - self.file_hash: str = file_hash - - self._id_attrs = (self.source, self.type, self.file_hash, self.message) + ) -> None: + PassportElementError.__init__( + self, "translation_file", type, message, api_kwargs=api_kwargs + ) + object.__setattr__(self, "file_hash", file_hash) +@tg_dataclass() class PassportElementErrorTranslationFiles(PassportElementError): """ Represents an issue with the translated version of a document. The error is considered @@ -388,8 +425,16 @@ class PassportElementErrorTranslationFiles(PassportElementError): """ - __slots__ = ("file_hashes",) + # Attribute only (init=False) + source: str = tg_field(compare=True, init=False, default="translation_files") + # Required + file_hashes: tuple[str, ...] = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Remove for next relase + message: str = tg_field(compare=True, init=False) + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering def __init__( self, type: str, @@ -397,15 +442,14 @@ def __init__( message: str, *, api_kwargs: JSONDict | None = None, - ): - # Required - super().__init__("translation_files", type, message, api_kwargs=api_kwargs) - with self._unfrozen(): - self.file_hashes: tuple[str, ...] = parse_sequence_arg(file_hashes) - - self._id_attrs = (self.source, self.type, self.message, self.file_hashes) + ) -> None: + PassportElementError.__init__( + self, "translation_files", type, message, api_kwargs=api_kwargs + ) + object.__setattr__(self, "file_hashes", parse_sequence_arg(file_hashes)) +@tg_dataclass() class PassportElementErrorUnspecified(PassportElementError): """ Represents an issue in an unspecified place. The error is considered resolved when new @@ -427,14 +471,18 @@ class PassportElementErrorUnspecified(PassportElementError): """ - __slots__ = ("element_hash",) + # Attribute only (init=False) + source: str = tg_field(compare=True, init=False, default="unspecified") + # Required + element_hash: str = tg_field(compare=True) + # tags: deprecated NEXT.VERSION + # Remove for next relase + message: str = tg_field(compare=True, init=False) + # tags: deprecated NEXT.VERSION + # Remove manual __init__, present here to preserve ordering def __init__( self, type: str, element_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ): - # Required - super().__init__("unspecified", type, message, api_kwargs=api_kwargs) - with self._unfrozen(): - self.element_hash: str = element_hash - - self._id_attrs = (self.source, self.type, self.element_hash, self.message) + ) -> None: + PassportElementError.__init__(self, "unspecified", type, message, api_kwargs=api_kwargs) + object.__setattr__(self, "element_hash", element_hash) diff --git a/src/telegram/_passport/passportfile.py b/src/telegram/_passport/passportfile.py index 54c00730ad6..098824e3495 100644 --- a/src/telegram/_passport/passportfile.py +++ b/src/telegram/_passport/passportfile.py @@ -22,6 +22,7 @@ from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput @@ -43,6 +44,7 @@ def with_file_credentials(data: JSONDict, credentials: "FileCredentials | None") return {**data, "credentials": credentials_data} +@tg_dataclass() class PassportFile(TelegramObject): """ This object represents a file uploaded to Telegram Passport. Currently all Telegram Passport @@ -78,38 +80,13 @@ class PassportFile(TelegramObject): |datetime_localization| """ - __slots__ = ( - "_credentials", - "file_date", - "file_id", - "file_size", - "file_unique_id", - ) - - def __init__( - self, - file_id: str, - file_unique_id: str, - file_date: dtm.datetime, - file_size: int, - credentials: "FileCredentials | None" = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - # Required - self.file_id: str = file_id - self.file_unique_id: str = file_unique_id - self.file_size: int = file_size - self.file_date: dtm.datetime = file_date - # Optionals - - self._credentials: FileCredentials | None = credentials - - self._id_attrs = (self.file_unique_id,) - - self._freeze() + # Required + file_id: str = tg_field() + file_unique_id: str = tg_field(compare=True) + file_date: dtm.datetime = tg_field() + file_size: int = tg_field() + # Optionals + _credentials: "FileCredentials | None" = tg_field(default=None, alias="credentials") @classmethod def de_json_decrypted( @@ -136,7 +113,7 @@ def de_json_decrypted( """ data = with_file_credentials(data, credentials) - return super().de_json(data=data, bot=bot) + return super(PassportFile, cls).de_json(data=data, bot=bot) # noqa: UP008 @classmethod def de_list_decrypted( diff --git a/src/telegram/_payment/invoice.py b/src/telegram/_payment/invoice.py index be7d7df173a..d4b24f7d79f 100644 --- a/src/telegram/_payment/invoice.py +++ b/src/telegram/_payment/invoice.py @@ -18,13 +18,14 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Invoice.""" -from typing import Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class Invoice(TelegramObject): """This object contains basic information about an invoice. @@ -62,72 +63,43 @@ class Invoice(TelegramObject): """ - __slots__ = ( - "currency", - "description", - "start_parameter", - "title", - "total_amount", - ) - - def __init__( - self, - title: str, - description: str, - start_parameter: str, - currency: str, - total_amount: int, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.title: str = title - self.description: str = description - self.start_parameter: str = start_parameter - self.currency: str = currency - self.total_amount: int = total_amount - - self._id_attrs = ( - self.title, - self.description, - self.start_parameter, - self.currency, - self.total_amount, - ) - - self._freeze() - - MIN_TITLE_LENGTH: Final[int] = constants.InvoiceLimit.MIN_TITLE_LENGTH + title: str = tg_field(compare=True) + description: str = tg_field(compare=True) + start_parameter: str = tg_field(compare=True) + currency: str = tg_field(compare=True) + total_amount: int = tg_field(compare=True) + + MIN_TITLE_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_TITLE_LENGTH """:const:`telegram.constants.InvoiceLimit.MIN_TITLE_LENGTH` .. versionadded:: 20.0 """ - MAX_TITLE_LENGTH: Final[int] = constants.InvoiceLimit.MAX_TITLE_LENGTH + MAX_TITLE_LENGTH: ClassVar[int] = constants.InvoiceLimit.MAX_TITLE_LENGTH """:const:`telegram.constants.InvoiceLimit.MAX_TITLE_LENGTH` .. versionadded:: 20.0 """ - MIN_DESCRIPTION_LENGTH: Final[int] = constants.InvoiceLimit.MIN_DESCRIPTION_LENGTH + MIN_DESCRIPTION_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_DESCRIPTION_LENGTH """:const:`telegram.constants.InvoiceLimit.MIN_DESCRIPTION_LENGTH` .. versionadded:: 20.0 """ - MAX_DESCRIPTION_LENGTH: Final[int] = constants.InvoiceLimit.MAX_DESCRIPTION_LENGTH + MAX_DESCRIPTION_LENGTH: ClassVar[int] = constants.InvoiceLimit.MAX_DESCRIPTION_LENGTH """:const:`telegram.constants.InvoiceLimit.MAX_DESCRIPTION_LENGTH` .. versionadded:: 20.0 """ - MIN_PAYLOAD_LENGTH: Final[int] = constants.InvoiceLimit.MIN_PAYLOAD_LENGTH + MIN_PAYLOAD_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_PAYLOAD_LENGTH """:const:`telegram.constants.InvoiceLimit.MIN_PAYLOAD_LENGTH` .. versionadded:: 20.0 """ - MAX_PAYLOAD_LENGTH: Final[int] = constants.InvoiceLimit.MAX_PAYLOAD_LENGTH + MAX_PAYLOAD_LENGTH: ClassVar[int] = constants.InvoiceLimit.MAX_PAYLOAD_LENGTH """:const:`telegram.constants.InvoiceLimit.MAX_PAYLOAD_LENGTH` .. versionadded:: 20.0 """ - MAX_TIP_AMOUNTS: Final[int] = constants.InvoiceLimit.MAX_TIP_AMOUNTS + MAX_TIP_AMOUNTS: ClassVar[int] = constants.InvoiceLimit.MAX_TIP_AMOUNTS """:const:`telegram.constants.InvoiceLimit.MAX_TIP_AMOUNTS` .. versionadded:: 20.0 diff --git a/src/telegram/_payment/labeledprice.py b/src/telegram/_payment/labeledprice.py index a9526d4f069..7f79f559999 100644 --- a/src/telegram/_payment/labeledprice.py +++ b/src/telegram/_payment/labeledprice.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram LabeledPrice.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class LabeledPrice(TelegramObject): """This object represents a portion of the price for goods or services. @@ -51,13 +52,5 @@ class LabeledPrice(TelegramObject): """ - __slots__ = ("amount", "label") - - def __init__(self, label: str, amount: int, *, api_kwargs: JSONDict | None = None): - super().__init__(api_kwargs=api_kwargs) - self.label: str = label - self.amount: int = amount - - self._id_attrs = (self.label, self.amount) - - self._freeze() + label: str = tg_field(compare=True) + amount: int = tg_field(compare=True) diff --git a/src/telegram/_payment/orderinfo.py b/src/telegram/_payment/orderinfo.py index 87997199b6c..c0d90aee213 100644 --- a/src/telegram/_payment/orderinfo.py +++ b/src/telegram/_payment/orderinfo.py @@ -20,9 +20,10 @@ from telegram._payment.shippingaddress import ShippingAddress from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class OrderInfo(TelegramObject): """This object represents information about an order. @@ -44,23 +45,7 @@ class OrderInfo(TelegramObject): """ - __slots__ = ("email", "name", "phone_number", "shipping_address") - - def __init__( - self, - name: str | None = None, - phone_number: str | None = None, - email: str | None = None, - shipping_address: ShippingAddress | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.name: str | None = name - self.phone_number: str | None = phone_number - self.email: str | None = email - self.shipping_address: ShippingAddress | None = shipping_address - - self._id_attrs = (self.name, self.phone_number, self.email, self.shipping_address) - - self._freeze() + name: str | None = tg_field(compare=True, default=None) + phone_number: str | None = tg_field(compare=True, default=None) + email: str | None = tg_field(compare=True, default=None) + shipping_address: ShippingAddress | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_payment/precheckoutquery.py b/src/telegram/_payment/precheckoutquery.py index 096e3954d7d..e0305ec0a31 100644 --- a/src/telegram/_payment/precheckoutquery.py +++ b/src/telegram/_payment/precheckoutquery.py @@ -21,10 +21,12 @@ from telegram._payment.orderinfo import OrderInfo from telegram._telegramobject import TelegramObject from telegram._user import User +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput +@tg_dataclass() class PreCheckoutQuery(TelegramObject): """This object contains information about an incoming pre-checkout query. @@ -69,40 +71,13 @@ class PreCheckoutQuery(TelegramObject): """ - __slots__ = ( - "currency", - "from_user", - "id", - "invoice_payload", - "order_info", - "shipping_option_id", - "total_amount", - ) - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - from_user: User, - currency: str, - total_amount: int, - invoice_payload: str, - shipping_option_id: str | None = None, - order_info: OrderInfo | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.id: str = id - self.from_user: User = from_user - self.currency: str = currency - self.total_amount: int = total_amount - self.invoice_payload: str = invoice_payload - self.shipping_option_id: str | None = shipping_option_id - self.order_info: OrderInfo | None = order_info - - self._id_attrs = (self.id,) - - self._freeze() + id: str = tg_field(compare=True) + from_user: User = tg_field() + currency: str = tg_field() + total_amount: int = tg_field() + invoice_payload: str = tg_field() + shipping_option_id: str | None = tg_field(default=None) + order_info: OrderInfo | None = tg_field(default=None) async def answer( self, diff --git a/src/telegram/_payment/refundedpayment.py b/src/telegram/_payment/refundedpayment.py index f29fe0face6..564766cc74f 100644 --- a/src/telegram/_payment/refundedpayment.py +++ b/src/telegram/_payment/refundedpayment.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram RefundedPayment.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class RefundedPayment(TelegramObject): """This object contains basic information about a refunded payment. @@ -60,32 +61,10 @@ class RefundedPayment(TelegramObject): """ - __slots__ = ( - "currency", - "invoice_payload", - "provider_payment_charge_id", - "telegram_payment_charge_id", - "total_amount", - ) - - def __init__( - self, - currency: str, - total_amount: int, - invoice_payload: str, - telegram_payment_charge_id: str, - provider_payment_charge_id: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.currency: str = currency - self.total_amount: int = total_amount - self.invoice_payload: str = invoice_payload - self.telegram_payment_charge_id: str = telegram_payment_charge_id - # Optional - self.provider_payment_charge_id: str | None = provider_payment_charge_id - - self._id_attrs = (self.telegram_payment_charge_id,) - - self._freeze() + # Required + currency: str = tg_field() + total_amount: int = tg_field() + invoice_payload: str = tg_field() + telegram_payment_charge_id: str = tg_field(compare=True) + # Optional + provider_payment_charge_id: str | None = tg_field(default=None) diff --git a/src/telegram/_payment/shippingaddress.py b/src/telegram/_payment/shippingaddress.py index 22e2f0c2860..aed3cc8f2ed 100644 --- a/src/telegram/_payment/shippingaddress.py +++ b/src/telegram/_payment/shippingaddress.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram ShippingAddress.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class ShippingAddress(TelegramObject): """This object represents a Telegram ShippingAddress. @@ -47,41 +48,9 @@ class ShippingAddress(TelegramObject): """ - __slots__ = ( - "city", - "country_code", - "post_code", - "state", - "street_line1", - "street_line2", - ) - - def __init__( - self, - country_code: str, - state: str, - city: str, - street_line1: str, - street_line2: str, - post_code: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.country_code: str = country_code - self.state: str = state - self.city: str = city - self.street_line1: str = street_line1 - self.street_line2: str = street_line2 - self.post_code: str = post_code - - self._id_attrs = ( - self.country_code, - self.state, - self.city, - self.street_line1, - self.street_line2, - self.post_code, - ) - - self._freeze() + country_code: str = tg_field(compare=True) + state: str = tg_field(compare=True) + city: str = tg_field(compare=True) + street_line1: str = tg_field(compare=True) + street_line2: str = tg_field(compare=True) + post_code: str = tg_field(compare=True) diff --git a/src/telegram/_payment/shippingoption.py b/src/telegram/_payment/shippingoption.py index 0b0a987a013..bb34231a33c 100644 --- a/src/telegram/_payment/shippingoption.py +++ b/src/telegram/_payment/shippingoption.py @@ -18,17 +18,17 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram ShippingOption.""" -from collections.abc import Sequence from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field if TYPE_CHECKING: from telegram import LabeledPrice +@tg_dataclass() class ShippingOption(TelegramObject): """This object represents one shipping option. @@ -56,22 +56,6 @@ class ShippingOption(TelegramObject): """ - __slots__ = ("id", "prices", "title") - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - title: str, - prices: Sequence["LabeledPrice"], - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - - self.id: str = id - self.title: str = title - self.prices: tuple[LabeledPrice, ...] = parse_sequence_arg(prices) - - self._id_attrs = (self.id,) - - self._freeze() + id: str = tg_field(compare=True) + title: str = tg_field() + prices: tuple["LabeledPrice", ...] = tg_field(converter=parse_sequence_arg) diff --git a/src/telegram/_payment/shippingquery.py b/src/telegram/_payment/shippingquery.py index fcbd45fad13..f90351f560c 100644 --- a/src/telegram/_payment/shippingquery.py +++ b/src/telegram/_payment/shippingquery.py @@ -24,6 +24,7 @@ from telegram._payment.shippingaddress import ShippingAddress from telegram._telegramobject import TelegramObject from telegram._user import User +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.types import JSONDict, ODVInput @@ -31,6 +32,7 @@ from telegram._payment.shippingoption import ShippingOption +@tg_dataclass() class ShippingQuery(TelegramObject): """This object contains information about an incoming shipping query. @@ -55,26 +57,10 @@ class ShippingQuery(TelegramObject): """ - __slots__ = ("from_user", "id", "invoice_payload", "shipping_address") - - def __init__( - self, - id: str, # pylint: disable=redefined-builtin - from_user: User, - invoice_payload: str, - shipping_address: ShippingAddress, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.id: str = id - self.from_user: User = from_user - self.invoice_payload: str = invoice_payload - self.shipping_address: ShippingAddress = shipping_address - - self._id_attrs = (self.id,) - - self._freeze() + id: str = tg_field(compare=True) + from_user: User = tg_field() + invoice_payload: str = tg_field() + shipping_address: ShippingAddress = tg_field() async def answer( self, diff --git a/src/telegram/_payment/stars/affiliateinfo.py b/src/telegram/_payment/stars/affiliateinfo.py index 862e3584705..a21d2a60dcd 100644 --- a/src/telegram/_payment/stars/affiliateinfo.py +++ b/src/telegram/_payment/stars/affiliateinfo.py @@ -21,13 +21,14 @@ from typing import TYPE_CHECKING from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field if TYPE_CHECKING: from telegram._chat import Chat from telegram._user import User +@tg_dataclass() class AffiliateInfo(TelegramObject): """Contains information about the affiliate that received a commission via this transaction. @@ -70,36 +71,8 @@ class AffiliateInfo(TelegramObject): can be negative for refunds """ - __slots__ = ( - "affiliate_chat", - "affiliate_user", - "amount", - "commission_per_mille", - "nanostar_amount", - ) - - def __init__( - self, - commission_per_mille: int, - amount: int, - affiliate_user: "User | None" = None, - affiliate_chat: "Chat | None" = None, - nanostar_amount: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.affiliate_user: User | None = affiliate_user - self.affiliate_chat: Chat | None = affiliate_chat - self.commission_per_mille: int = commission_per_mille - self.amount: int = amount - self.nanostar_amount: int | None = nanostar_amount - - self._id_attrs = ( - self.affiliate_user, - self.affiliate_chat, - self.commission_per_mille, - self.amount, - self.nanostar_amount, - ) - self._freeze() + commission_per_mille: int = tg_field(compare=True) + amount: int = tg_field(compare=True) + affiliate_user: "User | None" = tg_field(compare=True, default=None) + affiliate_chat: "Chat | None" = tg_field(compare=True, default=None) + nanostar_amount: int | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_payment/stars/revenuewithdrawalstate.py b/src/telegram/_payment/stars/revenuewithdrawalstate.py index a53e49f530f..15000f1ccb1 100644 --- a/src/telegram/_payment/stars/revenuewithdrawalstate.py +++ b/src/telegram/_payment/stars/revenuewithdrawalstate.py @@ -16,18 +16,18 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=redefined-builtin """This module contains the classes for Telegram Stars Revenue Withdrawals.""" import datetime as dtm -from typing import ClassVar, Final +from typing import ClassVar from telegram import constants from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class RevenueWithdrawalState(TelegramObject): """This object describes the state of a revenue withdrawal operation. Currently, it can be one of: @@ -48,8 +48,6 @@ class RevenueWithdrawalState(TelegramObject): type (:obj:`str`): The type of the state. """ - __slots__ = ("type",) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "type", { @@ -59,21 +57,21 @@ class RevenueWithdrawalState(TelegramObject): }, ) - PENDING: Final[str] = constants.RevenueWithdrawalStateType.PENDING + PENDING: ClassVar[str] = constants.RevenueWithdrawalStateType.PENDING """:const:`telegram.constants.RevenueWithdrawalStateType.PENDING`""" - SUCCEEDED: Final[str] = constants.RevenueWithdrawalStateType.SUCCEEDED + SUCCEEDED: ClassVar[str] = constants.RevenueWithdrawalStateType.SUCCEEDED """:const:`telegram.constants.RevenueWithdrawalStateType.SUCCEEDED`""" - FAILED: Final[str] = constants.RevenueWithdrawalStateType.FAILED + FAILED: ClassVar[str] = constants.RevenueWithdrawalStateType.FAILED """:const:`telegram.constants.RevenueWithdrawalStateType.FAILED`""" - def __init__(self, type: str, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.RevenueWithdrawalStateType, type, type) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.RevenueWithdrawalStateType, value, value) - self._id_attrs = (self.type,) - self._freeze() + type: str = tg_field(compare=True, converter=_type_converter) +@tg_dataclass() class RevenueWithdrawalStatePending(RevenueWithdrawalState): """The withdrawal is in progress. @@ -84,13 +82,11 @@ class RevenueWithdrawalStatePending(RevenueWithdrawalState): :tg-const:`telegram.RevenueWithdrawalState.PENDING`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(type=RevenueWithdrawalState.PENDING, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=RevenueWithdrawalState.PENDING) +@tg_dataclass() class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState): """The withdrawal succeeded. @@ -110,26 +106,14 @@ class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState): url (:obj:`str`): An HTTPS URL that can be used to see transaction details. """ - __slots__ = ("date", "url") + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=RevenueWithdrawalState.SUCCEEDED) - def __init__( - self, - date: dtm.datetime, - url: str, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=RevenueWithdrawalState.SUCCEEDED, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.date: dtm.datetime = date - self.url: str = url - self._id_attrs = ( - self.type, - self.date, - ) + date: dtm.datetime = tg_field(compare=True) + url: str = tg_field() +@tg_dataclass() class RevenueWithdrawalStateFailed(RevenueWithdrawalState): """The withdrawal failed and the transaction was refunded. @@ -140,8 +124,5 @@ class RevenueWithdrawalStateFailed(RevenueWithdrawalState): :tg-const:`telegram.RevenueWithdrawalState.FAILED`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(type=RevenueWithdrawalState.FAILED, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=RevenueWithdrawalState.FAILED) diff --git a/src/telegram/_payment/stars/staramount.py b/src/telegram/_payment/stars/staramount.py index 38765d8c8a9..51f5e953b95 100644 --- a/src/telegram/_payment/stars/staramount.py +++ b/src/telegram/_payment/stars/staramount.py @@ -19,9 +19,10 @@ """This module contains an object that represents a Telegram StarAmount.""" from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class StarAmount(TelegramObject): """Describes an amount of Telegram Stars. @@ -46,19 +47,5 @@ class StarAmount(TelegramObject): """ - __slots__ = ("amount", "nanostar_amount") - - def __init__( - self, - amount: int, - nanostar_amount: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.amount: int = amount - self.nanostar_amount: int | None = nanostar_amount - - self._id_attrs = (self.amount, self.nanostar_amount) - - self._freeze() + amount: int = tg_field(compare=True) + nanostar_amount: int | None = tg_field(compare=True, default=None) diff --git a/src/telegram/_payment/stars/startransactions.py b/src/telegram/_payment/stars/startransactions.py index d47c48291de..0780b55c28b 100644 --- a/src/telegram/_payment/stars/startransactions.py +++ b/src/telegram/_payment/stars/startransactions.py @@ -16,19 +16,18 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=redefined-builtin """This module contains the classes for Telegram Stars transactions.""" import datetime as dtm -from collections.abc import Sequence from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field from .transactionpartner import TransactionPartner +@tg_dataclass() class StarTransaction(TelegramObject): """Describes a Telegram Star transaction. Note that if the buyer initiates a chargeback with the payment provider from whom they @@ -81,35 +80,17 @@ class StarTransaction(TelegramObject): outgoing transactions. """ - __slots__ = ("amount", "date", "id", "nanostar_amount", "receiver", "source") - - def __init__( - self, - id: str, - amount: int, - date: dtm.datetime, - source: TransactionPartner | None = None, - receiver: TransactionPartner | None = None, - nanostar_amount: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.id: str = id - self.amount: int = amount - self.date: dtm.datetime = date - self.source: TransactionPartner | None = source - self.receiver: TransactionPartner | None = receiver - self.nanostar_amount: int | None = nanostar_amount - - self._id_attrs = ( - self.id, - self.source, - self.receiver, - ) - self._freeze() + # Required + id: str = tg_field(compare=True) + amount: int = tg_field() + date: dtm.datetime = tg_field() + # Optional + source: TransactionPartner | None = tg_field(compare=True, default=None) + receiver: TransactionPartner | None = tg_field(compare=True, default=None) + nanostar_amount: int | None = tg_field(default=None) +@tg_dataclass() class StarTransactions(TelegramObject): """ Contains a list of Telegram Star transactions. @@ -126,13 +107,6 @@ class StarTransactions(TelegramObject): transactions (tuple[:class:`telegram.StarTransaction`]): The list of transactions. """ - __slots__ = ("transactions",) - - def __init__( - self, transactions: Sequence[StarTransaction], *, api_kwargs: JSONDict | None = None - ): - super().__init__(api_kwargs=api_kwargs) - self.transactions: tuple[StarTransaction, ...] = parse_sequence_arg(transactions) - - self._id_attrs = (self.transactions,) - self._freeze() + transactions: tuple[StarTransaction, ...] = tg_field( + compare=True, converter=parse_sequence_arg + ) diff --git a/src/telegram/_payment/stars/transactionpartner.py b/src/telegram/_payment/stars/transactionpartner.py index d6069204f5e..f3e7b2b9ff4 100644 --- a/src/telegram/_payment/stars/transactionpartner.py +++ b/src/telegram/_payment/stars/transactionpartner.py @@ -16,11 +16,9 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=redefined-builtin """This module contains the classes for Telegram Stars transaction partners.""" -from collections.abc import Sequence -from typing import TYPE_CHECKING, ClassVar, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._chat import Chat @@ -32,7 +30,7 @@ parse_sequence_arg, to_timedelta, ) -from telegram._utils.types import JSONDict, TimePeriod +from telegram._utils.dataclass import tg_dataclass, tg_field from .affiliateinfo import AffiliateInfo @@ -44,6 +42,7 @@ from .revenuewithdrawalstate import RevenueWithdrawalState +@tg_dataclass() class TransactionPartner(TelegramObject): """This object describes the source of a transaction, or its recipient for outgoing transactions. Currently, it can be one of: @@ -71,8 +70,6 @@ class TransactionPartner(TelegramObject): type (:obj:`str`): The type of the transaction partner. """ - __slots__ = ("type",) - __DE_JSON_DISPATCH__: ClassVar[tuple[str, dict[str, str]] | None] = ( "type", { @@ -86,35 +83,35 @@ class TransactionPartner(TelegramObject): }, ) - AFFILIATE_PROGRAM: Final[str] = constants.TransactionPartnerType.AFFILIATE_PROGRAM + AFFILIATE_PROGRAM: ClassVar[str] = constants.TransactionPartnerType.AFFILIATE_PROGRAM """:const:`telegram.constants.TransactionPartnerType.AFFILIATE_PROGRAM` .. versionadded:: 21.9 """ - CHAT: Final[str] = constants.TransactionPartnerType.CHAT + CHAT: ClassVar[str] = constants.TransactionPartnerType.CHAT """:const:`telegram.constants.TransactionPartnerType.CHAT` .. versionadded:: 21.11 """ - FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT + FRAGMENT: ClassVar[str] = constants.TransactionPartnerType.FRAGMENT """:const:`telegram.constants.TransactionPartnerType.FRAGMENT`""" - OTHER: Final[str] = constants.TransactionPartnerType.OTHER + OTHER: ClassVar[str] = constants.TransactionPartnerType.OTHER """:const:`telegram.constants.TransactionPartnerType.OTHER`""" - TELEGRAM_ADS: Final[str] = constants.TransactionPartnerType.TELEGRAM_ADS + TELEGRAM_ADS: ClassVar[str] = constants.TransactionPartnerType.TELEGRAM_ADS """:const:`telegram.constants.TransactionPartnerType.TELEGRAM_ADS`""" - TELEGRAM_API: Final[str] = constants.TransactionPartnerType.TELEGRAM_API + TELEGRAM_API: ClassVar[str] = constants.TransactionPartnerType.TELEGRAM_API """:const:`telegram.constants.TransactionPartnerType.TELEGRAM_API`""" - USER: Final[str] = constants.TransactionPartnerType.USER + USER: ClassVar[str] = constants.TransactionPartnerType.USER """:const:`telegram.constants.TransactionPartnerType.USER`""" - def __init__(self, type: str, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.TransactionPartnerType, type, type) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.TransactionPartnerType, value, value) - self._id_attrs = (self.type,) - self._freeze() + type: str = tg_field(compare=True, converter=_type_converter) +@tg_dataclass() class TransactionPartnerAffiliateProgram(TransactionPartner): """Describes the affiliate program that issued the affiliate commission received via this transaction. @@ -139,26 +136,15 @@ class TransactionPartnerAffiliateProgram(TransactionPartner): each 1000 Telegram Stars received by the affiliate program sponsor from referred users. """ - __slots__ = ("commission_per_mille", "sponsor_user") - - def __init__( - self, - commission_per_mille: int, - sponsor_user: "User | None" = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=TransactionPartner.AFFILIATE_PROGRAM, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.sponsor_user: User | None = sponsor_user - self.commission_per_mille: int = commission_per_mille - self._id_attrs = ( - self.type, - self.commission_per_mille, - ) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=TransactionPartner.AFFILIATE_PROGRAM) + # Required + commission_per_mille: int = tg_field(compare=True) + # Optional + sponsor_user: "User | None" = tg_field(default=None) +@tg_dataclass() class TransactionPartnerChat(TransactionPartner): """Describes a transaction with a chat. @@ -179,30 +165,15 @@ class TransactionPartnerChat(TransactionPartner): """ - __slots__ = ( - "chat", - "gift", - ) - - def __init__( - self, - chat: Chat, - gift: Gift | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=TransactionPartner.CHAT, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.chat: Chat = chat - self.gift: Gift | None = gift - - self._id_attrs = ( - self.type, - self.chat, - ) + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=TransactionPartner.CHAT) + # Required + chat: Chat = tg_field(compare=True) + # Optional + gift: Gift | None = tg_field(default=None) +@tg_dataclass() class TransactionPartnerFragment(TransactionPartner): """Describes a withdrawal transaction with Fragment. @@ -219,20 +190,13 @@ class TransactionPartnerFragment(TransactionPartner): transaction if the transaction is outgoing. """ - __slots__ = ("withdrawal_state",) - - def __init__( - self, - withdrawal_state: "RevenueWithdrawalState | None" = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=TransactionPartner.FRAGMENT, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.withdrawal_state: RevenueWithdrawalState | None = withdrawal_state + # Attribute only (init=False) + type: str = tg_field(init=False, default=TransactionPartner.FRAGMENT) + # Optional + withdrawal_state: "RevenueWithdrawalState | None" = tg_field(default=None) +@tg_dataclass() class TransactionPartnerUser(TransactionPartner): """Describes a transaction with a user. @@ -352,52 +316,22 @@ class TransactionPartnerUser(TransactionPartner): """ - __slots__ = ( - "affiliate", - "gift", - "invoice_payload", - "paid_media", - "paid_media_payload", - "premium_subscription_duration", - "subscription_period", - "transaction_type", - "user", - ) - - def __init__( - self, - transaction_type: str, - user: "User", - invoice_payload: str | None = None, - paid_media: Sequence[PaidMedia] | None = None, - paid_media_payload: str | None = None, - subscription_period: TimePeriod | None = None, - gift: Gift | None = None, - affiliate: AffiliateInfo | None = None, - premium_subscription_duration: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(type=TransactionPartner.USER, api_kwargs=api_kwargs) - - with self._unfrozen(): - self.user: User = user - self.affiliate: AffiliateInfo | None = affiliate - self.invoice_payload: str | None = invoice_payload - self.paid_media: tuple[PaidMedia, ...] | None = parse_sequence_arg(paid_media) - self.paid_media_payload: str | None = paid_media_payload - self.subscription_period: dtm.timedelta | None = to_timedelta(subscription_period) - self.gift: Gift | None = gift - self.premium_subscription_duration: int | None = premium_subscription_duration - self.transaction_type: str = transaction_type - - self._id_attrs = ( - self.type, - self.user, - self.transaction_type, - ) - - + # Attribute only (init=False) + type: str = tg_field(compare=True, init=False, default=TransactionPartner.USER) + # Required + transaction_type: str = tg_field(compare=True) + user: "User" = tg_field(compare=True) + # Optional + invoice_payload: str | None = tg_field(default=None) + paid_media: tuple[PaidMedia, ...] | None = tg_field(default=None, converter=parse_sequence_arg) + paid_media_payload: str | None = tg_field(default=None) + subscription_period: "dtm.timedelta | None" = tg_field(default=None, converter=to_timedelta) + gift: Gift | None = tg_field(default=None) + affiliate: AffiliateInfo | None = tg_field(default=None) + premium_subscription_duration: int | None = tg_field(default=None) + + +@tg_dataclass() class TransactionPartnerOther(TransactionPartner): """Describes a transaction with an unknown partner. @@ -408,13 +342,11 @@ class TransactionPartnerOther(TransactionPartner): always :tg-const:`telegram.TransactionPartner.OTHER`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(type=TransactionPartner.OTHER, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=TransactionPartner.OTHER) +@tg_dataclass() class TransactionPartnerTelegramAds(TransactionPartner): """Describes a withdrawal transaction to the Telegram Ads platform. @@ -425,13 +357,11 @@ class TransactionPartnerTelegramAds(TransactionPartner): always :tg-const:`telegram.TransactionPartner.TELEGRAM_ADS`. """ - __slots__ = () - - def __init__(self, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(type=TransactionPartner.TELEGRAM_ADS, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=TransactionPartner.TELEGRAM_ADS) +@tg_dataclass() class TransactionPartnerTelegramApi(TransactionPartner): """Describes a transaction with payment for `paid broadcasting `_. @@ -452,10 +382,7 @@ class TransactionPartnerTelegramApi(TransactionPartner): and were therefore billed. """ - __slots__ = ("request_count",) - - def __init__(self, request_count: int, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(type=TransactionPartner.TELEGRAM_API, api_kwargs=api_kwargs) - with self._unfrozen(): - self.request_count: int = request_count - self._id_attrs = (self.request_count,) + # Attribute only (init=False) + type: str = tg_field(init=False, default=TransactionPartner.TELEGRAM_API) + # Required + request_count: int = tg_field(compare=True) diff --git a/src/telegram/_payment/successfulpayment.py b/src/telegram/_payment/successfulpayment.py index 8bf354330e4..ffab970caca 100644 --- a/src/telegram/_payment/successfulpayment.py +++ b/src/telegram/_payment/successfulpayment.py @@ -22,9 +22,10 @@ from telegram._payment.orderinfo import OrderInfo from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.dataclass import tg_dataclass, tg_field +@tg_dataclass() class SuccessfulPayment(TelegramObject): """This object contains basic information about a successful payment. Note that if the buyer initiates a chargeback with the relevant payment provider following @@ -91,46 +92,15 @@ class SuccessfulPayment(TelegramObject): """ - __slots__ = ( - "currency", - "invoice_payload", - "is_first_recurring", - "is_recurring", - "order_info", - "provider_payment_charge_id", - "shipping_option_id", - "subscription_expiration_date", - "telegram_payment_charge_id", - "total_amount", - ) - - def __init__( - self, - currency: str, - total_amount: int, - invoice_payload: str, - telegram_payment_charge_id: str, - provider_payment_charge_id: str, - shipping_option_id: str | None = None, - order_info: OrderInfo | None = None, - subscription_expiration_date: dtm.datetime | None = None, - is_recurring: bool | None = None, - is_first_recurring: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.currency: str = currency - self.total_amount: int = total_amount - self.invoice_payload: str = invoice_payload - self.shipping_option_id: str | None = shipping_option_id - self.order_info: OrderInfo | None = order_info - self.telegram_payment_charge_id: str = telegram_payment_charge_id - self.provider_payment_charge_id: str = provider_payment_charge_id - self.subscription_expiration_date: dtm.datetime | None = subscription_expiration_date - self.is_recurring: bool | None = is_recurring - self.is_first_recurring: bool | None = is_first_recurring - - self._id_attrs = (self.telegram_payment_charge_id, self.provider_payment_charge_id) - - self._freeze() + # Required + currency: str = tg_field() + total_amount: int = tg_field() + invoice_payload: str = tg_field() + telegram_payment_charge_id: str = tg_field(compare=True) + provider_payment_charge_id: str = tg_field(compare=True) + # Optional + shipping_option_id: str | None = tg_field(default=None) + order_info: OrderInfo | None = tg_field(default=None) + subscription_expiration_date: dtm.datetime | None = tg_field(default=None) + is_recurring: bool | None = tg_field(default=None) + is_first_recurring: bool | None = tg_field(default=None) diff --git a/src/telegram/ext/_callbackdatacache.py b/src/telegram/ext/_callbackdatacache.py index 4fd73b4ad44..9a19b1b5503 100644 --- a/src/telegram/ext/_callbackdatacache.py +++ b/src/telegram/ext/_callbackdatacache.py @@ -390,8 +390,7 @@ def process_callback_query(self, callback_query: CallbackQuery) -> None: # Get the cached callback data for the CallbackQuery keyboard_uuid, button_data = self.__get_keyboard_uuid_and_button_data(data) - with callback_query._unfrozen(): - callback_query.data = button_data # type: ignore[assignment] + object.__setattr__(callback_query, "data", button_data) # Map the callback queries ID to the keyboards UUID for later use if not mapped and not isinstance(button_data, InvalidCallbackData): From 82183175ab28efaa45c498612d8bf796b643e301 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:45:47 +0300 Subject: [PATCH 10/31] Migrate `Bot` and `ExtBot` --- src/telegram/_bot.py | 117 ++++----- src/telegram/ext/_extbot.py | 475 +++++++++++++++++++----------------- 2 files changed, 307 insertions(+), 285 deletions(-) diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index fe325d4b87a..37c3d221a0d 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -17,6 +17,8 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# ruff: noqa: UP008 +# pylint: disable=super-with-arguments """This module contains an object that represents a Telegram Bot.""" import asyncio @@ -36,6 +38,7 @@ ) from telegram._userprofileaudios import UserProfileAudios +from telegram._utils.dataclass import tg_dataclass, tg_field try: from cryptography.hazmat.backends import default_backend @@ -177,6 +180,7 @@ def _parse_base_url(value: BaseUrl, token: str) -> str: return value + token +@tg_dataclass(eq=False) class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]): """This object represents a Telegram Bot. @@ -308,17 +312,15 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]): # without having to change all places where this is used _LOGGER = get_logger(__name__) - __slots__ = ( - "_base_file_url", - "_base_url", - "_bot_initialized", - "_bot_user", - "_local_mode", - "_private_key", - "_request", - "_requests_initialized", - "_token", - ) + _token: str = tg_field(init=False) + _base_url: str = tg_field(init=False) + _base_file_url: str = tg_field(init=False) + _request: tuple[BaseRequest, BaseRequest] = tg_field(init=False) + _bot_user: User | None = tg_field(init=False) + _requests_initialized: bool = tg_field(init=False) + _bot_initialized: bool = tg_field(init=False) + _private_key: Any | None = tg_field(init=False) + _local_mode: bool = tg_field(init=False) def __init__( self, @@ -331,23 +333,23 @@ def __init__( private_key_password: bytes | None = None, local_mode: bool = False, ): - super().__init__(api_kwargs=None) + super(Bot, self).__init__(api_kwargs=None) if not token: raise InvalidToken("You must pass the token you received from https://t.me/Botfather!") - self._token: str = token + object.__setattr__(self, "_token", token) - self._base_url: str = _parse_base_url(base_url, self._token) - self._base_file_url: str = _parse_base_url(base_file_url, self._token) + object.__setattr__(self, "_base_url", _parse_base_url(base_url, self._token)) + object.__setattr__(self, "_base_file_url", _parse_base_url(base_file_url, self._token)) self._LOGGER.debug("Set Bot API URL: %s", self._base_url) self._LOGGER.debug("Set Bot API File URL: %s", self._base_file_url) - self._local_mode: bool = local_mode - self._bot_user: User | None = None - self._private_key: bytes | None = None - self._requests_initialized: bool = False - self._bot_initialized: bool = False + object.__setattr__(self, "_local_mode", local_mode) + object.__setattr__(self, "_bot_user", None) + object.__setattr__(self, "_private_key", None) + object.__setattr__(self, "_requests_initialized", False) + object.__setattr__(self, "_bot_initialized", False) - self._request: tuple[BaseRequest, BaseRequest] = ( + _request: tuple[BaseRequest, BaseRequest] = ( ( HTTPXRequest(connection_pool_size=1) if get_updates_request is None @@ -355,6 +357,7 @@ def __init__( ), HTTPXRequest() if request is None else request, ) + object.__setattr__(self, "_request", _request) # this section is about issuing a warning when using HTTP/2 and connect to a self-hosted # bot api instance, which currently only supports HTTP/1.1. Checking if a custom base url @@ -394,11 +397,10 @@ def __init__( "To use Telegram Passports, PTB must be installed via `pip install " '"python-telegram-bot[passport]"`.' ) - self._private_key = serialization.load_pem_private_key( + _private_key = serialization.load_pem_private_key( private_key, password=private_key_password, backend=default_backend() ) - - self._freeze() + object.__setattr__(self, "_private_key", _private_key) async def __aenter__(self: BT) -> BT: """ @@ -461,12 +463,12 @@ def __eq__(self, other: object) -> bool: """ if isinstance(other, Bot): return self.bot == other.bot - return super().__eq__(other) + return super(Bot, self).__eq__(other) def __hash__(self) -> int: """See :meth:`telegram.TelegramObject.__hash__`""" if self._bot_user is None: - return super().__hash__() + return super(Bot, self).__hash__() return hash((self.bot, Bot)) def __repr__(self) -> str: @@ -664,8 +666,7 @@ def _insert_defaults(self, data: dict[str, object]) -> None: if isinstance(val, InputMedia): # Copy object as not to edit it in-place new = copy.copy(val) - with new._unfrozen(): - new.parse_mode = DefaultValue.get_value(new.parse_mode) + object.__setattr__(new, "parse_mode", DefaultValue.get_value(new.parse_mode)) data[key] = new elif ( key == "media" @@ -675,8 +676,9 @@ def _insert_defaults(self, data: dict[str, object]) -> None: # Copy objects as not to edit them in-place copy_list = [copy.copy(media) for media in val] for media in copy_list: - with media._unfrozen(): - media.parse_mode = DefaultValue.get_value(media.parse_mode) + object.__setattr__( + media, "parse_mode", DefaultValue.get_value(media.parse_mode) + ) data[key] = copy_list # 2) else: @@ -855,14 +857,14 @@ async def initialize(self) -> None: # Initialize request objects if not already done if not self._requests_initialized: await asyncio.gather(self._request[0].initialize(), self._request[1].initialize()) - self._requests_initialized = True + object.__setattr__(self, "_requests_initialized", True) # Initialize bot user # Since the bot is to be initialized only once, we can also use it for # verifying the token passed and raising an exception if it's invalid. try: await self.get_me() - self._bot_initialized = True + object.__setattr__(self, "_bot_initialized", True) except InvalidToken as exc: raise InvalidToken(f"The token `{self._token}` was rejected by the server.") from exc @@ -879,8 +881,8 @@ async def shutdown(self) -> None: return await asyncio.gather(self._request[0].shutdown(), self._request[1].shutdown()) - self._requests_initialized = False - self._bot_initialized = False + object.__setattr__(self, "_requests_initialized", False) + object.__setattr__(self, "_bot_initialized", False) async def do_api_request( self, @@ -1009,8 +1011,9 @@ async def get_me( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) - self._bot_user = User.de_json(result, self) - return self._bot_user + _bot_user = User.de_json(result, self) + object.__setattr__(self, "_bot_user", _bot_user) + return _bot_user async def send_message( self, @@ -2936,11 +2939,12 @@ async def send_media_group( # Copy first item (to avoid mutation of original object), apply group caption to it. # This will lead to the group being shown with this caption. item_to_get_caption = copy.copy(media[0]) - with item_to_get_caption._unfrozen(): - item_to_get_caption.caption = caption - if parse_mode is not DEFAULT_NONE: - item_to_get_caption.parse_mode = parse_mode - item_to_get_caption.caption_entities = parse_sequence_arg(caption_entities) + object.__setattr__(item_to_get_caption, "caption", caption) + if parse_mode is not DEFAULT_NONE: + object.__setattr__(item_to_get_caption, "parse_mode", parse_mode) + object.__setattr__( + item_to_get_caption, "caption_entities", parse_sequence_arg(caption_entities) + ) # copy the list (just the references) to avoid mutating the original list media = list(media) @@ -3842,30 +3846,33 @@ def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQ if hasattr(res, "parse_mode"): res = copy.copy(res) copied = True - with res._unfrozen(): - res.parse_mode = DefaultValue.get_value(res.parse_mode) + object.__setattr__(res, "parse_mode", DefaultValue.get_value(res.parse_mode)) if hasattr(res, "input_message_content") and res.input_message_content: if hasattr(res.input_message_content, "parse_mode"): if not copied: res = copy.copy(res) copied = True - with res._unfrozen(): - res.input_message_content = copy.copy(res.input_message_content) - with res.input_message_content._unfrozen(): - res.input_message_content.parse_mode = DefaultValue.get_value( - res.input_message_content.parse_mode - ) + object.__setattr__( + res, "input_message_content", copy.copy(res.input_message_content) + ) + object.__setattr__( + res.input_message_content, + "parse_mode", + DefaultValue.get_value(res.input_message_content.parse_mode), + ) if hasattr(res.input_message_content, "link_preview_options"): if not copied: res = copy.copy(res) - with res._unfrozen(): - res.input_message_content = copy.copy(res.input_message_content) - with res.input_message_content._unfrozen(): - res.input_message_content.link_preview_options = DefaultValue.get_value( - res.input_message_content.link_preview_options - ) + object.__setattr__( + res, "input_message_content", copy.copy(res.input_message_content) + ) + object.__setattr__( + res.input_message_content, + "link_preview_options", + DefaultValue.get_value(res.input_message_content.link_preview_options), + ) return res diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index 6c46c47a622..f718c729b35 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -17,6 +17,8 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +# ruff: noqa: UP008 +# pylint: disable=super-with-arguments """This module contains an object that represents a Telegram Bot with convenience extensions.""" import datetime as dtm @@ -95,6 +97,7 @@ Voice, WebhookInfo, ) +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import to_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.logging import get_logger @@ -142,6 +145,7 @@ KT = TypeVar("KT", bound=ReplyMarkup) +@tg_dataclass(eq=False) class ExtBot(Bot, Generic[RLARGS]): """This object represents a Telegram Bot with convenience extensions. @@ -196,7 +200,9 @@ class ExtBot(Bot, Generic[RLARGS]): """ - __slots__ = ("_callback_data_cache", "_defaults", "_rate_limiter") + _callback_data_cache: CallbackDataCache | None = tg_field(init=False) + _defaults: "Defaults | None" = tg_field(init=False) + _rate_limiter: "BaseRateLimiter[RLARGS] | None" = tg_field(init=False) _LOGGER = get_logger(__name__, class_name="ExtBot") @@ -248,7 +254,7 @@ def __init__( local_mode: bool = False, rate_limiter: "BaseRateLimiter[RLARGS] | None" = None, ): - super().__init__( + super(ExtBot, self).__init__( token=token, base_url=base_url, base_file_url=base_file_url, @@ -258,21 +264,22 @@ def __init__( private_key_password=private_key_password, local_mode=local_mode, ) - with self._unfrozen(): - self._defaults: Defaults | None = defaults - self._rate_limiter: BaseRateLimiter | None = rate_limiter - self._callback_data_cache: CallbackDataCache | None = None + object.__setattr__(self, "_defaults", defaults) + object.__setattr__(self, "_rate_limiter", rate_limiter) + object.__setattr__(self, "_callback_data_cache", None) - # set up callback_data - if arbitrary_callback_data is False: - return + # set up callback_data + if arbitrary_callback_data is False: + return - if not isinstance(arbitrary_callback_data, bool): - maxsize = cast("int", arbitrary_callback_data) - else: - maxsize = 1024 + if not isinstance(arbitrary_callback_data, bool): + maxsize = cast("int", arbitrary_callback_data) + else: + maxsize = 1024 - self._callback_data_cache = CallbackDataCache(bot=self, maxsize=maxsize) + object.__setattr__( + self, "_callback_data_cache", CallbackDataCache(bot=self, maxsize=maxsize) + ) def __repr__(self) -> str: """Give a string representation of the bot in the form ``ExtBot[token=...]``. @@ -295,7 +302,7 @@ def _warn( """We override this method to add one more level to the stacklevel, so that the warning points to the user's code, not to the PTB code. """ - super()._warn(message=message, category=category, stacklevel=stacklevel + 2) + super(ExtBot, cls)._warn(message=message, category=category, stacklevel=stacklevel + 2) @property def callback_data_cache(self) -> CallbackDataCache | None: @@ -320,7 +327,7 @@ async def initialize(self) -> None: # Initialize before calling super, because super calls get_me if self.rate_limiter: await self.rate_limiter.initialize() - await super().initialize() + await super(ExtBot, self).initialize() async def shutdown(self) -> None: """See :meth:`telegram.Bot.shutdown`. Also shuts down the @@ -330,7 +337,7 @@ async def shutdown(self) -> None: # Shut down the rate limiter before shutting down the request objects! if self.rate_limiter: await self.rate_limiter.shutdown() - await super().shutdown() + await super(ExtBot, self).shutdown() @classmethod def _merge_api_rl_kwargs( @@ -374,7 +381,7 @@ async def _do_post( # getting updates should not be rate limited! if endpoint == "getUpdates" or not self.rate_limiter: - return await super()._do_post( + return await super(ExtBot, self)._do_post( endpoint=endpoint, data=data, write_timeout=write_timeout, @@ -395,7 +402,7 @@ async def _do_post( rate_limit_args, ) return await self.rate_limiter.process_request( - callback=super()._do_post, + callback=super(ExtBot, self)._do_post, args=(endpoint, data), kwargs=kwargs, endpoint=endpoint, @@ -437,7 +444,7 @@ def _merge_lpo_defaults(self, lpo: ODVInput[LinkPreviewOptions]) -> LinkPreviewO if isinstance(orig_attr := getattr(lpo, attr), DefaultValue) else orig_attr ) - for attr in defaults_lpo.__slots__ + for attr in defaults_lpo.__slots__ # type: ignore[attr-defined] } ) @@ -452,7 +459,7 @@ def _insert_defaults(self, data: dict[str, object]) -> None: """ if self.defaults is None: # If we have no defaults to insert, the behavior is the same as in `tg.Bot` - super()._insert_defaults(data) + super(ExtBot, self)._insert_defaults(data) return # if we have Defaults, we @@ -476,8 +483,7 @@ def _insert_defaults(self, data: dict[str, object]) -> None: elif isinstance(val, InputMedia) and val.parse_mode is DEFAULT_NONE: # Copy object as not to edit it in-place copied_val = copy(val) - with copied_val._unfrozen(): - copied_val.parse_mode = self.defaults.parse_mode + object.__setattr__(copied_val, "parse_mode", self.defaults.parse_mode) data[key] = copied_val elif ( key == "media" @@ -488,8 +494,7 @@ def _insert_defaults(self, data: dict[str, object]) -> None: copy_list = [copy(media) for media in val] for media in copy_list: if media.parse_mode is DEFAULT_NONE: - with media._unfrozen(): - media.parse_mode = self.defaults.parse_mode + object.__setattr__(media, "parse_mode", self.defaults.parse_mode) data[key] = copy_list @@ -504,17 +509,24 @@ def _insert_defaults(self, data: dict[str, object]) -> None: or self.defaults.quote_parse_mode is not None ): new_value = copy(val) - with new_value._unfrozen(): - new_value.allow_sending_without_reply = ( + object.__setattr__( + new_value, + "allow_sending_without_reply", + ( defaults_aswr if isinstance(val.allow_sending_without_reply, DefaultValue) else val.allow_sending_without_reply - ) - new_value.quote_parse_mode = ( + ), + ) + object.__setattr__( + new_value, + "quote_parse_mode", + ( self.defaults.quote_parse_mode if isinstance(val.quote_parse_mode, DefaultValue) else val.quote_parse_mode - ) + ), + ) data[key] = new_value @@ -530,8 +542,9 @@ def _insert_defaults(self, data: dict[str, object]) -> None: new_val.append(option) else: new_option = copy(option) - with new_option._unfrozen(): - new_option.text_parse_mode = self.defaults.text_parse_mode + object.__setattr__( + new_option, "text_parse_mode", self.defaults.text_parse_mode + ) new_val.append(new_option) data[key] = new_val @@ -636,7 +649,7 @@ async def _send_message( ) -> Any: # We override this method to call self._replace_keyboard and self._insert_callback_data. # This covers most methods that have a reply_markup - result = await super()._send_message( + result = await super(ExtBot, self)._send_message( endpoint=endpoint, data=data, reply_to_message_id=reply_to_message_id, @@ -678,7 +691,7 @@ async def get_updates( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict | None = None, ) -> tuple[Update, ...]: - updates = await super().get_updates( + updates = await super(ExtBot, self).get_updates( offset=offset, limit=limit, timeout=timeout, @@ -706,7 +719,7 @@ def _effective_inline_results( """This method is called by Bot.answer_inline_query to build the actual results list. Overriding this to call self._replace_keyboard suffices """ - effective_results, next_offset = super()._effective_inline_results( + effective_results, next_offset = super(ExtBot, self)._effective_inline_results( results=results, next_offset=next_offset, current_offset=current_offset ) @@ -723,9 +736,8 @@ def _effective_inline_results( # We build a new result in case the user wants to use the same object in # different places new_result = copy(result) - with new_result._unfrozen(): - markup = self._replace_keyboard(result.reply_markup) - new_result.reply_markup = markup + markup = self._replace_keyboard(result.reply_markup) + object.__setattr__(new_result, "reply_markup", markup) results.append(new_result) @@ -739,15 +751,14 @@ def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQ """ if self.defaults is None: # If we have no defaults to insert, the behavior is the same as in `tg.Bot` - return super()._insert_defaults_for_ilq_results(res) + return super(ExtBot, self)._insert_defaults_for_ilq_results(res) # Copy the objects that need modification to avoid modifying the original object copied = False if hasattr(res, "parse_mode") and res.parse_mode is DEFAULT_NONE: res = copy(res) - with res._unfrozen(): - copied = True - res.parse_mode = self.defaults.parse_mode + copied = True + object.__setattr__(res, "parse_mode", self.defaults.parse_mode) if hasattr(res, "input_message_content") and res.input_message_content: if ( hasattr(res.input_message_content, "parse_mode") @@ -756,21 +767,25 @@ def _insert_defaults_for_ilq_results(self, res: "InlineQueryResult") -> "InlineQ if not copied: res = copy(res) copied = True - with res.input_message_content._unfrozen(): - res.input_message_content.parse_mode = self.defaults.parse_mode + object.__setattr__( + res.input_message_content, "parse_mode", self.defaults.parse_mode + ) if hasattr(res.input_message_content, "link_preview_options"): if not copied: res = copy(res) - with res.input_message_content._unfrozen(): - if res.input_message_content.link_preview_options is DEFAULT_NONE: - res.input_message_content.link_preview_options = ( - self.defaults.link_preview_options - ) - else: - # merge the existing options with the defaults - res.input_message_content.link_preview_options = self._merge_lpo_defaults( - res.input_message_content.link_preview_options - ) + if res.input_message_content.link_preview_options is DEFAULT_NONE: + object.__setattr__( + res.input_message_content, + "link_preview_options", + self.defaults.link_preview_options, + ) + else: + # merge the existing options with the defaults + object.__setattr__( + res.input_message_content, + "link_preview_options", + self._merge_lpo_defaults(res.input_message_content.link_preview_options), + ) return res @@ -786,7 +801,7 @@ async def do_api_request( pool_timeout: ODVInput[float] = DEFAULT_NONE, rate_limit_args: RLARGS | None = None, ) -> Any: - return await super().do_api_request( + return await super(ExtBot, self).do_api_request( endpoint=endpoint, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), return_type=return_type, @@ -811,7 +826,7 @@ async def stop_poll( rate_limit_args: RLARGS | None = None, ) -> Poll: # We override this method to call self._replace_keyboard - return await super().stop_poll( + return await super(ExtBot, self).stop_poll( chat_id=chat_id, message_id=message_id, reply_markup=self._replace_keyboard(reply_markup), @@ -853,7 +868,7 @@ async def copy_message( rate_limit_args: RLARGS | None = None, ) -> MessageId: # We override this method to call self._replace_keyboard - return await super().copy_message( + return await super(ExtBot, self).copy_message( chat_id=chat_id, from_chat_id=from_chat_id, message_id=message_id, @@ -899,7 +914,7 @@ async def copy_messages( rate_limit_args: RLARGS | None = None, ) -> tuple["MessageId", ...]: # We override this method to call self._replace_keyboard - return await super().copy_messages( + return await super(ExtBot, self).copy_messages( chat_id=chat_id, from_chat_id=from_chat_id, message_ids=message_ids, @@ -927,7 +942,7 @@ async def get_chat( rate_limit_args: RLARGS | None = None, ) -> ChatFullInfo: # We override this method to call self._insert_callback_data - result = await super().get_chat( + result = await super(ExtBot, self).get_chat( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -950,7 +965,7 @@ async def add_sticker_to_set( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().add_sticker_to_set( + return await super(ExtBot, self).add_sticker_to_set( user_id=user_id, name=name, sticker=sticker, @@ -976,7 +991,7 @@ async def answer_callback_query( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().answer_callback_query( + return await super(ExtBot, self).answer_callback_query( callback_query_id=callback_query_id, text=text, show_alert=show_alert, @@ -1008,7 +1023,7 @@ async def answer_inline_query( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().answer_inline_query( + return await super(ExtBot, self).answer_inline_query( inline_query_id=inline_query_id, results=results, cache_time=cache_time, @@ -1039,7 +1054,7 @@ async def save_prepared_inline_message( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> PreparedInlineMessage: - return await super().save_prepared_inline_message( + return await super(ExtBot, self).save_prepared_inline_message( user_id=user_id, result=result, allow_user_chats=allow_user_chats, @@ -1066,7 +1081,7 @@ async def answer_pre_checkout_query( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().answer_pre_checkout_query( + return await super(ExtBot, self).answer_pre_checkout_query( pre_checkout_query_id=pre_checkout_query_id, ok=ok, error_message=error_message, @@ -1091,7 +1106,7 @@ async def answer_shipping_query( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().answer_shipping_query( + return await super(ExtBot, self).answer_shipping_query( shipping_query_id=shipping_query_id, ok=ok, shipping_options=shipping_options, @@ -1115,7 +1130,7 @@ async def answer_web_app_query( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> SentWebAppMessage: - return await super().answer_web_app_query( + return await super(ExtBot, self).answer_web_app_query( web_app_query_id=web_app_query_id, result=result, read_timeout=read_timeout, @@ -1137,7 +1152,7 @@ async def answer_guest_query( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> SentGuestMessage: - return await super().answer_guest_query( + return await super(ExtBot, self).answer_guest_query( guest_query_id=guest_query_id, result=result, read_timeout=read_timeout, @@ -1159,7 +1174,7 @@ async def approve_chat_join_request( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().approve_chat_join_request( + return await super(ExtBot, self).approve_chat_join_request( chat_id=chat_id, user_id=user_id, read_timeout=read_timeout, @@ -1183,7 +1198,7 @@ async def ban_chat_member( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().ban_chat_member( + return await super(ExtBot, self).ban_chat_member( chat_id=chat_id, user_id=user_id, until_date=until_date, @@ -1207,7 +1222,7 @@ async def ban_chat_sender_chat( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().ban_chat_sender_chat( + return await super(ExtBot, self).ban_chat_sender_chat( chat_id=chat_id, sender_chat_id=sender_chat_id, read_timeout=read_timeout, @@ -1232,7 +1247,7 @@ async def create_chat_invite_link( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> ChatInviteLink: - return await super().create_chat_invite_link( + return await super(ExtBot, self).create_chat_invite_link( chat_id=chat_id, expire_date=expire_date, member_limit=member_limit, @@ -1277,7 +1292,7 @@ async def create_invoice_link( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> str: - return await super().create_invoice_link( + return await super(ExtBot, self).create_invoice_link( title=title, description=description, payload=payload, @@ -1323,7 +1338,7 @@ async def create_new_sticker_set( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().create_new_sticker_set( + return await super(ExtBot, self).create_new_sticker_set( user_id=user_id, name=name, title=title, @@ -1349,7 +1364,7 @@ async def decline_chat_join_request( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().decline_chat_join_request( + return await super(ExtBot, self).decline_chat_join_request( chat_id=chat_id, user_id=user_id, read_timeout=read_timeout, @@ -1370,7 +1385,7 @@ async def delete_chat_photo( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_chat_photo( + return await super(ExtBot, self).delete_chat_photo( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1390,7 +1405,7 @@ async def delete_chat_sticker_set( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_chat_sticker_set( + return await super(ExtBot, self).delete_chat_sticker_set( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1411,7 +1426,7 @@ async def delete_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_forum_topic( + return await super(ExtBot, self).delete_forum_topic( chat_id=chat_id, message_thread_id=message_thread_id, read_timeout=read_timeout, @@ -1433,7 +1448,7 @@ async def delete_message( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_message( + return await super(ExtBot, self).delete_message( chat_id=chat_id, message_id=message_id, read_timeout=read_timeout, @@ -1455,7 +1470,7 @@ async def delete_messages( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_messages( + return await super(ExtBot, self).delete_messages( chat_id=chat_id, message_ids=message_ids, read_timeout=read_timeout, @@ -1477,7 +1492,7 @@ async def delete_my_commands( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_my_commands( + return await super(ExtBot, self).delete_my_commands( scope=scope, language_code=language_code, read_timeout=read_timeout, @@ -1498,7 +1513,7 @@ async def delete_sticker_from_set( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_sticker_from_set( + return await super(ExtBot, self).delete_sticker_from_set( sticker=sticker, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1518,7 +1533,7 @@ async def delete_webhook( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_webhook( + return await super(ExtBot, self).delete_webhook( drop_pending_updates=drop_pending_updates, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1543,7 +1558,7 @@ async def edit_chat_invite_link( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> ChatInviteLink: - return await super().edit_chat_invite_link( + return await super(ExtBot, self).edit_chat_invite_link( chat_id=chat_id, invite_link=invite_link, expire_date=expire_date, @@ -1571,7 +1586,7 @@ async def edit_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().edit_forum_topic( + return await super(ExtBot, self).edit_forum_topic( chat_id=chat_id, message_thread_id=message_thread_id, name=name, @@ -1595,7 +1610,7 @@ async def edit_general_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().edit_general_forum_topic( + return await super(ExtBot, self).edit_general_forum_topic( chat_id=chat_id, name=name, read_timeout=read_timeout, @@ -1624,7 +1639,7 @@ async def edit_message_caption( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> "Message | bool": - return await super().edit_message_caption( + return await super(ExtBot, self).edit_message_caption( chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, @@ -1663,7 +1678,7 @@ async def edit_message_live_location( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> "Message | bool": - return await super().edit_message_live_location( + return await super(ExtBot, self).edit_message_live_location( chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, @@ -1699,7 +1714,7 @@ async def edit_message_media( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> "Message | bool": - return await super().edit_message_media( + return await super(ExtBot, self).edit_message_media( media=media, chat_id=chat_id, message_id=message_id, @@ -1728,7 +1743,7 @@ async def edit_message_reply_markup( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> "Message | bool": - return await super().edit_message_reply_markup( + return await super(ExtBot, self).edit_message_reply_markup( chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, @@ -1761,7 +1776,7 @@ async def edit_message_text( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> "Message | bool": - return await super().edit_message_text( + return await super(ExtBot, self).edit_message_text( text=text, chat_id=chat_id, message_id=message_id, @@ -1790,7 +1805,7 @@ async def export_chat_invite_link( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> str: - return await super().export_chat_invite_link( + return await super(ExtBot, self).export_chat_invite_link( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1819,7 +1834,7 @@ async def forward_message( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().forward_message( + return await super(ExtBot, self).forward_message( chat_id=chat_id, from_chat_id=from_chat_id, message_id=message_id, @@ -1854,7 +1869,7 @@ async def forward_messages( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> tuple[MessageId, ...]: - return await super().forward_messages( + return await super(ExtBot, self).forward_messages( chat_id=chat_id, from_chat_id=from_chat_id, message_ids=message_ids, @@ -1881,7 +1896,7 @@ async def get_chat_administrators( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> tuple[ChatMember, ...]: - return await super().get_chat_administrators( + return await super(ExtBot, self).get_chat_administrators( chat_id=chat_id, return_bots=return_bots, read_timeout=read_timeout, @@ -1903,7 +1918,7 @@ async def get_chat_member( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> ChatMember: - return await super().get_chat_member( + return await super(ExtBot, self).get_chat_member( chat_id=chat_id, user_id=user_id, read_timeout=read_timeout, @@ -1924,7 +1939,7 @@ async def get_chat_member_count( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> int: - return await super().get_chat_member_count( + return await super(ExtBot, self).get_chat_member_count( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1944,7 +1959,7 @@ async def get_chat_menu_button( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> MenuButton: - return await super().get_chat_menu_button( + return await super(ExtBot, self).get_chat_menu_button( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1975,7 +1990,7 @@ async def get_file( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> File: - return await super().get_file( + return await super(ExtBot, self).get_file( file_id=file_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -1994,7 +2009,7 @@ async def get_forum_topic_icon_stickers( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> tuple[Sticker, ...]: - return await super().get_forum_topic_icon_stickers( + return await super(ExtBot, self).get_forum_topic_icon_stickers( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2016,7 +2031,7 @@ async def get_game_high_scores( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> tuple[GameHighScore, ...]: - return await super().get_game_high_scores( + return await super(ExtBot, self).get_game_high_scores( user_id=user_id, chat_id=chat_id, message_id=message_id, @@ -2038,7 +2053,7 @@ async def get_me( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> User: - return await super().get_me( + return await super(ExtBot, self).get_me( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2058,7 +2073,7 @@ async def get_my_commands( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> tuple[BotCommand, ...]: - return await super().get_my_commands( + return await super(ExtBot, self).get_my_commands( scope=scope, language_code=language_code, read_timeout=read_timeout, @@ -2079,7 +2094,7 @@ async def get_my_default_administrator_rights( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> ChatAdministratorRights: - return await super().get_my_default_administrator_rights( + return await super(ExtBot, self).get_my_default_administrator_rights( for_channels=for_channels, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2099,7 +2114,7 @@ async def get_sticker_set( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> StickerSet: - return await super().get_sticker_set( + return await super(ExtBot, self).get_sticker_set( name=name, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2119,7 +2134,7 @@ async def get_custom_emoji_stickers( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> tuple[Sticker, ...]: - return await super().get_custom_emoji_stickers( + return await super(ExtBot, self).get_custom_emoji_stickers( custom_emoji_ids=custom_emoji_ids, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2141,7 +2156,7 @@ async def get_user_profile_photos( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> "UserProfilePhotos": - return await super().get_user_profile_photos( + return await super(ExtBot, self).get_user_profile_photos( user_id=user_id, offset=offset, limit=limit, @@ -2162,7 +2177,7 @@ async def get_webhook_info( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> WebhookInfo: - return await super().get_webhook_info( + return await super(ExtBot, self).get_webhook_info( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2181,7 +2196,7 @@ async def leave_chat( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().leave_chat( + return await super(ExtBot, self).leave_chat( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2200,7 +2215,7 @@ async def log_out( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().log_out( + return await super(ExtBot, self).log_out( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2218,7 +2233,7 @@ async def close( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().close( + return await super(ExtBot, self).close( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -2238,7 +2253,7 @@ async def close_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().close_forum_topic( + return await super(ExtBot, self).close_forum_topic( chat_id=chat_id, message_thread_id=message_thread_id, read_timeout=read_timeout, @@ -2259,7 +2274,7 @@ async def close_general_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().close_general_forum_topic( + return await super(ExtBot, self).close_general_forum_topic( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2282,7 +2297,7 @@ async def create_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> ForumTopic: - return await super().create_forum_topic( + return await super(ExtBot, self).create_forum_topic( chat_id=chat_id, name=name, icon_color=icon_color, @@ -2305,7 +2320,7 @@ async def reopen_general_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().reopen_general_forum_topic( + return await super(ExtBot, self).reopen_general_forum_topic( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2325,7 +2340,7 @@ async def hide_general_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().hide_general_forum_topic( + return await super(ExtBot, self).hide_general_forum_topic( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2345,7 +2360,7 @@ async def unhide_general_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().unhide_general_forum_topic( + return await super(ExtBot, self).unhide_general_forum_topic( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -2368,7 +2383,7 @@ async def pin_chat_message( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().pin_chat_message( + return await super(ExtBot, self).pin_chat_message( chat_id=chat_id, message_id=message_id, disable_notification=disable_notification, @@ -2409,7 +2424,7 @@ async def promote_chat_member( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().promote_chat_member( + return await super(ExtBot, self).promote_chat_member( chat_id=chat_id, user_id=user_id, can_change_info=can_change_info, @@ -2448,7 +2463,7 @@ async def reopen_forum_topic( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().reopen_forum_topic( + return await super(ExtBot, self).reopen_forum_topic( chat_id=chat_id, message_thread_id=message_thread_id, read_timeout=read_timeout, @@ -2473,7 +2488,7 @@ async def restrict_chat_member( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().restrict_chat_member( + return await super(ExtBot, self).restrict_chat_member( chat_id=chat_id, user_id=user_id, permissions=permissions, @@ -2498,7 +2513,7 @@ async def revoke_chat_invite_link( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> ChatInviteLink: - return await super().revoke_chat_invite_link( + return await super(ExtBot, self).revoke_chat_invite_link( chat_id=chat_id, invite_link=invite_link, read_timeout=read_timeout, @@ -2542,7 +2557,7 @@ async def send_animation( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_animation( + return await super(ExtBot, self).send_animation( chat_id=chat_id, animation=animation, duration=duration, @@ -2606,7 +2621,7 @@ async def send_audio( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_audio( + return await super(ExtBot, self).send_audio( chat_id=chat_id, audio=audio, duration=duration, @@ -2650,7 +2665,7 @@ async def send_chat_action( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().send_chat_action( + return await super(ExtBot, self).send_chat_action( chat_id=chat_id, business_connection_id=business_connection_id, action=action, @@ -2690,7 +2705,7 @@ async def send_contact( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_contact( + return await super(ExtBot, self).send_contact( chat_id=chat_id, phone_number=phone_number, first_name=first_name, @@ -2736,7 +2751,7 @@ async def send_checklist( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_checklist( + return await super(ExtBot, self).send_checklist( business_connection_id=business_connection_id, chat_id=chat_id, checklist=checklist, @@ -2769,7 +2784,7 @@ async def edit_message_checklist( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().edit_message_checklist( + return await super(ExtBot, self).edit_message_checklist( business_connection_id=business_connection_id, chat_id=chat_id, message_id=message_id, @@ -2806,7 +2821,7 @@ async def send_dice( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_dice( + return await super(ExtBot, self).send_dice( chat_id=chat_id, disable_notification=disable_notification, business_connection_id=business_connection_id, @@ -2858,7 +2873,7 @@ async def send_document( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_document( + return await super(ExtBot, self).send_document( chat_id=chat_id, document=document, caption=caption, @@ -2908,7 +2923,7 @@ async def send_game( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_game( + return await super(ExtBot, self).send_game( chat_id=chat_id, game_short_name=game_short_name, disable_notification=disable_notification, @@ -2971,7 +2986,7 @@ async def send_invoice( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_invoice( + return await super(ExtBot, self).send_invoice( chat_id=chat_id, title=title, description=description, @@ -3042,7 +3057,7 @@ async def send_location( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_location( + return await super(ExtBot, self).send_location( chat_id=chat_id, latitude=latitude, longitude=longitude, @@ -3097,7 +3112,7 @@ async def send_media_group( parse_mode: ODVInput[str] = DEFAULT_NONE, caption_entities: Sequence["MessageEntity"] | None = None, ) -> tuple[Message, ...]: - return await super().send_media_group( + return await super(ExtBot, self).send_media_group( chat_id=chat_id, media=media, disable_notification=disable_notification, @@ -3148,7 +3163,7 @@ async def send_message( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_message( + return await super(ExtBot, self).send_message( chat_id=chat_id, text=text, parse_mode=parse_mode, @@ -3190,7 +3205,7 @@ async def send_message_draft( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().send_message_draft( + return await super(ExtBot, self).send_message_draft( chat_id=chat_id, draft_id=draft_id, text=text, @@ -3234,7 +3249,7 @@ async def send_photo( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_photo( + return await super(ExtBot, self).send_photo( chat_id=chat_id, photo=photo, caption=caption, @@ -3308,7 +3323,7 @@ async def send_poll( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_poll( + return await super(ExtBot, self).send_poll( chat_id=chat_id, question=question, options=options, @@ -3377,7 +3392,7 @@ async def send_sticker( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_sticker( + return await super(ExtBot, self).send_sticker( chat_id=chat_id, sticker=sticker, disable_notification=disable_notification, @@ -3432,7 +3447,7 @@ async def send_venue( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_venue( + return await super(ExtBot, self).send_venue( chat_id=chat_id, latitude=latitude, longitude=longitude, @@ -3499,7 +3514,7 @@ async def send_video( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_video( + return await super(ExtBot, self).send_video( chat_id=chat_id, video=video, duration=duration, @@ -3562,7 +3577,7 @@ async def send_video_note( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_video_note( + return await super(ExtBot, self).send_video_note( chat_id=chat_id, video_note=video_note, duration=duration, @@ -3617,7 +3632,7 @@ async def send_voice( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_voice( + return await super(ExtBot, self).send_voice( chat_id=chat_id, voice=voice, duration=duration, @@ -3657,7 +3672,7 @@ async def set_chat_administrator_custom_title( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_chat_administrator_custom_title( + return await super(ExtBot, self).set_chat_administrator_custom_title( chat_id=chat_id, user_id=user_id, custom_title=custom_title, @@ -3680,7 +3695,7 @@ async def set_chat_description( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_chat_description( + return await super(ExtBot, self).set_chat_description( chat_id=chat_id, description=description, read_timeout=read_timeout, @@ -3703,7 +3718,7 @@ async def set_user_emoji_status( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_user_emoji_status( + return await super(ExtBot, self).set_user_emoji_status( user_id=user_id, emoji_status_custom_emoji_id=emoji_status_custom_emoji_id, emoji_status_expiration_date=emoji_status_expiration_date, @@ -3726,7 +3741,7 @@ async def set_chat_menu_button( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_chat_menu_button( + return await super(ExtBot, self).set_chat_menu_button( chat_id=chat_id, menu_button=menu_button, read_timeout=read_timeout, @@ -3749,7 +3764,7 @@ async def set_chat_permissions( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_chat_permissions( + return await super(ExtBot, self).set_chat_permissions( chat_id=chat_id, permissions=permissions, use_independent_chat_permissions=use_independent_chat_permissions, @@ -3772,7 +3787,7 @@ async def set_chat_photo( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_chat_photo( + return await super(ExtBot, self).set_chat_photo( chat_id=chat_id, photo=photo, read_timeout=read_timeout, @@ -3794,7 +3809,7 @@ async def set_chat_sticker_set( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_chat_sticker_set( + return await super(ExtBot, self).set_chat_sticker_set( chat_id=chat_id, sticker_set_name=sticker_set_name, read_timeout=read_timeout, @@ -3816,7 +3831,7 @@ async def set_chat_title( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_chat_title( + return await super(ExtBot, self).set_chat_title( chat_id=chat_id, title=title, read_timeout=read_timeout, @@ -3843,7 +3858,7 @@ async def set_game_score( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> "Message | bool": - return await super().set_game_score( + return await super(ExtBot, self).set_game_score( user_id=user_id, score=score, chat_id=chat_id, @@ -3871,7 +3886,7 @@ async def set_my_commands( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_my_commands( + return await super(ExtBot, self).set_my_commands( commands=commands, scope=scope, language_code=language_code, @@ -3894,7 +3909,7 @@ async def set_my_default_administrator_rights( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_my_default_administrator_rights( + return await super(ExtBot, self).set_my_default_administrator_rights( rights=rights, for_channels=for_channels, read_timeout=read_timeout, @@ -3916,7 +3931,7 @@ async def set_passport_data_errors( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_passport_data_errors( + return await super(ExtBot, self).set_passport_data_errors( user_id=user_id, errors=errors, read_timeout=read_timeout, @@ -3938,7 +3953,7 @@ async def set_sticker_position_in_set( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_sticker_position_in_set( + return await super(ExtBot, self).set_sticker_position_in_set( sticker=sticker, position=position, read_timeout=read_timeout, @@ -3962,7 +3977,7 @@ async def set_sticker_set_thumbnail( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_sticker_set_thumbnail( + return await super(ExtBot, self).set_sticker_set_thumbnail( name=name, user_id=user_id, thumbnail=thumbnail, @@ -3991,7 +4006,7 @@ async def set_webhook( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_webhook( + return await super(ExtBot, self).set_webhook( url=url, certificate=certificate, max_connections=max_connections, @@ -4021,7 +4036,7 @@ async def stop_message_live_location( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> "Message | bool": - return await super().stop_message_live_location( + return await super(ExtBot, self).stop_message_live_location( chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, @@ -4047,7 +4062,7 @@ async def unban_chat_member( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().unban_chat_member( + return await super(ExtBot, self).unban_chat_member( chat_id=chat_id, user_id=user_id, only_if_banned=only_if_banned, @@ -4070,7 +4085,7 @@ async def unban_chat_sender_chat( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().unban_chat_sender_chat( + return await super(ExtBot, self).unban_chat_sender_chat( chat_id=chat_id, sender_chat_id=sender_chat_id, read_timeout=read_timeout, @@ -4091,7 +4106,7 @@ async def unpin_all_chat_messages( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().unpin_all_chat_messages( + return await super(ExtBot, self).unpin_all_chat_messages( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -4113,7 +4128,7 @@ async def unpin_chat_message( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().unpin_chat_message( + return await super(ExtBot, self).unpin_chat_message( chat_id=chat_id, message_id=message_id, read_timeout=read_timeout, @@ -4136,7 +4151,7 @@ async def unpin_all_forum_topic_messages( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().unpin_all_forum_topic_messages( + return await super(ExtBot, self).unpin_all_forum_topic_messages( chat_id=chat_id, message_thread_id=message_thread_id, read_timeout=read_timeout, @@ -4157,7 +4172,7 @@ async def unpin_all_general_forum_topic_messages( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().unpin_all_general_forum_topic_messages( + return await super(ExtBot, self).unpin_all_general_forum_topic_messages( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -4179,7 +4194,7 @@ async def upload_sticker_file( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> File: - return await super().upload_sticker_file( + return await super(ExtBot, self).upload_sticker_file( user_id=user_id, sticker=sticker, sticker_format=sticker_format, @@ -4202,7 +4217,7 @@ async def set_my_description( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_my_description( + return await super(ExtBot, self).set_my_description( description=description, language_code=language_code, read_timeout=read_timeout, @@ -4224,7 +4239,7 @@ async def set_my_short_description( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_my_short_description( + return await super(ExtBot, self).set_my_short_description( short_description=short_description, language_code=language_code, read_timeout=read_timeout, @@ -4245,7 +4260,7 @@ async def get_my_description( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> BotDescription: - return await super().get_my_description( + return await super(ExtBot, self).get_my_description( language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, @@ -4265,7 +4280,7 @@ async def get_my_short_description( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> BotShortDescription: - return await super().get_my_short_description( + return await super(ExtBot, self).get_my_short_description( language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, @@ -4286,7 +4301,7 @@ async def set_my_name( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_my_name( + return await super(ExtBot, self).set_my_name( name=name, language_code=language_code, read_timeout=read_timeout, @@ -4307,7 +4322,7 @@ async def get_my_name( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> BotName: - return await super().get_my_name( + return await super(ExtBot, self).get_my_name( language_code=language_code, read_timeout=read_timeout, write_timeout=write_timeout, @@ -4328,7 +4343,7 @@ async def set_custom_emoji_sticker_set_thumbnail( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_custom_emoji_sticker_set_thumbnail( + return await super(ExtBot, self).set_custom_emoji_sticker_set_thumbnail( name=name, custom_emoji_id=custom_emoji_id, read_timeout=read_timeout, @@ -4350,7 +4365,7 @@ async def set_sticker_set_title( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_sticker_set_title( + return await super(ExtBot, self).set_sticker_set_title( name=name, title=title, read_timeout=read_timeout, @@ -4371,7 +4386,7 @@ async def delete_sticker_set( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_sticker_set( + return await super(ExtBot, self).delete_sticker_set( name=name, read_timeout=read_timeout, write_timeout=write_timeout, @@ -4392,7 +4407,7 @@ async def set_sticker_emoji_list( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_sticker_emoji_list( + return await super(ExtBot, self).set_sticker_emoji_list( sticker=sticker, emoji_list=emoji_list, read_timeout=read_timeout, @@ -4414,7 +4429,7 @@ async def set_sticker_keywords( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_sticker_keywords( + return await super(ExtBot, self).set_sticker_keywords( sticker=sticker, keywords=keywords, read_timeout=read_timeout, @@ -4436,7 +4451,7 @@ async def set_sticker_mask_position( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_sticker_mask_position( + return await super(ExtBot, self).set_sticker_mask_position( sticker=sticker, mask_position=mask_position, read_timeout=read_timeout, @@ -4458,7 +4473,7 @@ async def get_user_chat_boosts( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> UserChatBoosts: - return await super().get_user_chat_boosts( + return await super(ExtBot, self).get_user_chat_boosts( chat_id=chat_id, user_id=user_id, read_timeout=read_timeout, @@ -4482,7 +4497,7 @@ async def set_message_reaction( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_message_reaction( + return await super(ExtBot, self).set_message_reaction( chat_id=chat_id, message_id=message_id, reaction=reaction, @@ -4510,7 +4525,7 @@ async def gift_premium_subscription( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().gift_premium_subscription( + return await super(ExtBot, self).gift_premium_subscription( user_id=user_id, month_count=month_count, star_count=star_count, @@ -4535,7 +4550,7 @@ async def get_business_connection( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> BusinessConnection: - return await super().get_business_connection( + return await super(ExtBot, self).get_business_connection( business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -4565,7 +4580,7 @@ async def get_business_account_gifts( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> OwnedGifts: - return await super().get_business_account_gifts( + return await super(ExtBot, self).get_business_account_gifts( business_connection_id=business_connection_id, exclude_unsaved=exclude_unsaved, exclude_saved=exclude_saved, @@ -4595,7 +4610,7 @@ async def get_business_account_star_balance( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> StarAmount: - return await super().get_business_account_star_balance( + return await super(ExtBot, self).get_business_account_star_balance( business_connection_id=business_connection_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -4617,7 +4632,7 @@ async def read_business_message( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().read_business_message( + return await super(ExtBot, self).read_business_message( business_connection_id=business_connection_id, chat_id=chat_id, message_id=message_id, @@ -4640,7 +4655,7 @@ async def delete_business_messages( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_business_messages( + return await super(ExtBot, self).delete_business_messages( business_connection_id=business_connection_id, message_ids=message_ids, read_timeout=read_timeout, @@ -4669,7 +4684,7 @@ async def post_story( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Story: - return await super().post_story( + return await super(ExtBot, self).post_story( business_connection_id=business_connection_id, content=content, active_period=active_period, @@ -4703,7 +4718,7 @@ async def edit_story( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Story: - return await super().edit_story( + return await super(ExtBot, self).edit_story( business_connection_id=business_connection_id, story_id=story_id, content=content, @@ -4730,7 +4745,7 @@ async def delete_story( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_story( + return await super(ExtBot, self).delete_story( business_connection_id=business_connection_id, story_id=story_id, read_timeout=read_timeout, @@ -4753,7 +4768,7 @@ async def set_business_account_name( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_business_account_name( + return await super(ExtBot, self).set_business_account_name( business_connection_id=business_connection_id, first_name=first_name, last_name=last_name, @@ -4776,7 +4791,7 @@ async def set_business_account_username( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_business_account_username( + return await super(ExtBot, self).set_business_account_username( business_connection_id=business_connection_id, username=username, read_timeout=read_timeout, @@ -4798,7 +4813,7 @@ async def set_business_account_bio( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_business_account_bio( + return await super(ExtBot, self).set_business_account_bio( business_connection_id=business_connection_id, bio=bio, read_timeout=read_timeout, @@ -4821,7 +4836,7 @@ async def set_business_account_gift_settings( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_business_account_gift_settings( + return await super(ExtBot, self).set_business_account_gift_settings( business_connection_id=business_connection_id, show_gift_button=show_gift_button, accepted_gift_types=accepted_gift_types, @@ -4845,7 +4860,7 @@ async def set_business_account_profile_photo( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_business_account_profile_photo( + return await super(ExtBot, self).set_business_account_profile_photo( business_connection_id=business_connection_id, photo=photo, is_public=is_public, @@ -4868,7 +4883,7 @@ async def remove_business_account_profile_photo( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().remove_business_account_profile_photo( + return await super(ExtBot, self).remove_business_account_profile_photo( business_connection_id=business_connection_id, is_public=is_public, read_timeout=read_timeout, @@ -4890,7 +4905,7 @@ async def convert_gift_to_stars( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().convert_gift_to_stars( + return await super(ExtBot, self).convert_gift_to_stars( business_connection_id=business_connection_id, owned_gift_id=owned_gift_id, read_timeout=read_timeout, @@ -4914,7 +4929,7 @@ async def upgrade_gift( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().upgrade_gift( + return await super(ExtBot, self).upgrade_gift( business_connection_id=business_connection_id, owned_gift_id=owned_gift_id, keep_original_details=keep_original_details, @@ -4940,7 +4955,7 @@ async def transfer_gift( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().transfer_gift( + return await super(ExtBot, self).transfer_gift( business_connection_id=business_connection_id, owned_gift_id=owned_gift_id, new_owner_chat_id=new_owner_chat_id, @@ -4964,7 +4979,7 @@ async def transfer_business_account_stars( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().transfer_business_account_stars( + return await super(ExtBot, self).transfer_business_account_stars( business_connection_id=business_connection_id, star_count=star_count, read_timeout=read_timeout, @@ -4988,7 +5003,7 @@ async def replace_sticker_in_set( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().replace_sticker_in_set( + return await super(ExtBot, self).replace_sticker_in_set( user_id=user_id, name=name, old_sticker=old_sticker, @@ -5012,7 +5027,7 @@ async def refund_star_payment( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().refund_star_payment( + return await super(ExtBot, self).refund_star_payment( user_id=user_id, telegram_payment_charge_id=telegram_payment_charge_id, read_timeout=read_timeout, @@ -5034,7 +5049,7 @@ async def get_star_transactions( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> StarTransactions: - return await super().get_star_transactions( + return await super(ExtBot, self).get_star_transactions( offset=offset, limit=limit, read_timeout=read_timeout, @@ -5057,7 +5072,7 @@ async def edit_user_star_subscription( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().edit_user_star_subscription( + return await super(ExtBot, self).edit_user_star_subscription( user_id=user_id, telegram_payment_charge_id=telegram_payment_charge_id, is_canceled=is_canceled, @@ -5080,7 +5095,7 @@ async def get_managed_bot_access_settings( rate_limit_args: RLARGS | None = None, ) -> BotAccessSettings: - return await super().get_managed_bot_access_settings( + return await super(ExtBot, self).get_managed_bot_access_settings( user_id=user_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -5103,7 +5118,7 @@ async def set_managed_bot_access_settings( rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_managed_bot_access_settings( + return await super(ExtBot, self).set_managed_bot_access_settings( user_id=user_id, is_access_restricted=is_access_restricted, added_user_ids=added_user_ids, @@ -5126,7 +5141,7 @@ async def get_user_personal_chat_messages( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> tuple[Message, ...]: - return await super().get_user_personal_chat_messages( + return await super(ExtBot, self).get_user_personal_chat_messages( user_id=user_id, limit=limit, read_timeout=read_timeout, @@ -5165,7 +5180,7 @@ async def send_paid_media( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_paid_media( + return await super(ExtBot, self).send_paid_media( chat_id=chat_id, star_count=star_count, media=media, @@ -5206,7 +5221,7 @@ async def create_chat_subscription_invite_link( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> ChatInviteLink: - return await super().create_chat_subscription_invite_link( + return await super(ExtBot, self).create_chat_subscription_invite_link( chat_id=chat_id, subscription_period=subscription_period, subscription_price=subscription_price, @@ -5231,7 +5246,7 @@ async def edit_chat_subscription_invite_link( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> ChatInviteLink: - return await super().edit_chat_subscription_invite_link( + return await super(ExtBot, self).edit_chat_subscription_invite_link( chat_id=chat_id, invite_link=invite_link, name=name, @@ -5252,7 +5267,7 @@ async def get_available_gifts( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Gifts: - return await super().get_available_gifts( + return await super(ExtBot, self).get_available_gifts( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -5277,7 +5292,7 @@ async def send_gift( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().send_gift( + return await super(ExtBot, self).send_gift( user_id=user_id, chat_id=chat_id, gift_id=gift_id, @@ -5304,7 +5319,7 @@ async def verify_chat( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().verify_chat( + return await super(ExtBot, self).verify_chat( chat_id=chat_id, custom_description=custom_description, read_timeout=read_timeout, @@ -5326,7 +5341,7 @@ async def verify_user( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().verify_user( + return await super(ExtBot, self).verify_user( user_id=user_id, custom_description=custom_description, read_timeout=read_timeout, @@ -5347,7 +5362,7 @@ async def remove_chat_verification( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().remove_chat_verification( + return await super(ExtBot, self).remove_chat_verification( chat_id=chat_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -5367,7 +5382,7 @@ async def remove_user_verification( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().remove_user_verification( + return await super(ExtBot, self).remove_user_verification( user_id=user_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -5386,7 +5401,7 @@ async def get_my_star_balance( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> StarAmount: - return await super().get_my_star_balance( + return await super(ExtBot, self).get_my_star_balance( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -5407,7 +5422,7 @@ async def decline_suggested_post( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().decline_suggested_post( + return await super(ExtBot, self).decline_suggested_post( chat_id=chat_id, message_id=message_id, comment=comment, @@ -5431,7 +5446,7 @@ async def approve_suggested_post( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().approve_suggested_post( + return await super(ExtBot, self).approve_suggested_post( chat_id=chat_id, message_id=message_id, send_date=send_date, @@ -5458,7 +5473,7 @@ async def repost_story( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> Story: - return await super().repost_story( + return await super(ExtBot, self).repost_story( business_connection_id=business_connection_id, from_chat_id=from_chat_id, from_story_id=from_story_id, @@ -5491,7 +5506,7 @@ async def get_user_gifts( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> OwnedGifts: - return await super().get_user_gifts( + return await super(ExtBot, self).get_user_gifts( user_id=user_id, exclude_unlimited=exclude_unlimited, exclude_limited_upgradable=exclude_limited_upgradable, @@ -5529,7 +5544,7 @@ async def get_chat_gifts( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> OwnedGifts: - return await super().get_chat_gifts( + return await super(ExtBot, self).get_chat_gifts( chat_id=chat_id, exclude_unsaved=exclude_unsaved, exclude_saved=exclude_saved, @@ -5559,7 +5574,7 @@ async def set_my_profile_photo( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_my_profile_photo( + return await super(ExtBot, self).set_my_profile_photo( photo=photo, read_timeout=read_timeout, write_timeout=write_timeout, @@ -5578,7 +5593,7 @@ async def remove_my_profile_photo( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().remove_my_profile_photo( + return await super(ExtBot, self).remove_my_profile_photo( read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, @@ -5599,7 +5614,7 @@ async def get_user_profile_audios( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> UserProfileAudios: - return await super().get_user_profile_audios( + return await super(ExtBot, self).get_user_profile_audios( user_id=user_id, offset=offset, limit=limit, @@ -5623,7 +5638,7 @@ async def set_chat_member_tag( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().set_chat_member_tag( + return await super(ExtBot, self).set_chat_member_tag( chat_id=chat_id, user_id=user_id, tag=tag, @@ -5645,7 +5660,7 @@ async def get_managed_bot_token( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> str: - return await super().get_managed_bot_token( + return await super(ExtBot, self).get_managed_bot_token( user_id=user_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -5665,7 +5680,7 @@ async def replace_managed_bot_token( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> str: - return await super().replace_managed_bot_token( + return await super(ExtBot, self).replace_managed_bot_token( user_id=user_id, read_timeout=read_timeout, write_timeout=write_timeout, @@ -5687,7 +5702,7 @@ async def save_prepared_keyboard_button( rate_limit_args: RLARGS | None = None, ) -> PreparedKeyboardButton: - return await super().save_prepared_keyboard_button( + return await super(ExtBot, self).save_prepared_keyboard_button( user_id=user_id, button=button, read_timeout=read_timeout, @@ -5729,7 +5744,7 @@ async def send_live_photo( rate_limit_args: RLARGS | None = None, ) -> Message: - return await super().send_live_photo( + return await super(ExtBot, self).send_live_photo( chat_id=chat_id, live_photo=live_photo, photo=photo, @@ -5772,7 +5787,7 @@ async def delete_message_reaction( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_message_reaction( + return await super(ExtBot, self).delete_message_reaction( chat_id=chat_id, message_id=message_id, user_id=user_id, @@ -5797,7 +5812,7 @@ async def delete_all_message_reactions( api_kwargs: JSONDict | None = None, rate_limit_args: RLARGS | None = None, ) -> bool: - return await super().delete_all_message_reactions( + return await super(ExtBot, self).delete_all_message_reactions( chat_id=chat_id, user_id=user_id, actor_chat_id=actor_chat_id, From 60c1ea0358fc5f135e8809e21eb84367f64fd942 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:07:32 +0300 Subject: [PATCH 11/31] Remove `test_slot_behaviour()` where its automated That makes all TOs, other application classes still have their tests --- tests/_files/test_animation.py | 6 -- tests/_files/test_audio.py | 6 -- tests/_files/test_chatphoto.py | 6 -- tests/_files/test_contact.py | 6 -- tests/_files/test_document.py | 6 -- tests/_files/test_file.py | 6 -- tests/_files/test_inputprofilephoto.py | 13 ---- tests/_files/test_inputsticker.py | 7 -- tests/_files/test_inputstorycontent.py | 19 ------ tests/_files/test_livephoto.py | 6 -- tests/_files/test_location.py | 6 -- tests/_files/test_photo.py | 6 -- tests/_files/test_sticker.py | 18 ----- tests/_files/test_venue.py | 6 -- tests/_files/test_video.py | 6 -- tests/_files/test_videonote.py | 6 -- tests/_files/test_videoquality.py | 8 --- tests/_files/test_voice.py | 6 -- tests/_games/test_game.py | 6 -- tests/_games/test_gamehighscore.py | 6 -- tests/_inline/test_inlinekeyboardbutton.py | 7 -- tests/_inline/test_inlinekeyboardmarkup.py | 7 -- tests/_inline/test_inlinequery.py | 6 -- .../_inline/test_inlinequeryresultarticle.py | 7 -- tests/_inline/test_inlinequeryresultaudio.py | 7 -- .../test_inlinequeryresultcachedaudio.py | 7 -- .../test_inlinequeryresultcacheddocument.py | 7 -- .../test_inlinequeryresultcachedgif.py | 7 -- .../test_inlinequeryresultcachedmpeg4gif.py | 7 -- .../test_inlinequeryresultcachedphoto.py | 7 -- .../test_inlinequeryresultcachedsticker.py | 7 -- .../test_inlinequeryresultcachedvideo.py | 7 -- .../test_inlinequeryresultcachedvoice.py | 7 -- .../_inline/test_inlinequeryresultcontact.py | 7 -- .../_inline/test_inlinequeryresultdocument.py | 7 -- tests/_inline/test_inlinequeryresultgame.py | 7 -- tests/_inline/test_inlinequeryresultgif.py | 7 -- .../_inline/test_inlinequeryresultlocation.py | 7 -- .../_inline/test_inlinequeryresultmpeg4gif.py | 7 -- tests/_inline/test_inlinequeryresultphoto.py | 7 -- .../_inline/test_inlinequeryresultsbutton.py | 7 -- tests/_inline/test_inlinequeryresultvenue.py | 7 -- tests/_inline/test_inlinequeryresultvideo.py | 7 -- tests/_inline/test_inlinequeryresultvoice.py | 7 -- .../test_inputcontactmessagecontent.py | 7 -- .../test_inputinvoicemessagecontent.py | 7 -- .../test_inputlocationmessagecontent.py | 7 -- tests/_inline/test_inputtextmessagecontent.py | 7 -- .../_inline/test_inputvenuemessagecontent.py | 7 -- tests/_inline/test_preparedinlinemessage.py | 7 -- tests/_passport/test_encryptedcredentials.py | 7 -- .../test_encryptedpassportelement.py | 7 -- tests/_passport/test_passport.py | 7 -- .../test_passportelementerrordatafield.py | 1 + .../test_passportelementerrorfile.py | 1 + .../test_passportelementerrorfiles.py | 1 + .../test_passportelementerrorfrontside.py | 1 + .../test_passportelementerrorreverseside.py | 1 + .../test_passportelementerrorselfie.py | 1 + ...est_passportelementerrortranslationfile.py | 1 + ...st_passportelementerrortranslationfiles.py | 1 + .../test_passportelementerrorunspecified.py | 1 + tests/_passport/test_passportfile.py | 7 -- tests/_payment/stars/test_affiliateinfo.py | 7 -- .../stars/test_revenuewithdrawelstate.py | 25 ------- tests/_payment/stars/test_staramount.py | 7 -- tests/_payment/stars/test_startransactions.py | 13 ---- .../_payment/stars/test_transactionpartner.py | 49 -------------- tests/_payment/test_invoice.py | 6 -- tests/_payment/test_labeledprice.py | 7 -- tests/_payment/test_orderinfo.py | 6 -- tests/_payment/test_precheckoutquery.py | 7 -- tests/_payment/test_refundedpayment.py | 7 -- tests/_payment/test_shippingaddress.py | 7 -- tests/_payment/test_shippingoption.py | 7 -- tests/_payment/test_shippingquery.py | 7 -- tests/_payment/test_successfulpayment.py | 7 -- tests/test_birthdate.py | 6 -- tests/test_botaccesssettings.py | 7 -- tests/test_botcommand.py | 6 -- tests/test_botcommandscope.py | 49 -------------- tests/test_botdescription.py | 15 ----- tests/test_botname.py | 6 -- tests/test_business_classes.py | 43 ------------ tests/test_callbackquery.py | 6 -- tests/test_chat.py | 6 -- tests/test_chatadministratorrights.py | 7 -- tests/test_chatbackground.py | 61 ----------------- tests/test_chatboost.py | 55 --------------- tests/test_chatfullinfo.py | 8 --- tests/test_chatinvitelink.py | 6 -- tests/test_chatjoinrequest.py | 7 -- tests/test_chatlocation.py | 7 -- tests/test_chatmember.py | 43 ------------ tests/test_chatmemberupdated.py | 7 -- tests/test_chatowner.py | 13 ---- tests/test_chatpermissions.py | 7 -- tests/test_checklists.py | 27 -------- tests/test_choseninlineresult.py | 7 -- tests/test_copytextbutton.py | 8 --- tests/test_dice.py | 6 -- tests/test_directmessagepricechanged.py | 7 -- tests/test_directmessagestopic.py | 8 --- tests/test_forcereply.py | 6 -- tests/test_forum.py | 43 ------------ tests/test_gifts.py | 30 --------- tests/test_giveaway.py | 27 -------- tests/test_inputchecklist.py | 15 ----- tests/test_keyboardbutton.py | 7 -- tests/test_keyboardbuttonpolltype.py | 7 -- tests/test_keyboardbuttonrequest.py | 13 ---- tests/test_keyboardbuttonrequestmanagedbot.py | 7 -- tests/test_linkpreviewoptions.py | 7 -- tests/test_loginurl.py | 6 -- tests/test_managedbot.py | 13 ---- tests/test_maybeinaccessiblemessage.py | 1 + tests/test_menubutton.py | 25 ------- tests/test_message.py | 12 ---- tests/test_messageautodeletetimerchanged.py | 7 -- tests/test_messageentity.py | 7 -- tests/test_messageid.py | 6 -- tests/test_messageorigin.py | 7 -- tests/test_ownedgift.py | 24 ------- tests/test_paidmedia.py | 43 ------------ tests/test_paidmessagepricechanged.py | 10 --- tests/test_poll.py | 32 --------- tests/test_preparedkeyboardbutton.py | 7 -- tests/test_proximityalerttriggered.py | 7 -- tests/test_reaction.py | 32 --------- tests/test_reply.py | 20 ------ tests/test_replykeyboardmarkup.py | 7 -- tests/test_replykeyboardremove.py | 7 -- tests/test_sentguestmessage.py | 7 -- tests/test_sentwebappmessage.py | 7 -- tests/test_shared.py | 16 ----- tests/test_story.py | 6 -- tests/test_storyarea.py | 55 --------------- tests/test_suggestedpost.py | 67 ------------------- tests/test_switchinlinequerychosenchat.py | 7 -- tests/test_telegramobject.py | 7 -- tests/test_uniquegift.py | 50 -------------- tests/test_update.py | 7 -- tests/test_user.py | 6 -- tests/test_userprofileaudios.py | 7 -- tests/test_userprofilephotos.py | 7 -- tests/test_userrating.py | 6 -- tests/test_videochat.py | 25 ------- tests/test_webappdata.py | 6 -- tests/test_webappinfo.py | 6 -- tests/test_webhookinfo.py | 6 -- 150 files changed, 10 insertions(+), 1714 deletions(-) diff --git a/tests/_files/test_animation.py b/tests/_files/test_animation.py index df4ae468949..a1dcf373e72 100644 --- a/tests/_files/test_animation.py +++ b/tests/_files/test_animation.py @@ -36,7 +36,6 @@ ) from tests.auxil.build_messages import make_message from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots class AnimationTestBase: @@ -55,11 +54,6 @@ class AnimationTestBase: class TestAnimationWithoutRequest(AnimationTestBase): - def test_slot_behaviour(self, animation): - for attr in animation.__slots__: - assert getattr(animation, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(animation)) == len(set(mro_slots(animation))), "duplicate slot" - def test_creation(self, animation): assert isinstance(animation, Animation) assert isinstance(animation.file_id, str) diff --git a/tests/_files/test_audio.py b/tests/_files/test_audio.py index dd832966fde..86c3405c90d 100644 --- a/tests/_files/test_audio.py +++ b/tests/_files/test_audio.py @@ -36,7 +36,6 @@ ) from tests.auxil.build_messages import make_message from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots class AudioTestBase: @@ -58,11 +57,6 @@ class AudioTestBase: class TestAudioWithoutRequest(AudioTestBase): - def test_slot_behaviour(self, audio): - for attr in audio.__slots__: - assert getattr(audio, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(audio)) == len(set(mro_slots(audio))), "duplicate slot" - def test_creation(self, audio): # Make sure file has been uploaded. assert isinstance(audio, Audio) diff --git a/tests/_files/test_chatphoto.py b/tests/_files/test_chatphoto.py index 651d2ced060..a4bf43380e0 100644 --- a/tests/_files/test_chatphoto.py +++ b/tests/_files/test_chatphoto.py @@ -33,7 +33,6 @@ ) from tests.auxil.files import data_file from tests.auxil.networking import expect_bad_request -from tests.auxil.slots import mro_slots @pytest.fixture @@ -61,11 +60,6 @@ class ChatPhotoTestBase: class TestChatPhotoWithoutRequest(ChatPhotoTestBase): - def test_slot_behaviour(self, chat_photo): - for attr in chat_photo.__slots__: - assert getattr(chat_photo, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(chat_photo)) == len(set(mro_slots(chat_photo))), "duplicate slot" - def test_de_json(self, offline_bot, chat_photo): json_dict = { "small_file_id": self.chatphoto_small_file_id, diff --git a/tests/_files/test_contact.py b/tests/_files/test_contact.py index b7e282d0271..a6dce80a7b9 100644 --- a/tests/_files/test_contact.py +++ b/tests/_files/test_contact.py @@ -26,7 +26,6 @@ from telegram.error import BadRequest from telegram.request import RequestData from tests.auxil.build_messages import make_message -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -47,11 +46,6 @@ class ContactTestBase: class TestContactWithoutRequest(ContactTestBase): - def test_slot_behaviour(self, contact): - for attr in contact.__slots__: - assert getattr(contact, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(contact)) == len(set(mro_slots(contact))), "duplicate slot" - def test_de_json_required(self, offline_bot): json_dict = {"phone_number": self.phone_number, "first_name": self.first_name} contact = Contact.de_json(json_dict, offline_bot) diff --git a/tests/_files/test_document.py b/tests/_files/test_document.py index 224e05aa2fa..27bdb1917a6 100644 --- a/tests/_files/test_document.py +++ b/tests/_files/test_document.py @@ -34,7 +34,6 @@ ) from tests.auxil.build_messages import make_message from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots class DocumentTestBase: @@ -51,11 +50,6 @@ class DocumentTestBase: class TestDocumentWithoutRequest(DocumentTestBase): - def test_slot_behaviour(self, document): - for attr in document.__slots__: - assert getattr(document, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(document)) == len(set(mro_slots(document))), "duplicate slot" - def test_creation(self, document): assert isinstance(document, Document) assert isinstance(document.file_id, str) diff --git a/tests/_files/test_file.py b/tests/_files/test_file.py index f1091f6cd9d..1ecdff98f16 100644 --- a/tests/_files/test_file.py +++ b/tests/_files/test_file.py @@ -26,7 +26,6 @@ from telegram import File, FileCredentials, Voice from telegram.error import TelegramError from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -103,11 +102,6 @@ class FileTestBase: class TestFileWithoutRequest(FileTestBase): - def test_slot_behaviour(self, file): - for attr in file.__slots__: - assert getattr(file, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(file)) == len(set(mro_slots(file))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "file_id": self.file_id, diff --git a/tests/_files/test_inputprofilephoto.py b/tests/_files/test_inputprofilephoto.py index 0eddcdf469d..10cffe691f3 100644 --- a/tests/_files/test_inputprofilephoto.py +++ b/tests/_files/test_inputprofilephoto.py @@ -28,7 +28,6 @@ ) from telegram.constants import InputProfilePhotoType from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots class TestInputProfilePhotoWithoutRequest: @@ -57,12 +56,6 @@ class InputProfilePhotoStaticTestBase: class TestInputProfilePhotoStaticWithoutRequest(InputProfilePhotoStaticTestBase): - def test_slot_behaviour(self, input_profile_photo_static): - inst = input_profile_photo_static - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_profile_photo_static): inst = input_profile_photo_static assert inst.type == self.type_ @@ -97,12 +90,6 @@ class InputProfilePhotoAnimatedTestBase: class TestInputProfilePhotoAnimatedWithoutRequest(InputProfilePhotoAnimatedTestBase): - def test_slot_behaviour(self, input_profile_photo_animated): - inst = input_profile_photo_animated - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_profile_photo_animated): inst = input_profile_photo_animated assert inst.type == self.type_ diff --git a/tests/_files/test_inputsticker.py b/tests/_files/test_inputsticker.py index 43af66f356c..7a9ea99ff9a 100644 --- a/tests/_files/test_inputsticker.py +++ b/tests/_files/test_inputsticker.py @@ -22,7 +22,6 @@ from telegram import InputSticker, MaskPosition from telegram._files.inputfile import InputFile from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -45,12 +44,6 @@ class InputStickerTestBase: class TestInputStickerWithoutRequest(InputStickerTestBase): - def test_slot_behaviour(self, input_sticker): - inst = input_sticker - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_sticker): assert input_sticker.sticker == self.sticker assert isinstance(input_sticker.sticker, str) diff --git a/tests/_files/test_inputstorycontent.py b/tests/_files/test_inputstorycontent.py index 7c923071b90..e2e649a763e 100644 --- a/tests/_files/test_inputstorycontent.py +++ b/tests/_files/test_inputstorycontent.py @@ -24,7 +24,6 @@ from telegram import InputFile, InputStoryContent, InputStoryContentPhoto, InputStoryContentVideo from telegram.constants import InputStoryContentType from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -39,12 +38,6 @@ class InputStoryContentTestBase: class TestInputStoryContent(InputStoryContentTestBase): - def test_slot_behaviour(self, input_story_content): - inst = input_story_content - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self): assert type(InputStoryContent(type="video").type) is InputStoryContentType assert InputStoryContent(type="unknown").type == "unknown" @@ -61,12 +54,6 @@ class InputStoryContentPhotoTestBase: class TestInputStoryContentPhotoWithoutRequest(InputStoryContentPhotoTestBase): - def test_slot_behaviour(self, input_story_content_photo): - inst = input_story_content_photo - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_story_content_photo): inst = input_story_content_photo assert inst.type is self.type @@ -107,12 +94,6 @@ class InputStoryContentVideoTestBase: class TestInputStoryContentVideoWithoutRequest(InputStoryContentVideoTestBase): - def test_slot_behaviour(self, input_story_content_video): - inst = input_story_content_video - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_story_content_video): inst = input_story_content_video assert inst.type is self.type diff --git a/tests/_files/test_livephoto.py b/tests/_files/test_livephoto.py index 0dba659033e..f937ed1d602 100644 --- a/tests/_files/test_livephoto.py +++ b/tests/_files/test_livephoto.py @@ -39,7 +39,6 @@ from telegram.request import RequestData from tests.auxil.build_messages import make_message from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -69,11 +68,6 @@ class LivePhotoTestBase: class TestLivePhotoWithoutRequest(LivePhotoTestBase): - def test_slot_behaviour(self, live_photo): - for attr in live_photo.__slots__: - assert getattr(live_photo, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(live_photo)) == len(set(mro_slots(live_photo))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "file_id": self.file_id, diff --git a/tests/_files/test_location.py b/tests/_files/test_location.py index 0deaea2e2f5..2fa747741fc 100644 --- a/tests/_files/test_location.py +++ b/tests/_files/test_location.py @@ -27,7 +27,6 @@ from telegram.request import RequestData from telegram.warnings import PTBDeprecationWarning from tests.auxil.build_messages import make_message -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -52,11 +51,6 @@ class LocationTestBase: class TestLocationWithoutRequest(LocationTestBase): - def test_slot_behaviour(self, location): - for attr in location.__slots__: - assert getattr(location, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(location)) == len(set(mro_slots(location))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "latitude": self.latitude, diff --git a/tests/_files/test_photo.py b/tests/_files/test_photo.py index 93e0ac40ea1..0de2040d176 100644 --- a/tests/_files/test_photo.py +++ b/tests/_files/test_photo.py @@ -34,7 +34,6 @@ ) from tests.auxil.build_messages import make_message from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots class PhotoTestBase: @@ -48,11 +47,6 @@ class PhotoTestBase: class TestPhotoWithoutRequest(PhotoTestBase): - def test_slot_behaviour(self, photo): - for attr in photo.__slots__: - assert getattr(photo, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(photo)) == len(set(mro_slots(photo))), "duplicate slot" - def test_creation(self, thumb, photo): # Make sure file has been uploaded. assert isinstance(photo, PhotoSize) diff --git a/tests/_files/test_sticker.py b/tests/_files/test_sticker.py index d76ce821e0f..39fdcd37a0a 100644 --- a/tests/_files/test_sticker.py +++ b/tests/_files/test_sticker.py @@ -46,7 +46,6 @@ ) from tests.auxil.build_messages import make_message from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots class StickerTestBase: @@ -101,11 +100,6 @@ class StickerTestBase: class TestStickerWithoutRequest(StickerTestBase): - def test_slot_behaviour(self, sticker): - for attr in sticker.__slots__: - assert getattr(sticker, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(sticker)) == len(set(mro_slots(sticker))), "duplicate slot" - def test_creation(self, sticker): # Make sure file has been uploaded. assert isinstance(sticker, Sticker) @@ -514,12 +508,6 @@ class StickerSetTestBase: class TestStickerSetWithoutRequest(StickerSetTestBase): - def test_slot_behaviour(self): - inst = StickerSet("this", "is", self.stickers, "not") - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = self.sticker_set.to_dict() json_dict["contains_masks"] = self.contains_masks @@ -1032,12 +1020,6 @@ def mask_position(): class TestMaskPositionWithoutRequest(MaskPositionTestBase): - def test_slot_behaviour(self, mask_position): - inst = mask_position - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_mask_position_de_json(self, offline_bot): json_dict = { "point": self.point, diff --git a/tests/_files/test_venue.py b/tests/_files/test_venue.py index 894cc97bfa3..b25076aee2b 100644 --- a/tests/_files/test_venue.py +++ b/tests/_files/test_venue.py @@ -25,7 +25,6 @@ from telegram.error import BadRequest from telegram.request import RequestData from tests.auxil.build_messages import make_message -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -52,11 +51,6 @@ class VenueTestBase: class TestVenueWithoutRequest(VenueTestBase): - def test_slot_behaviour(self, venue): - for attr in venue.__slots__: - assert getattr(venue, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(venue)) == len(set(mro_slots(venue))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "location": self.location.to_dict(), diff --git a/tests/_files/test_video.py b/tests/_files/test_video.py index 4a34e126668..f4389a8ced8 100644 --- a/tests/_files/test_video.py +++ b/tests/_files/test_video.py @@ -45,7 +45,6 @@ ) from tests.auxil.build_messages import make_message from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots # Override `video` fixture to provide start_timestamp @@ -83,11 +82,6 @@ class VideoTestBase: class TestVideoWithoutRequest(VideoTestBase): - def test_slot_behaviour(self, video): - for attr in video.__slots__: - assert getattr(video, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(video)) == len(set(mro_slots(video))), "duplicate slot" - def test_creation(self, video): # Make sure file has been uploaded. assert isinstance(video, Video) diff --git a/tests/_files/test_videonote.py b/tests/_files/test_videonote.py index c2993021883..a5d827f84a1 100644 --- a/tests/_files/test_videonote.py +++ b/tests/_files/test_videonote.py @@ -35,7 +35,6 @@ ) from tests.auxil.build_messages import make_message from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots @pytest.fixture @@ -63,11 +62,6 @@ class VideoNoteTestBase: class TestVideoNoteWithoutRequest(VideoNoteTestBase): - def test_slot_behaviour(self, video_note): - for attr in video_note.__slots__: - assert getattr(video_note, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(video_note)) == len(set(mro_slots(video_note))), "duplicate slot" - def test_creation(self, video_note): # Make sure file has been uploaded. assert isinstance(video_note, VideoNote) diff --git a/tests/_files/test_videoquality.py b/tests/_files/test_videoquality.py index 766e12f800f..b430f28e8b1 100644 --- a/tests/_files/test_videoquality.py +++ b/tests/_files/test_videoquality.py @@ -18,7 +18,6 @@ import pytest from telegram import PhotoSize, VideoQuality -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -56,13 +55,6 @@ def test_qualities_available(self, video_quality_list): # having exactly one video quality. assert len(video_quality_list) == 1 - def test_slot_behaviour(self, video_quality): - for attr in video_quality.__slots__: - assert getattr(video_quality, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(video_quality)) == len(set(mro_slots(video_quality))), ( - "duplicate slot" - ) - def test_creation(self, video_quality): assert isinstance(video_quality, VideoQuality) assert isinstance(video_quality.file_id, str) diff --git a/tests/_files/test_voice.py b/tests/_files/test_voice.py index e1e47759db9..8e195a15568 100644 --- a/tests/_files/test_voice.py +++ b/tests/_files/test_voice.py @@ -36,7 +36,6 @@ ) from tests.auxil.build_messages import make_message from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots @pytest.fixture @@ -62,11 +61,6 @@ class VoiceTestBase: class TestVoiceWithoutRequest(VoiceTestBase): - def test_slot_behaviour(self, voice): - for attr in voice.__slots__: - assert getattr(voice, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(voice)) == len(set(mro_slots(voice))), "duplicate slot" - async def test_creation(self, voice): # Make sure file has been uploaded. assert isinstance(voice, Voice) diff --git a/tests/_games/test_game.py b/tests/_games/test_game.py index 4db32d4c9aa..de08d9969c3 100644 --- a/tests/_games/test_game.py +++ b/tests/_games/test_game.py @@ -20,7 +20,6 @@ import pytest from telegram import Animation, Game, MessageEntity, PhotoSize -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -50,11 +49,6 @@ class GameTestBase: class TestGameWithoutRequest(GameTestBase): - def test_slot_behaviour(self, game): - for attr in game.__slots__: - assert getattr(game, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(game)) == len(set(mro_slots(game))), "duplicate slot" - def test_de_json_required(self, offline_bot): json_dict = { "title": self.title, diff --git a/tests/_games/test_gamehighscore.py b/tests/_games/test_gamehighscore.py index d0d6008b449..3bb4ed223a0 100644 --- a/tests/_games/test_gamehighscore.py +++ b/tests/_games/test_gamehighscore.py @@ -20,7 +20,6 @@ import pytest from telegram import GameHighScore, User -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -37,11 +36,6 @@ class GameHighScoreTestBase: class TestGameHighScoreWithoutRequest(GameHighScoreTestBase): - def test_slot_behaviour(self, game_highscore): - for attr in game_highscore.__slots__: - assert getattr(game_highscore, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(game_highscore)) == len(set(mro_slots(game_highscore))), "same slot" - def test_de_json(self, offline_bot): json_dict = { "position": self.position, diff --git a/tests/_inline/test_inlinekeyboardbutton.py b/tests/_inline/test_inlinekeyboardbutton.py index 1651d73582a..f922b646a58 100644 --- a/tests/_inline/test_inlinekeyboardbutton.py +++ b/tests/_inline/test_inlinekeyboardbutton.py @@ -27,7 +27,6 @@ SwitchInlineQueryChosenChat, WebAppInfo, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -70,12 +69,6 @@ class InlineKeyboardButtonTestBase: class TestInlineKeyboardButtonWithoutRequest(InlineKeyboardButtonTestBase): - def test_slot_behaviour(self, inline_keyboard_button): - inst = inline_keyboard_button - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_keyboard_button): assert inline_keyboard_button.text == self.text assert inline_keyboard_button.url == self.url diff --git a/tests/_inline/test_inlinekeyboardmarkup.py b/tests/_inline/test_inlinekeyboardmarkup.py index eca0e7e4508..c752763ac67 100644 --- a/tests/_inline/test_inlinekeyboardmarkup.py +++ b/tests/_inline/test_inlinekeyboardmarkup.py @@ -27,7 +27,6 @@ ReplyKeyboardRemove, ) from telegram.constants import KeyboardButtonStyle -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -45,12 +44,6 @@ class InlineKeyboardMarkupTestBase: class TestInlineKeyboardMarkupWithoutRequest(InlineKeyboardMarkupTestBase): - def test_slot_behaviour(self, inline_keyboard_markup): - inst = inline_keyboard_markup - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_to_dict(self, inline_keyboard_markup): inline_keyboard_markup_dict = inline_keyboard_markup.to_dict() diff --git a/tests/_inline/test_inlinequery.py b/tests/_inline/test_inlinequery.py index 9e16035117b..2c529410f2a 100644 --- a/tests/_inline/test_inlinequery.py +++ b/tests/_inline/test_inlinequery.py @@ -25,7 +25,6 @@ check_shortcut_call, check_shortcut_signature, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -50,11 +49,6 @@ class InlineQueryTestBase: class TestInlineQueryWithoutRequest(InlineQueryTestBase): - def test_slot_behaviour(self, inline_query): - for attr in inline_query.__slots__: - assert getattr(inline_query, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inline_query)) == len(set(mro_slots(inline_query))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "id": self.id_, diff --git a/tests/_inline/test_inlinequeryresultarticle.py b/tests/_inline/test_inlinequeryresultarticle.py index 9d99e4a52c0..30270e47d3f 100644 --- a/tests/_inline/test_inlinequeryresultarticle.py +++ b/tests/_inline/test_inlinequeryresultarticle.py @@ -28,7 +28,6 @@ InputTextMessageContent, ) from telegram.constants import InlineQueryResultType -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -60,12 +59,6 @@ class InlineQueryResultArticleTestBase: class TestInlineQueryResultArticleWithoutRequest(InlineQueryResultArticleTestBase): - def test_slot_behaviour(self, inline_query_result_article): - inst = inline_query_result_article - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_article): assert inline_query_result_article.type == self.type_ assert inline_query_result_article.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultaudio.py b/tests/_inline/test_inlinequeryresultaudio.py index 896d1f263bd..de68cd7dea1 100644 --- a/tests/_inline/test_inlinequeryresultaudio.py +++ b/tests/_inline/test_inlinequeryresultaudio.py @@ -30,7 +30,6 @@ MessageEntity, ) from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -64,12 +63,6 @@ class InlineQueryResultAudioTestBase: class TestInlineQueryResultAudioWithoutRequest(InlineQueryResultAudioTestBase): - def test_slot_behaviour(self, inline_query_result_audio): - inst = inline_query_result_audio - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_audio): assert inline_query_result_audio.type == self.type_ assert inline_query_result_audio.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultcachedaudio.py b/tests/_inline/test_inlinequeryresultcachedaudio.py index 6bbbfac4abb..49ecb4817a2 100644 --- a/tests/_inline/test_inlinequeryresultcachedaudio.py +++ b/tests/_inline/test_inlinequeryresultcachedaudio.py @@ -27,7 +27,6 @@ InputTextMessageContent, MessageEntity, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -55,12 +54,6 @@ class InlineQueryResultCachedAudioTestBase: class TestInlineQueryResultCachedAudioWithoutRequest(InlineQueryResultCachedAudioTestBase): - def test_slot_behaviour(self, inline_query_result_cached_audio): - inst = inline_query_result_cached_audio - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_cached_audio): assert inline_query_result_cached_audio.type == self.type_ assert inline_query_result_cached_audio.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultcacheddocument.py b/tests/_inline/test_inlinequeryresultcacheddocument.py index f9a7fd59233..540fb7ad34a 100644 --- a/tests/_inline/test_inlinequeryresultcacheddocument.py +++ b/tests/_inline/test_inlinequeryresultcacheddocument.py @@ -27,7 +27,6 @@ InputTextMessageContent, MessageEntity, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -59,12 +58,6 @@ class InlineQueryResultCachedDocumentTestBase: class TestInlineQueryResultCachedDocumentWithoutRequest(InlineQueryResultCachedDocumentTestBase): - def test_slot_behaviour(self, inline_query_result_cached_document): - inst = inline_query_result_cached_document - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_cached_document): assert inline_query_result_cached_document.id == self.id_ assert inline_query_result_cached_document.type == self.type_ diff --git a/tests/_inline/test_inlinequeryresultcachedgif.py b/tests/_inline/test_inlinequeryresultcachedgif.py index 481b100ae90..cab40a08c0c 100644 --- a/tests/_inline/test_inlinequeryresultcachedgif.py +++ b/tests/_inline/test_inlinequeryresultcachedgif.py @@ -26,7 +26,6 @@ InputTextMessageContent, MessageEntity, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -58,12 +57,6 @@ class InlineQueryResultCachedGifTestBase: class TestInlineQueryResultCachedGifWithoutRequest(InlineQueryResultCachedGifTestBase): - def test_slot_behaviour(self, inline_query_result_cached_gif): - inst = inline_query_result_cached_gif - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_cached_gif): assert inline_query_result_cached_gif.type == self.type_ assert inline_query_result_cached_gif.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultcachedmpeg4gif.py b/tests/_inline/test_inlinequeryresultcachedmpeg4gif.py index b3712bc6eaf..d8db14adcd9 100644 --- a/tests/_inline/test_inlinequeryresultcachedmpeg4gif.py +++ b/tests/_inline/test_inlinequeryresultcachedmpeg4gif.py @@ -26,7 +26,6 @@ InputTextMessageContent, MessageEntity, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -58,12 +57,6 @@ class InlineQueryResultCachedMpeg4GifTestBase: class TestInlineQueryResultCachedMpeg4GifWithoutRequest(InlineQueryResultCachedMpeg4GifTestBase): - def test_slot_behaviour(self, inline_query_result_cached_mpeg4_gif): - inst = inline_query_result_cached_mpeg4_gif - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_cached_mpeg4_gif): assert inline_query_result_cached_mpeg4_gif.type == self.type_ assert inline_query_result_cached_mpeg4_gif.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultcachedphoto.py b/tests/_inline/test_inlinequeryresultcachedphoto.py index 0433a6b6cc6..6aec33fade8 100644 --- a/tests/_inline/test_inlinequeryresultcachedphoto.py +++ b/tests/_inline/test_inlinequeryresultcachedphoto.py @@ -26,7 +26,6 @@ InputTextMessageContent, MessageEntity, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -60,12 +59,6 @@ class InlineQueryResultCachedPhotoTestBase: class TestInlineQueryResultCachedPhotoWithoutRequest(InlineQueryResultCachedPhotoTestBase): - def test_slot_behaviour(self, inline_query_result_cached_photo): - inst = inline_query_result_cached_photo - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_cached_photo): assert inline_query_result_cached_photo.type == self.type_ assert inline_query_result_cached_photo.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultcachedsticker.py b/tests/_inline/test_inlinequeryresultcachedsticker.py index 062db4f4d00..8e3933150e7 100644 --- a/tests/_inline/test_inlinequeryresultcachedsticker.py +++ b/tests/_inline/test_inlinequeryresultcachedsticker.py @@ -25,7 +25,6 @@ InlineQueryResultCachedVoice, InputTextMessageContent, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -47,12 +46,6 @@ class InlineQueryResultCachedStickerTestBase: class TestInlineQueryResultCachedStickerWithoutRequest(InlineQueryResultCachedStickerTestBase): - def test_slot_behaviour(self, inline_query_result_cached_sticker): - inst = inline_query_result_cached_sticker - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_cached_sticker): assert inline_query_result_cached_sticker.type == self.type_ assert inline_query_result_cached_sticker.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultcachedvideo.py b/tests/_inline/test_inlinequeryresultcachedvideo.py index 3b1b7dfbef3..d14577df5d3 100644 --- a/tests/_inline/test_inlinequeryresultcachedvideo.py +++ b/tests/_inline/test_inlinequeryresultcachedvideo.py @@ -26,7 +26,6 @@ InputTextMessageContent, MessageEntity, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -60,12 +59,6 @@ class InlineQueryResultCachedVideoTestBase: class TestInlineQueryResultCachedVideoWithoutRequest(InlineQueryResultCachedVideoTestBase): - def test_slot_behaviour(self, inline_query_result_cached_video): - inst = inline_query_result_cached_video - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_cached_video): assert inline_query_result_cached_video.type == self.type_ assert inline_query_result_cached_video.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultcachedvoice.py b/tests/_inline/test_inlinequeryresultcachedvoice.py index 4fe608aa047..74bafc9330d 100644 --- a/tests/_inline/test_inlinequeryresultcachedvoice.py +++ b/tests/_inline/test_inlinequeryresultcachedvoice.py @@ -26,7 +26,6 @@ InputTextMessageContent, MessageEntity, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -56,12 +55,6 @@ class InlineQueryResultCachedVoiceTestBase: class TestInlineQueryResultCachedVoiceWithoutRequest(InlineQueryResultCachedVoiceTestBase): - def test_slot_behaviour(self, inline_query_result_cached_voice): - inst = inline_query_result_cached_voice - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_cached_voice): assert inline_query_result_cached_voice.type == self.type_ assert inline_query_result_cached_voice.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultcontact.py b/tests/_inline/test_inlinequeryresultcontact.py index 3fee14f7a17..d2af9f547f4 100644 --- a/tests/_inline/test_inlinequeryresultcontact.py +++ b/tests/_inline/test_inlinequeryresultcontact.py @@ -25,7 +25,6 @@ InlineQueryResultVoice, InputTextMessageContent, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -57,12 +56,6 @@ class InlineQueryResultContactTestBase: class TestInlineQueryResultContactWithoutRequest(InlineQueryResultContactTestBase): - def test_slot_behaviour(self, inline_query_result_contact): - inst = inline_query_result_contact - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_contact): assert inline_query_result_contact.id == self.id_ assert inline_query_result_contact.type == self.type_ diff --git a/tests/_inline/test_inlinequeryresultdocument.py b/tests/_inline/test_inlinequeryresultdocument.py index 326e330b36d..83bb2a85d6c 100644 --- a/tests/_inline/test_inlinequeryresultdocument.py +++ b/tests/_inline/test_inlinequeryresultdocument.py @@ -26,7 +26,6 @@ InputTextMessageContent, MessageEntity, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -66,12 +65,6 @@ class InlineQueryResultDocumentTestBase: class TestInlineQueryResultDocumentWithoutRequest(InlineQueryResultDocumentTestBase): - def test_slot_behaviour(self, inline_query_result_document): - inst = inline_query_result_document - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_document): assert inline_query_result_document.id == self.id_ assert inline_query_result_document.type == self.type_ diff --git a/tests/_inline/test_inlinequeryresultgame.py b/tests/_inline/test_inlinequeryresultgame.py index 15c783a3fed..f9093c209db 100644 --- a/tests/_inline/test_inlinequeryresultgame.py +++ b/tests/_inline/test_inlinequeryresultgame.py @@ -24,7 +24,6 @@ InlineQueryResultGame, InlineQueryResultVoice, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -44,12 +43,6 @@ class InlineQueryResultGameTestBase: class TestInlineQueryResultGameWithoutRequest(InlineQueryResultGameTestBase): - def test_slot_behaviour(self, inline_query_result_game): - inst = inline_query_result_game - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_game): assert inline_query_result_game.type == self.type_ assert inline_query_result_game.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultgif.py b/tests/_inline/test_inlinequeryresultgif.py index 9b25286eb0f..c39c70b8e61 100644 --- a/tests/_inline/test_inlinequeryresultgif.py +++ b/tests/_inline/test_inlinequeryresultgif.py @@ -29,7 +29,6 @@ MessageEntity, ) from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -71,12 +70,6 @@ class InlineQueryResultGifTestBase: class TestInlineQueryResultGifWithoutRequest(InlineQueryResultGifTestBase): - def test_slot_behaviour(self, inline_query_result_gif): - inst = inline_query_result_gif - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_caption_entities_always_tuple(self): result = InlineQueryResultGif(self.id_, self.gif_url, self.thumbnail_url) assert result.caption_entities == () diff --git a/tests/_inline/test_inlinequeryresultlocation.py b/tests/_inline/test_inlinequeryresultlocation.py index 183e1818268..e88c75b7766 100644 --- a/tests/_inline/test_inlinequeryresultlocation.py +++ b/tests/_inline/test_inlinequeryresultlocation.py @@ -28,7 +28,6 @@ InputTextMessageContent, ) from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -68,12 +67,6 @@ class InlineQueryResultLocationTestBase: class TestInlineQueryResultLocationWithoutRequest(InlineQueryResultLocationTestBase): - def test_slot_behaviour(self, inline_query_result_location): - inst = inline_query_result_location - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_location): assert inline_query_result_location.id == self.id_ assert inline_query_result_location.type == self.type_ diff --git a/tests/_inline/test_inlinequeryresultmpeg4gif.py b/tests/_inline/test_inlinequeryresultmpeg4gif.py index 7b1fa84e5d1..640ebe4d736 100644 --- a/tests/_inline/test_inlinequeryresultmpeg4gif.py +++ b/tests/_inline/test_inlinequeryresultmpeg4gif.py @@ -29,7 +29,6 @@ MessageEntity, ) from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -71,12 +70,6 @@ class InlineQueryResultMpeg4GifTestBase: class TestInlineQueryResultMpeg4GifWithoutRequest(InlineQueryResultMpeg4GifTestBase): - def test_slot_behaviour(self, inline_query_result_mpeg4_gif): - inst = inline_query_result_mpeg4_gif - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_mpeg4_gif): assert inline_query_result_mpeg4_gif.type == self.type_ assert inline_query_result_mpeg4_gif.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultphoto.py b/tests/_inline/test_inlinequeryresultphoto.py index 3f4d0f12b56..fbe8a258db7 100644 --- a/tests/_inline/test_inlinequeryresultphoto.py +++ b/tests/_inline/test_inlinequeryresultphoto.py @@ -26,7 +26,6 @@ InputTextMessageContent, MessageEntity, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -67,12 +66,6 @@ class InlineQueryResultPhotoTestBase: class TestInlineQueryResultPhotoWithoutRequest(InlineQueryResultPhotoTestBase): - def test_slot_behaviour(self, inline_query_result_photo): - inst = inline_query_result_photo - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_photo): assert inline_query_result_photo.type == self.type_ assert inline_query_result_photo.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultsbutton.py b/tests/_inline/test_inlinequeryresultsbutton.py index fbc92c3649e..0439e90241b 100644 --- a/tests/_inline/test_inlinequeryresultsbutton.py +++ b/tests/_inline/test_inlinequeryresultsbutton.py @@ -19,7 +19,6 @@ import pytest from telegram import InlineQueryResultsButton, WebAppInfo -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -38,12 +37,6 @@ class InlineQueryResultsButtonTestBase: class TestInlineQueryResultsButtonWithoutRequest(InlineQueryResultsButtonTestBase): - def test_slot_behaviour(self, inline_query_results_button): - inst = inline_query_results_button - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_to_dict(self, inline_query_results_button): inline_query_results_button_dict = inline_query_results_button.to_dict() assert isinstance(inline_query_results_button_dict, dict) diff --git a/tests/_inline/test_inlinequeryresultvenue.py b/tests/_inline/test_inlinequeryresultvenue.py index f6bc75c0108..25b9a4fcf17 100644 --- a/tests/_inline/test_inlinequeryresultvenue.py +++ b/tests/_inline/test_inlinequeryresultvenue.py @@ -25,7 +25,6 @@ InlineQueryResultVoice, InputTextMessageContent, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -67,12 +66,6 @@ class InlineQueryResultVenueTestBase: class TestInlineQueryResultVenueWithoutRequest(InlineQueryResultVenueTestBase): - def test_slot_behaviour(self, inline_query_result_venue): - inst = inline_query_result_venue - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_venue): assert inline_query_result_venue.id == self.id_ assert inline_query_result_venue.type == self.type_ diff --git a/tests/_inline/test_inlinequeryresultvideo.py b/tests/_inline/test_inlinequeryresultvideo.py index 9d918f91626..f43a855e1f4 100644 --- a/tests/_inline/test_inlinequeryresultvideo.py +++ b/tests/_inline/test_inlinequeryresultvideo.py @@ -29,7 +29,6 @@ MessageEntity, ) from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -73,12 +72,6 @@ class InlineQueryResultVideoTestBase: class TestInlineQueryResultVideoWithoutRequest(InlineQueryResultVideoTestBase): - def test_slot_behaviour(self, inline_query_result_video): - inst = inline_query_result_video - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_video): assert inline_query_result_video.type == self.type_ assert inline_query_result_video.id == self.id_ diff --git a/tests/_inline/test_inlinequeryresultvoice.py b/tests/_inline/test_inlinequeryresultvoice.py index ccc9db85494..27ce3092f7f 100644 --- a/tests/_inline/test_inlinequeryresultvoice.py +++ b/tests/_inline/test_inlinequeryresultvoice.py @@ -29,7 +29,6 @@ MessageEntity, ) from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -61,12 +60,6 @@ class InlineQueryResultVoiceTestBase: class TestInlineQueryResultVoiceWithoutRequest(InlineQueryResultVoiceTestBase): - def test_slot_behaviour(self, inline_query_result_voice): - inst = inline_query_result_voice - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, inline_query_result_voice): assert inline_query_result_voice.type == self.type_ assert inline_query_result_voice.id == self.id_ diff --git a/tests/_inline/test_inputcontactmessagecontent.py b/tests/_inline/test_inputcontactmessagecontent.py index e70b87c4d07..0ecbddd6990 100644 --- a/tests/_inline/test_inputcontactmessagecontent.py +++ b/tests/_inline/test_inputcontactmessagecontent.py @@ -19,7 +19,6 @@ import pytest from telegram import InputContactMessageContent, User -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -38,12 +37,6 @@ class InputContactMessageContentTestBase: class TestInputContactMessageContentWithoutRequest(InputContactMessageContentTestBase): - def test_slot_behaviour(self, input_contact_message_content): - inst = input_contact_message_content - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_contact_message_content): assert input_contact_message_content.first_name == self.first_name assert input_contact_message_content.phone_number == self.phone_number diff --git a/tests/_inline/test_inputinvoicemessagecontent.py b/tests/_inline/test_inputinvoicemessagecontent.py index 4e0dc9acae3..fa6bdfc0037 100644 --- a/tests/_inline/test_inputinvoicemessagecontent.py +++ b/tests/_inline/test_inputinvoicemessagecontent.py @@ -20,7 +20,6 @@ import pytest from telegram import InputInvoiceMessageContent, InputTextMessageContent, LabeledPrice -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -75,12 +74,6 @@ class InputInvoiceMessageContentTestBase: class TestInputInvoiceMessageContentWithoutRequest(InputInvoiceMessageContentTestBase): - def test_slot_behaviour(self, input_invoice_message_content): - inst = input_invoice_message_content - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_invoice_message_content): assert input_invoice_message_content.title == self.title assert input_invoice_message_content.description == self.description diff --git a/tests/_inline/test_inputlocationmessagecontent.py b/tests/_inline/test_inputlocationmessagecontent.py index f5cc16c1386..dda616b4371 100644 --- a/tests/_inline/test_inputlocationmessagecontent.py +++ b/tests/_inline/test_inputlocationmessagecontent.py @@ -22,7 +22,6 @@ from telegram import InputLocationMessageContent, Location from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -47,12 +46,6 @@ class InputLocationMessageContentTestBase: class TestInputLocationMessageContentWithoutRequest(InputLocationMessageContentTestBase): - def test_slot_behaviour(self, input_location_message_content): - inst = input_location_message_content - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_location_message_content): assert input_location_message_content.longitude == self.longitude assert input_location_message_content.latitude == self.latitude diff --git a/tests/_inline/test_inputtextmessagecontent.py b/tests/_inline/test_inputtextmessagecontent.py index da2132823ce..35581735a85 100644 --- a/tests/_inline/test_inputtextmessagecontent.py +++ b/tests/_inline/test_inputtextmessagecontent.py @@ -20,7 +20,6 @@ from telegram import InputTextMessageContent, LinkPreviewOptions, MessageEntity from telegram.constants import ParseMode -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -42,12 +41,6 @@ class InputTextMessageContentTestBase: class TestInputTextMessageContentWithoutRequest(InputTextMessageContentTestBase): - def test_slot_behaviour(self, input_text_message_content): - inst = input_text_message_content - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_text_message_content): assert input_text_message_content.parse_mode == self.parse_mode assert input_text_message_content.message_text == self.message_text diff --git a/tests/_inline/test_inputvenuemessagecontent.py b/tests/_inline/test_inputvenuemessagecontent.py index b55eb23e8af..d9063660cca 100644 --- a/tests/_inline/test_inputvenuemessagecontent.py +++ b/tests/_inline/test_inputvenuemessagecontent.py @@ -19,7 +19,6 @@ import pytest from telegram import InputVenueMessageContent, Location -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -48,12 +47,6 @@ class InputVenueMessageContentTestBase: class TestInputVenueMessageContentWithoutRequest(InputVenueMessageContentTestBase): - def test_slot_behaviour(self, input_venue_message_content): - inst = input_venue_message_content - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, input_venue_message_content): assert input_venue_message_content.longitude == self.longitude assert input_venue_message_content.latitude == self.latitude diff --git a/tests/_inline/test_preparedinlinemessage.py b/tests/_inline/test_preparedinlinemessage.py index c9f3deeb320..81e824326f2 100644 --- a/tests/_inline/test_preparedinlinemessage.py +++ b/tests/_inline/test_preparedinlinemessage.py @@ -22,7 +22,6 @@ from telegram import Location, PreparedInlineMessage from telegram._utils.datetime import UTC, to_timestamp -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -39,12 +38,6 @@ class PreparedInlineMessageTestBase: class TestPreparedInlineMessageWithoutRequest(PreparedInlineMessageTestBase): - def test_slot_behaviour(self, prepared_inline_message): - inst = prepared_inline_message - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, prepared_inline_message): assert prepared_inline_message.id == self.id assert prepared_inline_message.expiration_date == self.expiration_date diff --git a/tests/_passport/test_encryptedcredentials.py b/tests/_passport/test_encryptedcredentials.py index 154f8b35fea..a18bede4d5a 100644 --- a/tests/_passport/test_encryptedcredentials.py +++ b/tests/_passport/test_encryptedcredentials.py @@ -20,7 +20,6 @@ import pytest from telegram import EncryptedCredentials, PassportElementError -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -39,12 +38,6 @@ class EncryptedCredentialsTestBase: class TestEncryptedCredentialsWithoutRequest(EncryptedCredentialsTestBase): - def test_slot_behaviour(self, encrypted_credentials): - inst = encrypted_credentials - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, encrypted_credentials): assert encrypted_credentials.data == self.data assert encrypted_credentials.hash == self.hash diff --git a/tests/_passport/test_encryptedpassportelement.py b/tests/_passport/test_encryptedpassportelement.py index f1bab591b36..1e2304f44ca 100644 --- a/tests/_passport/test_encryptedpassportelement.py +++ b/tests/_passport/test_encryptedpassportelement.py @@ -20,7 +20,6 @@ import pytest from telegram import EncryptedPassportElement, PassportElementError, PassportFile -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -51,12 +50,6 @@ class EncryptedPassportElementTestBase: class TestEncryptedPassportElementWithoutRequest(EncryptedPassportElementTestBase): - def test_slot_behaviour(self, encrypted_passport_element): - inst = encrypted_passport_element - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, encrypted_passport_element): assert encrypted_passport_element.type == self.type_ assert encrypted_passport_element.hash == self.hash diff --git a/tests/_passport/test_passport.py b/tests/_passport/test_passport.py index 5b87074105c..1735a17d8cd 100644 --- a/tests/_passport/test_passport.py +++ b/tests/_passport/test_passport.py @@ -36,7 +36,6 @@ # worth it. from telegram.request import RequestData from tests.auxil.pytest_classes import make_bot -from tests.auxil.slots import mro_slots RAW_PASSPORT_DATA = { "credentials": { @@ -244,12 +243,6 @@ class PassportTestBase: class TestPassportWithoutRequest(PassportTestBase): - def test_slot_behaviour(self, passport_data): - inst = passport_data - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_creation(self, passport_data): assert isinstance(passport_data, PassportData) diff --git a/tests/_passport/test_passportelementerrordatafield.py b/tests/_passport/test_passportelementerrordatafield.py index d0e53bbb162..396f106d27c 100644 --- a/tests/_passport/test_passportelementerrordatafield.py +++ b/tests/_passport/test_passportelementerrordatafield.py @@ -41,6 +41,7 @@ class PassportElementErrorDataFieldTestBase: class TestPassportElementErrorDataFieldWithoutRequest(PassportElementErrorDataFieldTestBase): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, passport_element_error_data_field): inst = passport_element_error_data_field for attr in inst.__slots__: diff --git a/tests/_passport/test_passportelementerrorfile.py b/tests/_passport/test_passportelementerrorfile.py index 95309431c80..f666ff55824 100644 --- a/tests/_passport/test_passportelementerrorfile.py +++ b/tests/_passport/test_passportelementerrorfile.py @@ -39,6 +39,7 @@ class PassportElementErrorFileTestBase: class TestPassportElementErrorFileWithoutRequest(PassportElementErrorFileTestBase): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, passport_element_error_file): inst = passport_element_error_file for attr in inst.__slots__: diff --git a/tests/_passport/test_passportelementerrorfiles.py b/tests/_passport/test_passportelementerrorfiles.py index ea55875fbf7..8efbb40e1fb 100644 --- a/tests/_passport/test_passportelementerrorfiles.py +++ b/tests/_passport/test_passportelementerrorfiles.py @@ -39,6 +39,7 @@ class PassportElementErrorFilesTestBase: class TestPassportElementErrorFilesWithoutRequest(PassportElementErrorFilesTestBase): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, passport_element_error_files): inst = passport_element_error_files for attr in inst.__slots__: diff --git a/tests/_passport/test_passportelementerrorfrontside.py b/tests/_passport/test_passportelementerrorfrontside.py index a4f7e56aa3f..de842738c54 100644 --- a/tests/_passport/test_passportelementerrorfrontside.py +++ b/tests/_passport/test_passportelementerrorfrontside.py @@ -39,6 +39,7 @@ class PassportElementErrorFrontSideTestBase: class TestPassportElementErrorFrontSideWithoutRequest(PassportElementErrorFrontSideTestBase): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, passport_element_error_front_side): inst = passport_element_error_front_side for attr in inst.__slots__: diff --git a/tests/_passport/test_passportelementerrorreverseside.py b/tests/_passport/test_passportelementerrorreverseside.py index 97ce2b71efe..d1a4c6852e6 100644 --- a/tests/_passport/test_passportelementerrorreverseside.py +++ b/tests/_passport/test_passportelementerrorreverseside.py @@ -39,6 +39,7 @@ class PassportElementErrorReverseSideTestBase: class TestPassportElementErrorReverseSideWithoutRequest(PassportElementErrorReverseSideTestBase): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, passport_element_error_reverse_side): inst = passport_element_error_reverse_side for attr in inst.__slots__: diff --git a/tests/_passport/test_passportelementerrorselfie.py b/tests/_passport/test_passportelementerrorselfie.py index 5bc87b6a579..3924e4204dc 100644 --- a/tests/_passport/test_passportelementerrorselfie.py +++ b/tests/_passport/test_passportelementerrorselfie.py @@ -39,6 +39,7 @@ class PassportElementErrorSelfieTestBase: class TestPassportElementErrorSelfieWithoutRequest(PassportElementErrorSelfieTestBase): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, passport_element_error_selfie): inst = passport_element_error_selfie for attr in inst.__slots__: diff --git a/tests/_passport/test_passportelementerrortranslationfile.py b/tests/_passport/test_passportelementerrortranslationfile.py index f1ccdfb9fb8..42fb4a81d74 100644 --- a/tests/_passport/test_passportelementerrortranslationfile.py +++ b/tests/_passport/test_passportelementerrortranslationfile.py @@ -41,6 +41,7 @@ class PassportElementErrorTranslationFileTestBase: class TestPassportElementErrorTranslationFileWithoutRequest( PassportElementErrorTranslationFileTestBase ): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, passport_element_error_translation_file): inst = passport_element_error_translation_file for attr in inst.__slots__: diff --git a/tests/_passport/test_passportelementerrortranslationfiles.py b/tests/_passport/test_passportelementerrortranslationfiles.py index e2be406fb7e..4c245fa86c2 100644 --- a/tests/_passport/test_passportelementerrortranslationfiles.py +++ b/tests/_passport/test_passportelementerrortranslationfiles.py @@ -41,6 +41,7 @@ class PassportElementErrorTranslationFilesTestBase: class TestPassportElementErrorTranslationFilesWithoutRequest( PassportElementErrorTranslationFilesTestBase ): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, passport_element_error_translation_files): inst = passport_element_error_translation_files for attr in inst.__slots__: diff --git a/tests/_passport/test_passportelementerrorunspecified.py b/tests/_passport/test_passportelementerrorunspecified.py index f367df4d3da..b85b328a291 100644 --- a/tests/_passport/test_passportelementerrorunspecified.py +++ b/tests/_passport/test_passportelementerrorunspecified.py @@ -39,6 +39,7 @@ class PassportElementErrorUnspecifiedTestBase: class TestPassportElementErrorUnspecifiedWithoutRequest(PassportElementErrorUnspecifiedTestBase): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, passport_element_error_unspecified): inst = passport_element_error_unspecified for attr in inst.__slots__: diff --git a/tests/_passport/test_passportfile.py b/tests/_passport/test_passportfile.py index cd199c42c2a..e099cbcc2da 100644 --- a/tests/_passport/test_passportfile.py +++ b/tests/_passport/test_passportfile.py @@ -27,7 +27,6 @@ check_shortcut_call, check_shortcut_signature, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="class") @@ -50,12 +49,6 @@ class PassportFileTestBase: class TestPassportFileWithoutRequest(PassportFileTestBase): - def test_slot_behaviour(self, passport_file): - inst = passport_file - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, passport_file): assert passport_file.file_id == self.file_id assert passport_file.file_unique_id == self.file_unique_id diff --git a/tests/_payment/stars/test_affiliateinfo.py b/tests/_payment/stars/test_affiliateinfo.py index b6e72cc724f..a3503c04409 100644 --- a/tests/_payment/stars/test_affiliateinfo.py +++ b/tests/_payment/stars/test_affiliateinfo.py @@ -19,7 +19,6 @@ import pytest from telegram import AffiliateInfo, Chat, Dice, User -from tests.auxil.slots import mro_slots @pytest.fixture @@ -42,12 +41,6 @@ class AffiliateInfoTestBase: class TestAffiliateInfoWithoutRequest(AffiliateInfoTestBase): - def test_slot_behaviour(self, affiliate_info): - inst = affiliate_info - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "affiliate_user": self.affiliate_user.to_dict(), diff --git a/tests/_payment/stars/test_revenuewithdrawelstate.py b/tests/_payment/stars/test_revenuewithdrawelstate.py index da24129b68e..eba7b164a48 100644 --- a/tests/_payment/stars/test_revenuewithdrawelstate.py +++ b/tests/_payment/stars/test_revenuewithdrawelstate.py @@ -29,7 +29,6 @@ ) from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import RevenueWithdrawalStateType -from tests.auxil.slots import mro_slots @pytest.fixture @@ -61,12 +60,6 @@ class RevenueWithdrawalStateTestBase: class TestRevenueWithdrawalStateWithoutRequest(RevenueWithdrawalStateTestBase): - def test_slot_behaviour(self, revenue_withdrawal_state): - inst = revenue_withdrawal_state - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self): assert type(RevenueWithdrawalState("failed").type) is RevenueWithdrawalStateType assert RevenueWithdrawalState("unknown").type == "unknown" @@ -114,12 +107,6 @@ def test_equality(self, revenue_withdrawal_state): class TestRevenueWithdrawalStatePendingWithoutRequest(RevenueWithdrawalStateTestBase): - def test_slot_behaviour(self, revenue_withdrawal_state_pending): - inst = revenue_withdrawal_state_pending - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {} rws = RevenueWithdrawalStatePending.de_json(json_dict, offline_bot) @@ -149,12 +136,6 @@ def test_equality(self, revenue_withdrawal_state_pending): class TestRevenueWithdrawalStateSucceededWithoutRequest(RevenueWithdrawalStateTestBase): state = RevenueWithdrawalStateType.SUCCEEDED - def test_slot_behaviour(self, revenue_withdrawal_state_succeeded): - inst = revenue_withdrawal_state_succeeded - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"date": to_timestamp(self.date), "url": self.url} rws = RevenueWithdrawalStateSucceeded.de_json(json_dict, offline_bot) @@ -194,12 +175,6 @@ def test_equality(self, revenue_withdrawal_state_succeeded): class TestRevenueWithdrawalStateFailedWithoutRequest(RevenueWithdrawalStateTestBase): state = RevenueWithdrawalStateType.FAILED - def test_slot_behaviour(self, revenue_withdrawal_state_failed): - inst = revenue_withdrawal_state_failed - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {} rws = RevenueWithdrawalStateFailed.de_json(json_dict, offline_bot) diff --git a/tests/_payment/stars/test_staramount.py b/tests/_payment/stars/test_staramount.py index 6f9ea649450..98405a4fa2b 100644 --- a/tests/_payment/stars/test_staramount.py +++ b/tests/_payment/stars/test_staramount.py @@ -20,7 +20,6 @@ import pytest from telegram import StarAmount -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -37,12 +36,6 @@ class StarTransactionTestBase: class TestStarAmountWithoutRequest(StarTransactionTestBase): - def test_slot_behaviour(self, star_amount): - inst = star_amount - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "amount": self.amount, diff --git a/tests/_payment/stars/test_startransactions.py b/tests/_payment/stars/test_startransactions.py index aa3ef7fe592..fe8e97882d5 100644 --- a/tests/_payment/stars/test_startransactions.py +++ b/tests/_payment/stars/test_startransactions.py @@ -28,7 +28,6 @@ User, ) from telegram._utils.datetime import UTC, from_timestamp, to_timestamp -from tests.auxil.slots import mro_slots def star_transaction_factory(): @@ -74,12 +73,6 @@ class StarTransactionTestBase: class TestStarTransactionWithoutRequest(StarTransactionTestBase): - def test_slot_behaviour(self, star_transaction): - inst = star_transaction - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "id": self.id, @@ -167,12 +160,6 @@ class StarTransactionsTestBase: class TestStarTransactionsWithoutRequest(StarTransactionsTestBase): - def test_slot_behaviour(self, star_transactions): - inst = star_transactions - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "transactions": [t.to_dict() for t in self.transactions], diff --git a/tests/_payment/stars/test_transactionpartner.py b/tests/_payment/stars/test_transactionpartner.py index e17e8480b62..47a19606a64 100644 --- a/tests/_payment/stars/test_transactionpartner.py +++ b/tests/_payment/stars/test_transactionpartner.py @@ -39,7 +39,6 @@ Video, ) from telegram.constants import TransactionPartnerType -from tests.auxil.slots import mro_slots @pytest.fixture @@ -106,12 +105,6 @@ class TransactionPartnerTestBase: class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase): - def test_slot_behaviour(self, transaction_partner): - inst = transaction_partner - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self, transaction_partner): assert type(TransactionPartner("affiliate_program").type) is TransactionPartnerType assert TransactionPartner("unknown").type == "unknown" @@ -181,12 +174,6 @@ def transaction_partner_affiliate_program(): class TestTransactionPartnerAffiliateProgramWithoutRequest(TransactionPartnerTestBase): type = TransactionPartnerType.AFFILIATE_PROGRAM - def test_slot_behaviour(self, transaction_partner_affiliate_program): - inst = transaction_partner_affiliate_program - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "commission_per_mille": self.commission_per_mille, @@ -234,12 +221,6 @@ def transaction_partner_fragment(): class TestTransactionPartnerFragmentWithoutRequest(TransactionPartnerTestBase): type = TransactionPartnerType.FRAGMENT - def test_slot_behaviour(self, transaction_partner_fragment): - inst = transaction_partner_fragment - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"withdrawal_state": self.withdrawal_state.to_dict()} tp = TransactionPartnerFragment.de_json(json_dict, offline_bot) @@ -284,12 +265,6 @@ def transaction_partner_user(): class TestTransactionPartnerUserWithoutRequest(TransactionPartnerTestBase): type = TransactionPartnerType.USER - def test_slot_behaviour(self, transaction_partner_user): - inst = transaction_partner_user - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "user": self.user.to_dict(), @@ -352,12 +327,6 @@ def transaction_partner_other(): class TestTransactionPartnerOtherWithoutRequest(TransactionPartnerTestBase): type = TransactionPartnerType.OTHER - def test_slot_behaviour(self, transaction_partner_other): - inst = transaction_partner_other - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {} tp = TransactionPartnerOther.de_json(json_dict, offline_bot) @@ -392,12 +361,6 @@ def transaction_partner_telegram_ads(): class TestTransactionPartnerTelegramAdsWithoutRequest(TransactionPartnerTestBase): type = TransactionPartnerType.TELEGRAM_ADS - def test_slot_behaviour(self, transaction_partner_telegram_ads): - inst = transaction_partner_telegram_ads - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {} tp = TransactionPartnerTelegramAds.de_json(json_dict, offline_bot) @@ -434,12 +397,6 @@ def transaction_partner_telegram_api(): class TestTransactionPartnerTelegramApiWithoutRequest(TransactionPartnerTestBase): type = TransactionPartnerType.TELEGRAM_API - def test_slot_behaviour(self, transaction_partner_telegram_api): - inst = transaction_partner_telegram_api - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"request_count": self.request_count} tp = TransactionPartnerTelegramApi.de_json(json_dict, offline_bot) @@ -483,12 +440,6 @@ def transaction_partner_chat(): class TestTransactionPartnerChatWithoutRequest(TransactionPartnerTestBase): type = TransactionPartnerType.CHAT - def test_slot_behaviour(self, transaction_partner_chat): - inst = transaction_partner_chat - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "chat": self.chat.to_dict(), diff --git a/tests/_payment/test_invoice.py b/tests/_payment/test_invoice.py index 06985f552aa..42a95fc8325 100644 --- a/tests/_payment/test_invoice.py +++ b/tests/_payment/test_invoice.py @@ -26,7 +26,6 @@ from telegram.error import BadRequest from telegram.request import RequestData from tests.auxil.build_messages import make_message -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -54,11 +53,6 @@ class InvoiceTestBase: class TestInvoiceWithoutRequest(InvoiceTestBase): - def test_slot_behaviour(self, invoice): - for attr in invoice.__slots__: - assert getattr(invoice, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(invoice)) == len(set(mro_slots(invoice))), "duplicate slot" - def test_de_json(self, offline_bot): invoice_json = Invoice.de_json( { diff --git a/tests/_payment/test_labeledprice.py b/tests/_payment/test_labeledprice.py index babea2aae62..0a4621aa7f3 100644 --- a/tests/_payment/test_labeledprice.py +++ b/tests/_payment/test_labeledprice.py @@ -19,7 +19,6 @@ import pytest from telegram import LabeledPrice, Location -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -33,12 +32,6 @@ class LabeledPriceTestBase: class TestLabeledPriceWithoutRequest(LabeledPriceTestBase): - def test_slot_behaviour(self, labeled_price): - inst = labeled_price - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, labeled_price): assert labeled_price.label == self.label assert labeled_price.amount == self.amount diff --git a/tests/_payment/test_orderinfo.py b/tests/_payment/test_orderinfo.py index 3f12a4b582e..fe6a6f364eb 100644 --- a/tests/_payment/test_orderinfo.py +++ b/tests/_payment/test_orderinfo.py @@ -19,7 +19,6 @@ import pytest from telegram import OrderInfo, ShippingAddress -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -40,11 +39,6 @@ class OrderInfoTestBase: class TestOrderInfoWithoutRequest(OrderInfoTestBase): - def test_slot_behaviour(self, order_info): - for attr in order_info.__slots__: - assert getattr(order_info, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(order_info)) == len(set(mro_slots(order_info))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "name": self.name, diff --git a/tests/_payment/test_precheckoutquery.py b/tests/_payment/test_precheckoutquery.py index e8352d52be7..490721dcb0e 100644 --- a/tests/_payment/test_precheckoutquery.py +++ b/tests/_payment/test_precheckoutquery.py @@ -25,7 +25,6 @@ check_shortcut_call, check_shortcut_signature, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -54,12 +53,6 @@ class PreCheckoutQueryTestBase: class TestPreCheckoutQueryWithoutRequest(PreCheckoutQueryTestBase): - def test_slot_behaviour(self, pre_checkout_query): - inst = pre_checkout_query - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "id": self.id_, diff --git a/tests/_payment/test_refundedpayment.py b/tests/_payment/test_refundedpayment.py index cff668fad3b..ccaa9e72ee2 100644 --- a/tests/_payment/test_refundedpayment.py +++ b/tests/_payment/test_refundedpayment.py @@ -19,7 +19,6 @@ import pytest from telegram import RefundedPayment -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -42,12 +41,6 @@ class RefundedPaymentTestBase: class TestRefundedPaymentWithoutRequest(RefundedPaymentTestBase): - def test_slot_behaviour(self, refunded_payment): - inst = refunded_payment - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "invoice_payload": self.invoice_payload, diff --git a/tests/_payment/test_shippingaddress.py b/tests/_payment/test_shippingaddress.py index 8e7348afea7..120453f2327 100644 --- a/tests/_payment/test_shippingaddress.py +++ b/tests/_payment/test_shippingaddress.py @@ -19,7 +19,6 @@ import pytest from telegram import ShippingAddress -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -44,12 +43,6 @@ class ShippingAddressTestBase: class TestShippingAddressWithoutRequest(ShippingAddressTestBase): - def test_slot_behaviour(self, shipping_address): - inst = shipping_address - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "country_code": self.country_code, diff --git a/tests/_payment/test_shippingoption.py b/tests/_payment/test_shippingoption.py index 953e2cf25d6..b8d99be6a3d 100644 --- a/tests/_payment/test_shippingoption.py +++ b/tests/_payment/test_shippingoption.py @@ -19,7 +19,6 @@ import pytest from telegram import LabeledPrice, ShippingOption, Voice -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -36,12 +35,6 @@ class ShippingOptionTestBase: class TestShippingOptionWithoutRequest(ShippingOptionTestBase): - def test_slot_behaviour(self, shipping_option): - inst = shipping_option - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, shipping_option): assert shipping_option.id == self.id_ assert shipping_option.title == self.title diff --git a/tests/_payment/test_shippingquery.py b/tests/_payment/test_shippingquery.py index 23606faedce..ef36d508730 100644 --- a/tests/_payment/test_shippingquery.py +++ b/tests/_payment/test_shippingquery.py @@ -25,7 +25,6 @@ check_shortcut_call, check_shortcut_signature, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -48,12 +47,6 @@ class ShippingQueryTestBase: class TestShippingQueryWithoutRequest(ShippingQueryTestBase): - def test_slot_behaviour(self, shipping_query): - inst = shipping_query - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "id": self.id_, diff --git a/tests/_payment/test_successfulpayment.py b/tests/_payment/test_successfulpayment.py index 51d485052d6..4800a59a3b0 100644 --- a/tests/_payment/test_successfulpayment.py +++ b/tests/_payment/test_successfulpayment.py @@ -22,7 +22,6 @@ from telegram import OrderInfo, SuccessfulPayment from telegram._utils.datetime import UTC, to_timestamp -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -55,12 +54,6 @@ class SuccessfulPaymentTestBase: class TestSuccessfulPaymentWithoutRequest(SuccessfulPaymentTestBase): - def test_slot_behaviour(self, successful_payment): - inst = successful_payment - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "invoice_payload": self.invoice_payload, diff --git a/tests/test_birthdate.py b/tests/test_birthdate.py index 8a2d4d240ec..d062101eaf3 100644 --- a/tests/test_birthdate.py +++ b/tests/test_birthdate.py @@ -21,7 +21,6 @@ import pytest from telegram import Birthdate -from tests.auxil.slots import mro_slots class BirthdateTestBase: @@ -36,11 +35,6 @@ def birthdate(): class TestBirthdateWithoutRequest(BirthdateTestBase): - def test_slot_behaviour(self, birthdate): - for attr in birthdate.__slots__: - assert getattr(birthdate, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(birthdate)) == len(set(mro_slots(birthdate))), "duplicate slot" - def test_to_dict(self, birthdate): bd_dict = birthdate.to_dict() assert isinstance(bd_dict, dict) diff --git a/tests/test_botaccesssettings.py b/tests/test_botaccesssettings.py index 56cb8d05f50..752abf99d9d 100644 --- a/tests/test_botaccesssettings.py +++ b/tests/test_botaccesssettings.py @@ -22,7 +22,6 @@ from telegram import BotAccessSettings, Dice from telegram._user import User -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -39,12 +38,6 @@ class BotAccessSettingsTestBase: class TestBotAccessSettingsWithoutRequest(BotAccessSettingsTestBase): - def test_slot_behaviour(self, bot_access_settings): - inst = bot_access_settings - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "is_access_restricted": self.is_access_restricted, diff --git a/tests/test_botcommand.py b/tests/test_botcommand.py index 4ecceb796b8..0e6ed964af4 100644 --- a/tests/test_botcommand.py +++ b/tests/test_botcommand.py @@ -20,7 +20,6 @@ import pytest from telegram import BotCommand, Dice -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -32,11 +31,6 @@ class TestBotCommandWithoutRequest: command = "start" description = "A command" - def test_slot_behaviour(self, bot_command): - for attr in bot_command.__slots__: - assert getattr(bot_command, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(bot_command)) == len(set(mro_slots(bot_command))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"command": self.command, "description": self.description} bot_command = BotCommand.de_json(json_dict, offline_bot) diff --git a/tests/test_botcommandscope.py b/tests/test_botcommandscope.py index 6b520d53192..dec22bf2ca1 100644 --- a/tests/test_botcommandscope.py +++ b/tests/test_botcommandscope.py @@ -31,7 +31,6 @@ Dice, ) from telegram.constants import BotCommandScopeType -from tests.auxil.slots import mro_slots @pytest.fixture @@ -46,12 +45,6 @@ class BotCommandScopeTestBase: class TestBotCommandScopeWithoutRequest(BotCommandScopeTestBase): - def test_slot_behaviour(self, bot_command_scope): - inst = bot_command_scope - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self, bot_command_scope): assert type(BotCommandScope("default").type) is BotCommandScopeType assert BotCommandScope("unknown").type == "unknown" @@ -116,12 +109,6 @@ def bot_command_scope_all_private_chats(): class TestBotCommandScopeAllPrivateChatsWithoutRequest(BotCommandScopeTestBase): type = BotCommandScopeType.ALL_PRIVATE_CHATS - def test_slot_behaviour(self, bot_command_scope_all_private_chats): - inst = bot_command_scope_all_private_chats - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): transaction_partner = BotCommandScopeAllPrivateChats.de_json({}, offline_bot) assert transaction_partner.api_kwargs == {} @@ -156,12 +143,6 @@ def bot_command_scope_all_chat_administrators(): class TestBotCommandScopeAllChatAdministratorsWithoutRequest(BotCommandScopeTestBase): type = BotCommandScopeType.ALL_CHAT_ADMINISTRATORS - def test_slot_behaviour(self, bot_command_scope_all_chat_administrators): - inst = bot_command_scope_all_chat_administrators - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): transaction_partner = BotCommandScopeAllChatAdministrators.de_json({}, offline_bot) assert transaction_partner.api_kwargs == {} @@ -196,12 +177,6 @@ def bot_command_scope_all_group_chats(): class TestBotCommandScopeAllGroupChatsWithoutRequest(BotCommandScopeTestBase): type = BotCommandScopeType.ALL_GROUP_CHATS - def test_slot_behaviour(self, bot_command_scope_all_group_chats): - inst = bot_command_scope_all_group_chats - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): transaction_partner = BotCommandScopeAllGroupChats.de_json({}, offline_bot) assert transaction_partner.api_kwargs == {} @@ -236,12 +211,6 @@ def bot_command_scope_chat(): class TestBotCommandScopeChatWithoutRequest(BotCommandScopeTestBase): type = BotCommandScopeType.CHAT - def test_slot_behaviour(self, bot_command_scope_chat): - inst = bot_command_scope_chat - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): transaction_partner = BotCommandScopeChat.de_json({"chat_id": self.chat_id}, offline_bot) assert transaction_partner.api_kwargs == {} @@ -284,12 +253,6 @@ def bot_command_scope_chat_administrators(): class TestBotCommandScopeChatAdministratorsWithoutRequest(BotCommandScopeTestBase): type = BotCommandScopeType.CHAT_ADMINISTRATORS - def test_slot_behaviour(self, bot_command_scope_chat_administrators): - inst = bot_command_scope_chat_administrators - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): transaction_partner = BotCommandScopeChatAdministrators.de_json( {"chat_id": self.chat_id}, offline_bot @@ -335,12 +298,6 @@ def bot_command_scope_chat_member(): class TestBotCommandScopeChatMemberWithoutRequest(BotCommandScopeTestBase): type = BotCommandScopeType.CHAT_MEMBER - def test_slot_behaviour(self, bot_command_scope_chat_member): - inst = bot_command_scope_chat_member - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): transaction_partner = BotCommandScopeChatMember.de_json( {"chat_id": self.chat_id, "user_id": self.user_id}, offline_bot @@ -389,12 +346,6 @@ def bot_command_scope_default(): class TestBotCommandScopeDefaultWithoutRequest(BotCommandScopeTestBase): type = BotCommandScopeType.DEFAULT - def test_slot_behaviour(self, bot_command_scope_default): - inst = bot_command_scope_default - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): transaction_partner = BotCommandScopeDefault.de_json({}, offline_bot) assert transaction_partner.api_kwargs == {} diff --git a/tests/test_botdescription.py b/tests/test_botdescription.py index 142545c3419..4436025a915 100644 --- a/tests/test_botdescription.py +++ b/tests/test_botdescription.py @@ -19,7 +19,6 @@ import pytest from telegram import BotDescription, BotShortDescription -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -38,13 +37,6 @@ class BotDescriptionTestBase: class TestBotDescriptionWithoutRequest(BotDescriptionTestBase): - def test_slot_behaviour(self, bot_description): - for attr in bot_description.__slots__: - assert getattr(bot_description, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(bot_description)) == len(set(mro_slots(bot_description))), ( - "duplicate slot" - ) - def test_to_dict(self, bot_description): bot_description_dict = bot_description.to_dict() @@ -65,13 +57,6 @@ def test_equality(self): class TestBotShortDescriptionWithoutRequest(BotDescriptionTestBase): - def test_slot_behaviour(self, bot_short_description): - for attr in bot_short_description.__slots__: - assert getattr(bot_short_description, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(bot_short_description)) == len( - set(mro_slots(bot_short_description)) - ), "duplicate slot" - def test_to_dict(self, bot_short_description): bot_short_description_dict = bot_short_description.to_dict() diff --git a/tests/test_botname.py b/tests/test_botname.py index 289e07e1add..389b052634e 100644 --- a/tests/test_botname.py +++ b/tests/test_botname.py @@ -19,7 +19,6 @@ import pytest from telegram import BotName -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -32,11 +31,6 @@ class BotNameTestBase: class TestBotNameWithoutRequest(BotNameTestBase): - def test_slot_behaviour(self, bot_name): - for attr in bot_name.__slots__: - assert getattr(bot_name, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(bot_name)) == len(set(mro_slots(bot_name))), "duplicate slot" - def test_to_dict(self, bot_name): bot_name_dict = bot_name.to_dict() diff --git a/tests/test_business_classes.py b/tests/test_business_classes.py index 8994243d1e6..6adf4fee2b4 100644 --- a/tests/test_business_classes.py +++ b/tests/test_business_classes.py @@ -35,7 +35,6 @@ ) from telegram._business import BusinessBotRights from telegram._utils.datetime import UTC, to_timestamp -from tests.auxil.slots import mro_slots class BusinessTestBase: @@ -149,12 +148,6 @@ def business_opening_hours(): class TestBusinessBotRightsWithoutRequest(BusinessTestBase): - def test_slot_behaviour(self, business_bot_rights): - inst = business_bot_rights - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_to_dict(self, business_bot_rights): rights_dict = business_bot_rights.to_dict() @@ -233,12 +226,6 @@ def test_equality(self): class TestBusinessConnectionWithoutRequest(BusinessTestBase): - def test_slots(self, business_connection): - bc = business_connection - for attr in bc.__slots__: - assert getattr(bc, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(bc)) == len(set(mro_slots(bc))), "duplicate slot" - def test_de_json(self, business_bot_rights): json_dict = { "id": self.id_, @@ -334,12 +321,6 @@ def test_equality(self, business_bot_rights): class TestBusinessMessagesDeleted(BusinessTestBase): - def test_slots(self, business_messages_deleted): - bmd = business_messages_deleted - for attr in bmd.__slots__: - assert getattr(bmd, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(bmd)) == len(set(mro_slots(bmd))), "duplicate slot" - def test_to_dict(self, business_messages_deleted): bmd_dict = business_messages_deleted.to_dict() assert isinstance(bmd_dict, dict) @@ -373,12 +354,6 @@ def test_equality(self): class TestBusinessIntroWithoutRequest(BusinessTestBase): - def test_slot_behaviour(self, business_intro): - intro = business_intro - for attr in intro.__slots__: - assert getattr(intro, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(intro)) == len(set(mro_slots(intro))), "duplicate slot" - def test_to_dict(self, business_intro): intro_dict = business_intro.to_dict() assert isinstance(intro_dict, dict) @@ -413,12 +388,6 @@ def test_equality(self): class TestBusinessLocationWithoutRequest(BusinessTestBase): - def test_slot_behaviour(self, business_location): - inst = business_location - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_to_dict(self, business_location): blc_dict = business_location.to_dict() assert isinstance(blc_dict, dict) @@ -450,12 +419,6 @@ def test_equality(self): class TestBusinessOpeningHoursIntervalWithoutRequest(BusinessTestBase): - def test_slot_behaviour(self, business_opening_hours_interval): - inst = business_opening_hours_interval - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_to_dict(self, business_opening_hours_interval): bohi_dict = business_opening_hours_interval.to_dict() assert isinstance(bohi_dict, dict) @@ -521,12 +484,6 @@ def test_closing_time(self, closing_minute, expected): class TestBusinessOpeningHoursWithoutRequest(BusinessTestBase): - def test_slot_behaviour(self, business_opening_hours): - inst = business_opening_hours - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_to_dict(self, business_opening_hours): boh_dict = business_opening_hours.to_dict() assert isinstance(boh_dict, dict) diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index d45f171ef8a..734e72d1970 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -37,7 +37,6 @@ check_shortcut_call, check_shortcut_signature, ) -from tests.auxil.slots import mro_slots @pytest.fixture(params=["message", "inline", "inaccessible_message"]) @@ -101,11 +100,6 @@ def check_passed_ids(callback_query: CallbackQuery, kwargs): message_id = kwargs["message_id"] == callback_query.message.message_id return id_ and chat_id and message_id - def test_slot_behaviour(self, callback_query): - for attr in callback_query.__slots__: - assert getattr(callback_query, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(callback_query)) == len(set(mro_slots(callback_query))), "same slot" - def test_de_json(self, offline_bot): json_dict = { "id": self.id_, diff --git a/tests/test_chat.py b/tests/test_chat.py index 99208b5844c..24a99b2404e 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -36,7 +36,6 @@ check_shortcut_call, check_shortcut_signature, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -68,11 +67,6 @@ class ChatTestBase: class TestChatWithoutRequest(ChatTestBase): - def test_slot_behaviour(self, chat): - for attr in chat.__slots__: - assert getattr(chat, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(chat)) == len(set(mro_slots(chat))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "id": self.id_, diff --git a/tests/test_chatadministratorrights.py b/tests/test_chatadministratorrights.py index c66cd98e588..d91a00de065 100644 --- a/tests/test_chatadministratorrights.py +++ b/tests/test_chatadministratorrights.py @@ -19,7 +19,6 @@ import pytest from telegram import ChatAdministratorRights -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -46,12 +45,6 @@ def chat_admin_rights(): class TestChatAdministratorRightsWithoutRequest: - def test_slot_behaviour(self, chat_admin_rights): - inst = chat_admin_rights - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot, chat_admin_rights): json_dict = { "can_change_info": True, diff --git a/tests/test_chatbackground.py b/tests/test_chatbackground.py index b5a4ebc7b43..bd4bf88ed63 100644 --- a/tests/test_chatbackground.py +++ b/tests/test_chatbackground.py @@ -34,7 +34,6 @@ Document, ) from telegram.constants import BackgroundFillType, BackgroundTypeType -from tests.auxil.slots import mro_slots @pytest.fixture @@ -52,12 +51,6 @@ class BackgroundFillTestBase: class TestBackgroundFillWithoutRequest(BackgroundFillTestBase): - def test_slots(self, background_fill): - inst = background_fill - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self, background_fill): assert type(BackgroundFill("solid").type) is BackgroundFillType assert BackgroundFill("unknown").type == "unknown" @@ -124,12 +117,6 @@ def background_fill_gradient(): class TestBackgroundFillGradientWithoutRequest(BackgroundFillTestBase): type = BackgroundFill.GRADIENT - def test_slots(self, background_fill_gradient): - inst = background_fill_gradient - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = { "top_color": self.top_color, @@ -182,12 +169,6 @@ def background_fill_freeform_gradient(): class TestBackgroundFillFreeformGradientWithoutRequest(BackgroundFillTestBase): type = BackgroundFill.FREEFORM_GRADIENT - def test_slots(self, background_fill_freeform_gradient): - inst = background_fill_freeform_gradient - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = {"colors": self.colors} transaction_partner = BackgroundFillFreeformGradient.de_json(data, offline_bot) @@ -224,12 +205,6 @@ def background_fill_solid(): class TestBackgroundFillSolidWithoutRequest(BackgroundFillTestBase): type = BackgroundFill.SOLID - def test_slots(self, background_fill_solid): - inst = background_fill_solid - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = {"color": self.color} transaction_partner = BackgroundFillSolid.de_json(data, offline_bot) @@ -276,12 +251,6 @@ class BackgroundTypeTestBase: class TestBackgroundTypeWithoutRequest(BackgroundTypeTestBase): - def test_slots(self, background_type): - inst = background_type - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self, background_type): assert type(BackgroundType("wallpaper").type) is BackgroundTypeType assert BackgroundType("unknown").type == "unknown" @@ -351,12 +320,6 @@ def background_type_fill(): class TestBackgroundTypeFillWithoutRequest(BackgroundTypeTestBase): type = BackgroundType.FILL - def test_slots(self, background_type_fill): - inst = background_type_fill - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = {"fill": self.fill.to_dict(), "dark_theme_dimming": self.dark_theme_dimming} transaction_partner = BackgroundTypeFill.de_json(data, offline_bot) @@ -400,12 +363,6 @@ def background_type_pattern(): class TestBackgroundTypePatternWithoutRequest(BackgroundTypeTestBase): type = BackgroundType.PATTERN - def test_slots(self, background_type_pattern): - inst = background_type_pattern - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = { "document": self.document.to_dict(), @@ -463,12 +420,6 @@ def background_type_chat_theme(): class TestBackgroundTypeChatThemeWithoutRequest(BackgroundTypeTestBase): type = BackgroundType.CHAT_THEME - def test_slots(self, background_type_chat_theme): - inst = background_type_chat_theme - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = {"theme_name": self.theme_name} transaction_partner = BackgroundTypeChatTheme.de_json(data, offline_bot) @@ -510,12 +461,6 @@ def background_type_wallpaper(): class TestBackgroundTypeWallpaperWithoutRequest(BackgroundTypeTestBase): type = BackgroundType.WALLPAPER - def test_slots(self, background_type_wallpaper): - inst = background_type_wallpaper - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = { "document": self.document.to_dict(), @@ -572,12 +517,6 @@ class ChatBackgroundTestBase: class TestChatBackgroundWithoutRequest(ChatBackgroundTestBase): - def test_slots(self, chat_background): - inst = chat_background - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = {"type": self.type.to_dict()} transaction_partner = ChatBackground.de_json(data, offline_bot) diff --git a/tests/test_chatboost.py b/tests/test_chatboost.py index 578bc4f0824..3f899b0f557 100644 --- a/tests/test_chatboost.py +++ b/tests/test_chatboost.py @@ -37,7 +37,6 @@ from telegram.constants import ChatBoostSources from telegram.request import RequestData from tests.auxil.dummy_objects import get_dummy_object_json_dict -from tests.auxil.slots import mro_slots class ChatBoostDefaults: @@ -67,12 +66,6 @@ def chat_boost_source(): class TestChatBoostSourceWithoutRequest(ChatBoostDefaults): - def test_slot_behaviour(self, chat_boost_source): - inst = chat_boost_source - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self, chat_boost_source): assert type(ChatBoostSource("premium").source) is ChatBoostSources assert ChatBoostSource("unknown").source == "unknown" @@ -140,12 +133,6 @@ def chat_boost_source_premium(): class TestChatBoostSourcePremiumWithoutRequest(ChatBoostDefaults): source = ChatBoostSources.PREMIUM - def test_slot_behaviour(self, chat_boost_source_premium): - inst = chat_boost_source_premium - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "user": self.user.to_dict(), @@ -184,12 +171,6 @@ def chat_boost_source_gift_code(): class TestChatBoostSourceGiftCodeWithoutRequest(ChatBoostDefaults): source = ChatBoostSources.GIFT_CODE - def test_slot_behaviour(self, chat_boost_source_gift_code): - inst = chat_boost_source_gift_code - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "user": self.user.to_dict(), @@ -229,12 +210,6 @@ def chat_boost_source_giveaway(): class TestChatBoostSourceGiveawayWithoutRequest(ChatBoostDefaults): source = ChatBoostSources.GIVEAWAY - def test_slot_behaviour(self, chat_boost_source_giveaway): - inst = chat_boost_source_giveaway - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "user": self.user.to_dict(), @@ -277,12 +252,6 @@ def chat_boost(): class TestChatBoostWithoutRequest(ChatBoostDefaults): - def test_slot_behaviour(self, chat_boost): - inst = chat_boost - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot, chat_boost): json_dict = { "boost_id": self.boost_id, @@ -363,12 +332,6 @@ def chat_boost_updated(chat_boost): class TestChatBoostUpdatedWithoutRequest(ChatBoostDefaults): - def test_slot_behaviour(self, chat_boost_updated): - inst = chat_boost_updated - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot, chat_boost): json_dict = { "chat": self.chat.to_dict(), @@ -434,12 +397,6 @@ def chat_boost_removed(): class TestChatBoostRemovedWithoutRequest(ChatBoostDefaults): - def test_slot_behaviour(self, chat_boost_removed): - inst = chat_boost_removed - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot, chat_boost_removed): json_dict = { "chat": self.chat.to_dict(), @@ -521,12 +478,6 @@ def user_chat_boosts(chat_boost): class TestUserChatBoostsWithoutRequest(ChatBoostDefaults): - def test_slot_behaviour(self, user_chat_boosts): - inst = user_chat_boosts - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot, user_chat_boosts): json_dict = { "boosts": [ @@ -568,12 +519,6 @@ async def test_get_user_chat_boosts(self, bot, channel_id, chat_id): class TestChatBoostAddedWithoutRequest: boost_count = 100 - def test_slot_behaviour(self): - action = ChatBoostAdded(8) - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self): json_dict = {"boost_count": self.boost_count} chat_boost_added = ChatBoostAdded.de_json(json_dict, None) diff --git a/tests/test_chatfullinfo.py b/tests/test_chatfullinfo.py index eaac1ab662b..69e5e71f437 100644 --- a/tests/test_chatfullinfo.py +++ b/tests/test_chatfullinfo.py @@ -41,7 +41,6 @@ from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import ReactionEmoji from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -175,13 +174,6 @@ class ChatFullInfoTestBase: class TestChatFullInfoWithoutRequest(ChatFullInfoTestBase): - def test_slot_behaviour(self, chat_full_info): - cfi = chat_full_info - for attr in cfi.__slots__: - assert getattr(cfi, attr, "err") != "err", f"got extra slot '{attr}'" - - assert len(mro_slots(cfi)) == len(set(mro_slots(cfi))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "id": self.id_, diff --git a/tests/test_chatinvitelink.py b/tests/test_chatinvitelink.py index 36acfbffbab..3ef08dbf0d4 100644 --- a/tests/test_chatinvitelink.py +++ b/tests/test_chatinvitelink.py @@ -23,7 +23,6 @@ from telegram import ChatInviteLink, User from telegram._utils.datetime import UTC, to_timestamp from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -62,11 +61,6 @@ class ChatInviteLinkTestBase: class TestChatInviteLinkWithoutRequest(ChatInviteLinkTestBase): - def test_slot_behaviour(self, invite_link): - for attr in invite_link.__slots__: - assert getattr(invite_link, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(invite_link)) == len(set(mro_slots(invite_link))), "duplicate slot" - def test_de_json_required_args(self, offline_bot, creator): json_dict = { "invite_link": self.link, diff --git a/tests/test_chatjoinrequest.py b/tests/test_chatjoinrequest.py index 85c94e5852f..6c0ae7a4150 100644 --- a/tests/test_chatjoinrequest.py +++ b/tests/test_chatjoinrequest.py @@ -27,7 +27,6 @@ check_shortcut_call, check_shortcut_signature, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -64,12 +63,6 @@ class ChatJoinRequestTestBase: class TestChatJoinRequestWithoutRequest(ChatJoinRequestTestBase): - def test_slot_behaviour(self, chat_join_request): - inst = chat_join_request - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot, time): json_dict = { "chat": self.chat.to_dict(), diff --git a/tests/test_chatlocation.py b/tests/test_chatlocation.py index e0ba62d3cd3..b0a9870e18d 100644 --- a/tests/test_chatlocation.py +++ b/tests/test_chatlocation.py @@ -20,7 +20,6 @@ import pytest from telegram import ChatLocation, Location, User -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -34,12 +33,6 @@ class ChatLocationTestBase: class TestChatLocationWithoutRequest(ChatLocationTestBase): - def test_slot_behaviour(self, chat_location): - inst = chat_location - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "location": self.location.to_dict(), diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index a321f035fac..2830bc8413f 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -35,7 +35,6 @@ ) from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import ChatMemberStatus -from tests.auxil.slots import mro_slots @pytest.fixture @@ -83,12 +82,6 @@ class ChatMemberTestBase: class TestChatMemberWithoutRequest(ChatMemberTestBase): - def test_slot_behaviour(self, chat_member): - inst = chat_member - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_status_enum_conversion(self, chat_member): assert type(ChatMember(ChatMemberTestBase.user, "member").status) is ChatMemberStatus assert ChatMember(ChatMemberTestBase.user, "unknown").status == "unknown" @@ -183,12 +176,6 @@ def chat_member_administrator(): class TestChatMemberAdministratorWithoutRequest(ChatMemberTestBase): status = ChatMemberStatus.ADMINISTRATOR - def test_slot_behaviour(self, chat_member_administrator): - inst = chat_member_administrator - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = { "user": self.user.to_dict(), @@ -322,12 +309,6 @@ def chat_member_banned(): class TestChatMemberBannedWithoutRequest(ChatMemberTestBase): status = ChatMemberStatus.BANNED - def test_slot_behaviour(self, chat_member_banned): - inst = chat_member_banned - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = { "user": self.user.to_dict(), @@ -396,12 +377,6 @@ def chat_member_left(): class TestChatMemberLeftWithoutRequest(ChatMemberTestBase): status = ChatMemberStatus.LEFT - def test_slot_behaviour(self, chat_member_left): - inst = chat_member_left - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = {"user": self.user.to_dict()} chat_member = ChatMemberLeft.de_json(data, offline_bot) @@ -441,12 +416,6 @@ def chat_member_member(): class TestChatMemberMemberWithoutRequest(ChatMemberTestBase): status = ChatMemberStatus.MEMBER - def test_slot_behaviour(self, chat_member_member): - inst = chat_member_member - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = {"user": self.user.to_dict(), "until_date": to_timestamp(self.until_date)} chat_member = ChatMemberMember.de_json(data, offline_bot) @@ -511,12 +480,6 @@ def chat_member_owner(): class TestChatMemberOwnerWithoutRequest(ChatMemberTestBase): status = ChatMemberStatus.OWNER - def test_slot_behaviour(self, chat_member_owner): - inst = chat_member_owner - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = { "user": self.user.to_dict(), @@ -585,12 +548,6 @@ def chat_member_restricted(): class TestChatMemberRestrictedWithoutRequest(ChatMemberTestBase): status = ChatMemberStatus.RESTRICTED - def test_slot_behaviour(self, chat_member_restricted): - inst = chat_member_restricted - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): data = { "user": self.user.to_dict(), diff --git a/tests/test_chatmemberupdated.py b/tests/test_chatmemberupdated.py index ec76fd2fce7..512fad13042 100644 --- a/tests/test_chatmemberupdated.py +++ b/tests/test_chatmemberupdated.py @@ -32,7 +32,6 @@ User, ) from telegram._utils.datetime import UTC, to_timestamp -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -93,12 +92,6 @@ class ChatMemberUpdatedTestBase: class TestChatMemberUpdatedWithoutRequest(ChatMemberUpdatedTestBase): - def test_slot_behaviour(self, chat_member_updated): - action = chat_member_updated - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json_required_args( self, offline_bot, user, chat, old_chat_member, new_chat_member, time ): diff --git a/tests/test_chatowner.py b/tests/test_chatowner.py index 03b56bdb103..deee2a4e69c 100644 --- a/tests/test_chatowner.py +++ b/tests/test_chatowner.py @@ -19,7 +19,6 @@ import pytest from telegram import BotCommand, ChatOwnerChanged, ChatOwnerLeft, User -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -37,12 +36,6 @@ class ChatOwnerTestBase: class TestChatOwnerChangedWithoutRequest(ChatOwnerTestBase): - def test_slot_behaviour(self, chat_owner_changed): - inst = chat_owner_changed - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "new_owner": self.new_owner.to_dict(), @@ -76,12 +69,6 @@ def test_equality(self, chat_owner_changed): class TestChatOwnerLeftWithoutRequest(ChatOwnerTestBase): - def test_slot_behaviour(self, chat_owner_left): - inst = chat_owner_left - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "new_owner": self.new_owner.to_dict(), diff --git a/tests/test_chatpermissions.py b/tests/test_chatpermissions.py index 43aeabcf537..a6d6d06f33f 100644 --- a/tests/test_chatpermissions.py +++ b/tests/test_chatpermissions.py @@ -20,7 +20,6 @@ import pytest from telegram import ChatPermissions, User -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -65,12 +64,6 @@ class ChatPermissionsTestBase: class TestChatPermissionsWithoutRequest(ChatPermissionsTestBase): - def test_slot_behaviour(self, chat_permissions): - inst = chat_permissions - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "can_send_messages": self.can_send_messages, diff --git a/tests/test_checklists.py b/tests/test_checklists.py index 5395a900e35..13a2588c074 100644 --- a/tests/test_checklists.py +++ b/tests/test_checklists.py @@ -33,7 +33,6 @@ from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import ZERO_DATE from tests.auxil.build_messages import make_message -from tests.auxil.slots import mro_slots class ChecklistTaskTestBase: @@ -61,13 +60,6 @@ def checklist_task(): class TestChecklistTaskWithoutRequest(ChecklistTaskTestBase): - def test_slot_behaviour(self, checklist_task): - for attr in checklist_task.__slots__: - assert getattr(checklist_task, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(checklist_task)) == len(set(mro_slots(checklist_task))), ( - "duplicate slot" - ) - def test_to_dict(self, checklist_task): clt_dict = checklist_task.to_dict() assert isinstance(clt_dict, dict) @@ -215,11 +207,6 @@ def checklist(): class TestChecklistWithoutRequest(ChecklistTestBase): - def test_slot_behaviour(self, checklist): - for attr in checklist.__slots__: - assert getattr(checklist, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(checklist)) == len(set(mro_slots(checklist))), "duplicate slot" - def test_to_dict(self, checklist): cl_dict = checklist.to_dict() assert isinstance(cl_dict, dict) @@ -310,13 +297,6 @@ def checklist_tasks_done(): class TestChecklistTasksDoneWithoutRequest(ChecklistTasksDoneTestBase): - def test_slot_behaviour(self, checklist_tasks_done): - for attr in checklist_tasks_done.__slots__: - assert getattr(checklist_tasks_done, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(checklist_tasks_done)) == len(set(mro_slots(checklist_tasks_done))), ( - "duplicate slot" - ) - def test_to_dict(self, checklist_tasks_done): cltd_dict = checklist_tasks_done.to_dict() assert isinstance(cltd_dict, dict) @@ -386,13 +366,6 @@ def checklist_tasks_added(): class TestChecklistTasksAddedWithoutRequest(ChecklistTasksAddedTestBase): - def test_slot_behaviour(self, checklist_tasks_added): - for attr in checklist_tasks_added.__slots__: - assert getattr(checklist_tasks_added, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(checklist_tasks_added)) == len( - set(mro_slots(checklist_tasks_added)) - ), "duplicate slot" - def test_to_dict(self, checklist_tasks_added): clta_dict = checklist_tasks_added.to_dict() assert isinstance(clta_dict, dict) diff --git a/tests/test_choseninlineresult.py b/tests/test_choseninlineresult.py index f5b1ce99274..dc5b2405cbd 100644 --- a/tests/test_choseninlineresult.py +++ b/tests/test_choseninlineresult.py @@ -20,7 +20,6 @@ import pytest from telegram import ChosenInlineResult, Location, User, Voice -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -43,12 +42,6 @@ class ChosenInlineResultTestBase: class TestChosenInlineResultWithoutRequest(ChosenInlineResultTestBase): - def test_slot_behaviour(self, chosen_inline_result): - inst = chosen_inline_result - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json_required(self, offline_bot, user): json_dict = {"result_id": self.result_id, "from": user.to_dict(), "query": self.query} result = ChosenInlineResult.de_json(json_dict, offline_bot) diff --git a/tests/test_copytextbutton.py b/tests/test_copytextbutton.py index 537a302e694..3497140ddb8 100644 --- a/tests/test_copytextbutton.py +++ b/tests/test_copytextbutton.py @@ -20,7 +20,6 @@ import pytest from telegram import BotCommand, CopyTextButton -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -33,13 +32,6 @@ class CopyTextButtonTestBase: class TestCopyTextButtonWithoutRequest(CopyTextButtonTestBase): - def test_slot_behaviour(self, copy_text_button): - for attr in copy_text_button.__slots__: - assert getattr(copy_text_button, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(copy_text_button)) == len(set(mro_slots(copy_text_button))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = {"text": self.text} copy_text_button = CopyTextButton.de_json(json_dict, offline_bot) diff --git a/tests/test_dice.py b/tests/test_dice.py index bab02522f9f..bcc0f694f34 100644 --- a/tests/test_dice.py +++ b/tests/test_dice.py @@ -20,7 +20,6 @@ import pytest from telegram import BotCommand, Dice -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module", params=Dice.ALL_EMOJI) @@ -33,11 +32,6 @@ class DiceTestBase: class TestDiceWithoutRequest(DiceTestBase): - def test_slot_behaviour(self, dice): - for attr in dice.__slots__: - assert getattr(dice, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(dice)) == len(set(mro_slots(dice))), "duplicate slot" - @pytest.mark.parametrize("emoji", Dice.ALL_EMOJI) def test_de_json(self, offline_bot, emoji): json_dict = {"value": self.value, "emoji": emoji} diff --git a/tests/test_directmessagepricechanged.py b/tests/test_directmessagepricechanged.py index 58dd722b4b9..88ccccf46b9 100644 --- a/tests/test_directmessagepricechanged.py +++ b/tests/test_directmessagepricechanged.py @@ -23,7 +23,6 @@ import pytest from telegram import DirectMessagePriceChanged, User -from tests.auxil.slots import mro_slots if TYPE_CHECKING: from telegram._utils.types import JSONDict @@ -43,12 +42,6 @@ class DirectMessagePriceChangedTestBase: class TestDirectMessagePriceChangedWithoutRequest(DirectMessagePriceChangedTestBase): - def test_slot_behaviour(self, direct_message_price_changed): - action = direct_message_price_changed - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict: JSONDict = { "are_direct_messages_enabled": self.are_direct_messages_enabled, diff --git a/tests/test_directmessagestopic.py b/tests/test_directmessagestopic.py index 53161478b5c..609c5424141 100644 --- a/tests/test_directmessagestopic.py +++ b/tests/test_directmessagestopic.py @@ -21,7 +21,6 @@ import pytest from telegram import DirectMessagesTopic, User -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -41,13 +40,6 @@ class DirectMessagesTopicTestBase: class TestDirectMessagesTopicWithoutRequest(DirectMessagesTopicTestBase): - def test_slot_behaviour(self, direct_messages_topic): - cfi = direct_messages_topic - for attr in cfi.__slots__: - assert getattr(cfi, attr, "err") != "err", f"got extra slot '{attr}'" - - assert len(mro_slots(cfi)) == len(set(mro_slots(cfi))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "topic_id": self.topic_id, diff --git a/tests/test_forcereply.py b/tests/test_forcereply.py index cfbe51cce34..6f8edfd3253 100644 --- a/tests/test_forcereply.py +++ b/tests/test_forcereply.py @@ -20,7 +20,6 @@ import pytest from telegram import ForceReply, ReplyKeyboardRemove -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -35,11 +34,6 @@ class ForceReplyTestBase: class TestForceReplyWithoutRequest(ForceReplyTestBase): - def test_slot_behaviour(self, force_reply): - for attr in force_reply.__slots__: - assert getattr(force_reply, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(force_reply)) == len(set(mro_slots(force_reply))), "duplicate slot" - def test_expected(self, force_reply): assert force_reply.force_reply == self.force_reply assert force_reply.selective == self.selective diff --git a/tests/test_forum.py b/tests/test_forum.py index 73d20c6d786..ef5b4203108 100644 --- a/tests/test_forum.py +++ b/tests/test_forum.py @@ -33,7 +33,6 @@ ) from telegram.error import BadRequest from tests.auxil.constants import TEST_MSG_TEXT, TEST_TOPIC_ICON_COLOR, TEST_TOPIC_NAME -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -54,12 +53,6 @@ class ForumTopicTestBase: class TestForumTopicWithoutRequest(ForumTopicTestBase): - def test_slot_behaviour(self, forum_topic_object): - inst = forum_topic_object - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - async def test_expected_values(self, emoji_id, forum_group_id, forum_topic_object): assert forum_topic_object.message_thread_id == forum_group_id assert forum_topic_object.icon_color == self.TEST_TOPIC_ICON_COLOR @@ -314,13 +307,6 @@ class ForumTopicCreatedTestBase: class TestForumTopicCreatedWithoutRequest(ForumTopicCreatedTestBase): - def test_slot_behaviour(self, topic_created): - for attr in topic_created.__slots__: - assert getattr(topic_created, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(topic_created)) == len(set(mro_slots(topic_created))), ( - "duplicate slot" - ) - def test_expected_values(self, topic_created): assert topic_created.icon_color == self.TEST_TOPIC_ICON_COLOR assert topic_created.name == self.TEST_TOPIC_NAME @@ -368,12 +354,6 @@ def test_equality(self, emoji_id): class TestForumTopicClosedWithoutRequest: - def test_slot_behaviour(self): - action = ForumTopicClosed() - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self): action = ForumTopicClosed.de_json({}, None) assert action.api_kwargs == {} @@ -386,12 +366,6 @@ def test_to_dict(self): class TestForumTopicReopenedWithoutRequest: - def test_slot_behaviour(self): - action = ForumTopicReopened() - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self): action = ForumTopicReopened.de_json({}, None) assert action.api_kwargs == {} @@ -409,11 +383,6 @@ def topic_edited(emoji_id): class TestForumTopicEdited: - def test_slot_behaviour(self, topic_edited): - for attr in topic_edited.__slots__: - assert getattr(topic_edited, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(topic_edited)) == len(set(mro_slots(topic_edited))), "duplicate slot" - def test_expected_values(self, topic_edited, emoji_id): assert topic_edited.name == TEST_TOPIC_NAME assert topic_edited.icon_custom_emoji_id == emoji_id @@ -458,12 +427,6 @@ def test_equality(self, emoji_id): class TestGeneralForumTopicHidden: - def test_slot_behaviour(self): - action = GeneralForumTopicHidden() - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self): action = GeneralForumTopicHidden.de_json({}, None) assert action.api_kwargs == {} @@ -476,12 +439,6 @@ def test_to_dict(self): class TestGeneralForumTopicUnhidden: - def test_slot_behaviour(self): - action = GeneralForumTopicUnhidden() - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self): action = GeneralForumTopicUnhidden.de_json({}, None) assert action.api_kwargs == {} diff --git a/tests/test_gifts.py b/tests/test_gifts.py index c09d1b30a6e..1a4ab9a4d85 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -24,7 +24,6 @@ from telegram._gifts import AcceptedGiftTypes, GiftBackground from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.request import RequestData -from tests.auxil.slots import mro_slots @pytest.fixture @@ -43,13 +42,6 @@ class GiftBackgroundTestBase: class TestGiftBackgroundWithoutRequest(GiftBackgroundTestBase): - def test_slot_behaviour(self, gift_background): - for attr in gift_background.__slots__: - assert getattr(gift_background, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(gift_background)) == len(set(mro_slots(gift_background))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "center_color": self.center_color, @@ -136,11 +128,6 @@ class GiftTestBase: class TestGiftWithoutRequest(GiftTestBase): - def test_slot_behaviour(self, gift): - for attr in gift.__slots__: - assert getattr(gift, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(gift)) == len(set(mro_slots(gift))), "duplicate slot" - def test_de_json(self, offline_bot, gift): json_dict = { "id": self.id, @@ -352,11 +339,6 @@ class GiftsTestBase: class TestGiftsWithoutRequest(GiftsTestBase): - def test_slot_behaviour(self, gifts): - for attr in gifts.__slots__: - assert getattr(gifts, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(gifts)) == len(set(mro_slots(gifts))), "duplicate slot" - def test_de_json(self, offline_bot, gifts): json_dict = {"gifts": [gift.to_dict() for gift in self.gifts]} gifts = Gifts.de_json(json_dict, offline_bot) @@ -440,11 +422,6 @@ class GiftInfoTestBase: class TestGiftInfoWithoutRequest(GiftInfoTestBase): - def test_slot_behaviour(self, gift_info): - for attr in gift_info.__slots__: - assert getattr(gift_info, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(gift_info)) == len(set(mro_slots(gift_info))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "gift": self.gift.to_dict(), @@ -548,13 +525,6 @@ class AcceptedGiftTypesTestBase: class TestAcceptedGiftTypesWithoutRequest(AcceptedGiftTypesTestBase): - def test_slot_behaviour(self, accepted_gift_types): - for attr in accepted_gift_types.__slots__: - assert getattr(accepted_gift_types, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(accepted_gift_types)) == len(set(mro_slots(accepted_gift_types))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "unlimited_gifts": self.unlimited_gifts, diff --git a/tests/test_giveaway.py b/tests/test_giveaway.py index 1c54dd6d8f5..4ef1b042562 100644 --- a/tests/test_giveaway.py +++ b/tests/test_giveaway.py @@ -32,7 +32,6 @@ User, ) from telegram._utils.datetime import UTC, to_timestamp -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -63,11 +62,6 @@ class TestGiveawayWithoutRequest: premium_subscription_month_count = 3 prize_star_count = 99 - def test_slot_behaviour(self, giveaway): - for attr in giveaway.__slots__: - assert getattr(giveaway, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(giveaway)) == len(set(mro_slots(giveaway))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "chats": [chat.to_dict() for chat in self.chats], @@ -178,13 +172,6 @@ def giveaway_created(): class TestGiveawayCreatedWithoutRequest: prize_star_count = 99 - def test_slot_behaviour(self, giveaway_created): - for attr in giveaway_created.__slots__: - assert getattr(giveaway_created, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(giveaway_created)) == len(set(mro_slots(giveaway_created))), ( - "duplicate slot" - ) - def test_de_json(self, bot): json_dict = { "prize_star_count": self.prize_star_count, @@ -235,13 +222,6 @@ class TestGiveawayWinnersWithoutRequest: prize_description = "prize_description" prize_star_count = 99 - def test_slot_behaviour(self, giveaway_winners): - for attr in giveaway_winners.__slots__: - assert getattr(giveaway_winners, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(giveaway_winners)) == len(set(mro_slots(giveaway_winners))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "chat": self.chat.to_dict(), @@ -382,13 +362,6 @@ class TestGiveawayCompletedWithoutRequest: from_user=User(1, "user1", False), ) - def test_slot_behaviour(self, giveaway_completed): - for attr in giveaway_completed.__slots__: - assert getattr(giveaway_completed, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(giveaway_completed)) == len(set(mro_slots(giveaway_completed))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "winner_count": self.winner_count, diff --git a/tests/test_inputchecklist.py b/tests/test_inputchecklist.py index e2543562d54..ca8e6148d40 100644 --- a/tests/test_inputchecklist.py +++ b/tests/test_inputchecklist.py @@ -19,7 +19,6 @@ import pytest from telegram import Dice, InputChecklist, InputChecklistTask, MessageEntity -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -43,13 +42,6 @@ class InputChecklistTaskTestBase: class TestInputChecklistTaskWithoutRequest(InputChecklistTaskTestBase): - def test_slot_behaviour(self, input_checklist_task): - for attr in input_checklist_task.__slots__: - assert getattr(input_checklist_task, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(input_checklist_task)) == len(set(mro_slots(input_checklist_task))), ( - "duplicate slot" - ) - def test_expected_values(self, input_checklist_task): assert input_checklist_task.id == self.id assert input_checklist_task.text == self.text @@ -114,13 +106,6 @@ class InputChecklistTestBase: class TestInputChecklistWithoutRequest(InputChecklistTestBase): - def test_slot_behaviour(self, input_checklist): - for attr in input_checklist.__slots__: - assert getattr(input_checklist, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(input_checklist)) == len(set(mro_slots(input_checklist))), ( - "duplicate slot" - ) - def test_expected_values(self, input_checklist): assert input_checklist.title == self.title assert input_checklist.tasks == tuple(self.tasks) diff --git a/tests/test_keyboardbutton.py b/tests/test_keyboardbutton.py index a664b81cd8e..2f5f00ee8a7 100644 --- a/tests/test_keyboardbutton.py +++ b/tests/test_keyboardbutton.py @@ -27,7 +27,6 @@ KeyboardButtonRequestUsers, WebAppInfo, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -60,12 +59,6 @@ class KeyboardButtonTestBase: class TestKeyboardButtonWithoutRequest(KeyboardButtonTestBase): - def test_slot_behaviour(self, keyboard_button): - inst = keyboard_button - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, keyboard_button): assert keyboard_button.text == self.text assert keyboard_button.request_location == self.request_location diff --git a/tests/test_keyboardbuttonpolltype.py b/tests/test_keyboardbuttonpolltype.py index 5abcfed0aa7..9df8265cd8d 100644 --- a/tests/test_keyboardbuttonpolltype.py +++ b/tests/test_keyboardbuttonpolltype.py @@ -20,7 +20,6 @@ from telegram import KeyboardButtonPollType, Poll from telegram.constants import PollType -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -33,12 +32,6 @@ class KeyboardButtonPollTypeTestBase: class TestKeyboardButtonPollTypeWithoutRequest(KeyboardButtonPollTypeTestBase): - def test_slot_behaviour(self, keyboard_button_poll_type): - inst = keyboard_button_poll_type - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_to_dict(self, keyboard_button_poll_type): keyboard_button_poll_type_dict = keyboard_button_poll_type.to_dict() assert isinstance(keyboard_button_poll_type_dict, dict) diff --git a/tests/test_keyboardbuttonrequest.py b/tests/test_keyboardbuttonrequest.py index 8a63bb2dbad..720342d31ed 100644 --- a/tests/test_keyboardbuttonrequest.py +++ b/tests/test_keyboardbuttonrequest.py @@ -20,7 +20,6 @@ import pytest from telegram import ChatAdministratorRights, KeyboardButtonRequestChat, KeyboardButtonRequestUsers -from tests.auxil.slots import mro_slots @pytest.fixture(scope="class") @@ -41,13 +40,6 @@ class KeyboardButtonRequestUsersTestBase: class TestKeyboardButtonRequestUsersWithoutRequest(KeyboardButtonRequestUsersTestBase): - def test_slot_behaviour(self, request_users): - for attr in request_users.__slots__: - assert getattr(request_users, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(request_users)) == len(set(mro_slots(request_users))), ( - "duplicate slot" - ) - def test_to_dict(self, request_users): request_users_dict = request_users.to_dict() @@ -135,11 +127,6 @@ class KeyboardButtonRequestChatTestBase: class TestKeyboardButtonRequestChatWithoutRequest(KeyboardButtonRequestChatTestBase): - def test_slot_behaviour(self, request_chat): - for attr in request_chat.__slots__: - assert getattr(request_chat, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(request_chat)) == len(set(mro_slots(request_chat))), "duplicate slot" - def test_to_dict(self, request_chat): request_chat_dict = request_chat.to_dict() diff --git a/tests/test_keyboardbuttonrequestmanagedbot.py b/tests/test_keyboardbuttonrequestmanagedbot.py index 4f3fa95b73e..8303abe4d01 100644 --- a/tests/test_keyboardbuttonrequestmanagedbot.py +++ b/tests/test_keyboardbuttonrequestmanagedbot.py @@ -20,7 +20,6 @@ import pytest from telegram import KeyboardButtonRequestManagedBot -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -39,12 +38,6 @@ class KeyboardButtonRequestManagedBotTestBase: class TestKeyboardButtonRequestManagedBotWithoutRequest(KeyboardButtonRequestManagedBotTestBase): - def test_slot_behaviour(self, keyboard_button_request_managed_bot): - inst = keyboard_button_request_managed_bot - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "request_id": self.request_id, diff --git a/tests/test_linkpreviewoptions.py b/tests/test_linkpreviewoptions.py index 946a1f7bf6f..de187576c92 100644 --- a/tests/test_linkpreviewoptions.py +++ b/tests/test_linkpreviewoptions.py @@ -19,7 +19,6 @@ import pytest from telegram import LinkPreviewOptions -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -42,12 +41,6 @@ class LinkPreviewOptionsTestBase: class TestLinkPreviewOptionsWithoutRequest(LinkPreviewOptionsTestBase): - def test_slot_behaviour(self, link_preview_options): - a = link_preview_options - for attr in a.__slots__: - assert getattr(a, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(a)) == len(set(mro_slots(a))), "duplicate slot" - def test_to_dict(self, link_preview_options): link_preview_options_dict = link_preview_options.to_dict() diff --git a/tests/test_loginurl.py b/tests/test_loginurl.py index c98bd974404..3160fc58fb9 100644 --- a/tests/test_loginurl.py +++ b/tests/test_loginurl.py @@ -19,7 +19,6 @@ import pytest from telegram import LoginUrl -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -40,11 +39,6 @@ class LoginUrlTestBase: class TestLoginUrlWithoutRequest(LoginUrlTestBase): - def test_slot_behaviour(self, login_url): - for attr in login_url.__slots__: - assert getattr(login_url, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(login_url)) == len(set(mro_slots(login_url))), "duplicate slot" - def test_to_dict(self, login_url): login_url_dict = login_url.to_dict() diff --git a/tests/test_managedbot.py b/tests/test_managedbot.py index 9ec374c5894..771d0fd43c9 100644 --- a/tests/test_managedbot.py +++ b/tests/test_managedbot.py @@ -19,7 +19,6 @@ import pytest from telegram import ManagedBotCreated, ManagedBotUpdated, User -from tests.auxil.slots import mro_slots @pytest.fixture @@ -63,12 +62,6 @@ class ManagedBotTestBase: class TestManagedBotCreatedWithoutRequest(ManagedBotTestBase): - def test_slot_behaviour(self, managed_bot_created): - inst = managed_bot_created - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot, managed_bot_created): json_dict = { "bot": managed_bot_created.bot.to_dict(), @@ -127,12 +120,6 @@ def test_equality(self, managed_bot_created): class TestManagedBotUpdatedWithoutRequest(ManagedBotTestBase): - def test_slot_behaviour(self, managed_bot_updated): - inst = managed_bot_updated - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot, managed_bot_updated): json_dict = { "user": managed_bot_updated.user.to_dict(), diff --git a/tests/test_maybeinaccessiblemessage.py b/tests/test_maybeinaccessiblemessage.py index d92e8c99591..30a37980b0b 100644 --- a/tests/test_maybeinaccessiblemessage.py +++ b/tests/test_maybeinaccessiblemessage.py @@ -42,6 +42,7 @@ class MaybeInaccessibleMessageTestBase: class TestMaybeInaccessibleMessageWithoutRequest(MaybeInaccessibleMessageTestBase): + # tags: deprecated NEXT.VERSION def test_slot_behaviour(self, maybe_inaccessible_message): for attr in maybe_inaccessible_message.__slots__: assert getattr(maybe_inaccessible_message, attr, "err") != "err", ( diff --git a/tests/test_menubutton.py b/tests/test_menubutton.py index 8253ec33d86..a8b4d42e41d 100644 --- a/tests/test_menubutton.py +++ b/tests/test_menubutton.py @@ -27,7 +27,6 @@ WebAppInfo, ) from telegram.constants import MenuButtonType -from tests.auxil.slots import mro_slots @pytest.fixture @@ -42,12 +41,6 @@ class MenuButtonTestBase: class TestMenuButtonWithoutRequest(MenuButtonTestBase): - def test_slot_behaviour(self, menu_button): - inst = menu_button - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self, menu_button): assert type(MenuButton("default").type) is MenuButtonType assert MenuButton("unknown").type == "unknown" @@ -107,12 +100,6 @@ def menu_button_commands(): class TestMenuButtonCommandsWithoutRequest(MenuButtonTestBase): type = MenuButtonType.COMMANDS - def test_slot_behaviour(self, menu_button_commands): - inst = menu_button_commands - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): transaction_partner = MenuButtonCommands.de_json({}, offline_bot) assert transaction_partner.api_kwargs == {} @@ -145,12 +132,6 @@ def menu_button_default(): class TestMenuButtonDefaultWithoutRequest(MenuButtonTestBase): type = MenuButtonType.DEFAULT - def test_slot_behaviour(self, menu_button_default): - inst = menu_button_default - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): transaction_partner = MenuButtonDefault.de_json({}, offline_bot) assert transaction_partner.api_kwargs == {} @@ -186,12 +167,6 @@ def menu_button_web_app(): class TestMenuButtonWebAppWithoutRequest(MenuButtonTestBase): type = MenuButtonType.WEB_APP - def test_slot_behaviour(self, menu_button_web_app): - inst = menu_button_web_app - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"web_app": self.web_app.to_dict(), "text": self.text} transaction_partner = MenuButtonWebApp.de_json(json_dict, offline_bot) diff --git a/tests/test_message.py b/tests/test_message.py index 1003e3b161e..7f111729de6 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -123,7 +123,6 @@ from tests.auxil.build_messages import make_message from tests.auxil.dummy_objects import get_dummy_object_json_dict from tests.auxil.pytest_classes import PytestExtBot, PytestMessage -from tests.auxil.slots import mro_slots @pytest.fixture @@ -813,17 +812,6 @@ async def extract_message_thread_id(*args, **kwargs): ) assert message_thread_id == (message.message_thread_id if is_topic_message else None) - def test_slot_behaviour(self): - message = Message( - message_id=MessageTestBase.id_, - date=MessageTestBase.date, - chat=copy(MessageTestBase.chat), - from_user=copy(MessageTestBase.from_user), - ) - for attr in message.__slots__: - assert getattr(message, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(message)) == len(set(mro_slots(message))), "duplicate slot" - def test_all_possibilities_de_json_and_to_dict(self, offline_bot, message_params): new = Message.de_json(message_params.to_dict(), offline_bot) assert new.api_kwargs == {} diff --git a/tests/test_messageautodeletetimerchanged.py b/tests/test_messageautodeletetimerchanged.py index fc92af4fc7c..f29ba0d0e9c 100644 --- a/tests/test_messageautodeletetimerchanged.py +++ b/tests/test_messageautodeletetimerchanged.py @@ -20,18 +20,11 @@ from telegram import MessageAutoDeleteTimerChanged, VideoChatEnded from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots class TestMessageAutoDeleteTimerChangedWithoutRequest: message_auto_delete_time = dtm.timedelta(seconds=100) - def test_slot_behaviour(self): - action = MessageAutoDeleteTimerChanged(self.message_auto_delete_time) - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self): json_dict = { "message_auto_delete_time": int(self.message_auto_delete_time.total_seconds()) diff --git a/tests/test_messageentity.py b/tests/test_messageentity.py index 7341ce56c99..8b611576d6a 100644 --- a/tests/test_messageentity.py +++ b/tests/test_messageentity.py @@ -24,7 +24,6 @@ from telegram import MessageEntity, User from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import MessageEntityType -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module", params=MessageEntity.ALL_TYPES) @@ -68,12 +67,6 @@ class MessageEntityTestBase: class TestMessageEntityWithoutRequest(MessageEntityTestBase): - def test_slot_behaviour(self, message_entity): - inst = message_entity - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"type": self.type_, "offset": self.offset, "length": self.length} entity = MessageEntity.de_json(json_dict, offline_bot) diff --git a/tests/test_messageid.py b/tests/test_messageid.py index 85053d82ba9..f6b9a3b460d 100644 --- a/tests/test_messageid.py +++ b/tests/test_messageid.py @@ -18,7 +18,6 @@ import pytest from telegram import MessageId, User -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -29,11 +28,6 @@ def message_id(): class TestMessageIdWithoutRequest: m_id = 1234 - def test_slot_behaviour(self, message_id): - for attr in message_id.__slots__: - assert getattr(message_id, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(message_id)) == len(set(mro_slots(message_id))), "duplicate slot" - def test_de_json(self): json_dict = {"message_id": self.m_id} message_id = MessageId.de_json(json_dict, None) diff --git a/tests/test_messageorigin.py b/tests/test_messageorigin.py index 0666c85bf2b..05d1009e3c6 100644 --- a/tests/test_messageorigin.py +++ b/tests/test_messageorigin.py @@ -33,7 +33,6 @@ User, ) from telegram._utils.datetime import UTC, to_timestamp -from tests.auxil.slots import mro_slots ignored = ["self", "api_kwargs"] @@ -130,12 +129,6 @@ def message_origin_type(request): indirect=True, ) class TestMessageOriginTypesWithoutRequest: - def test_slot_behaviour(self, message_origin_type): - inst = message_origin_type - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json_required_args(self, offline_bot, message_origin_type): cls = message_origin_type.__class__ diff --git a/tests/test_ownedgift.py b/tests/test_ownedgift.py index ed6c6bbd9da..74f746c294f 100644 --- a/tests/test_ownedgift.py +++ b/tests/test_ownedgift.py @@ -36,7 +36,6 @@ ) from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import OwnedGiftType -from tests.auxil.slots import mro_slots @pytest.fixture @@ -102,12 +101,6 @@ class OwnedGiftTestBase: class TestOwnedGiftWithoutRequest(OwnedGiftTestBase): - def test_slot_behaviour(self, owned_gift): - inst = owned_gift - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self, owned_gift): assert type(OwnedGift("regular").type) is OwnedGiftType assert OwnedGift("unknown").type == "unknown" @@ -194,12 +187,6 @@ def owned_gift_regular(): class TestOwnedGiftRegularWithoutRequest(OwnedGiftTestBase): type = OwnedGiftType.REGULAR - def test_slot_behaviour(self, owned_gift_regular): - inst = owned_gift_regular - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "gift": self.gift.to_dict(), @@ -311,12 +298,6 @@ def owned_gift_unique(): class TestOwnedGiftUniqueWithoutRequest(OwnedGiftTestBase): type = OwnedGiftType.UNIQUE - def test_slot_behaviour(self, owned_gift_unique): - inst = owned_gift_unique - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "gift": self.unique_gift.to_dict(), @@ -433,11 +414,6 @@ class OwnedGiftsTestBase: class TestOwnedGiftsWithoutRequest(OwnedGiftsTestBase): - def test_slot_behaviour(self, owned_gifts): - for attr in owned_gifts.__slots__: - assert getattr(owned_gifts, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(owned_gifts)) == len(set(mro_slots(owned_gifts))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "total_count": self.total_count, diff --git a/tests/test_paidmedia.py b/tests/test_paidmedia.py index b6efb1b01a8..21d9ad82eda 100644 --- a/tests/test_paidmedia.py +++ b/tests/test_paidmedia.py @@ -38,7 +38,6 @@ ) from telegram.constants import PaidMediaType from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture @@ -77,12 +76,6 @@ class PaidMediaTestBase: class TestPaidMediaWithoutRequest(PaidMediaTestBase): - def test_slot_behaviour(self, paid_media): - inst = paid_media - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self, paid_media): assert type(PaidMedia("photo").type) is PaidMediaType assert PaidMedia("unknown").type == "unknown" @@ -152,12 +145,6 @@ def paid_media_photo(): class TestPaidMediaPhotoWithoutRequest(PaidMediaTestBase): type = PaidMediaType.PHOTO - def test_slot_behaviour(self, paid_media_photo): - inst = paid_media_photo - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "photo": [p.to_dict() for p in self.photo], @@ -198,12 +185,6 @@ def paid_media_video(): class TestPaidMediaVideoWithoutRequest(PaidMediaTestBase): type = PaidMediaType.VIDEO - def test_slot_behaviour(self, paid_media_video): - inst = paid_media_video - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "video": self.video.to_dict(), @@ -248,12 +229,6 @@ def paid_media_live_photo(): class TestPaidMediaLivePhotoWithoutRequest(PaidMediaTestBase): type = PaidMediaType.LIVE_PHOTO - def test_slot_behaviour(self, paid_media_live_photo): - inst = paid_media_live_photo - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "live_photo": self.live_photo.to_dict(), @@ -300,12 +275,6 @@ def paid_media_preview(): class TestPaidMediaPreviewWithoutRequest(PaidMediaTestBase): type = PaidMediaType.PREVIEW - def test_slot_behaviour(self, paid_media_preview): - inst = paid_media_preview - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "width": self.width, @@ -433,12 +402,6 @@ class PaidMediaInfoTestBase: class TestPaidMediaInfoWithoutRequest(PaidMediaInfoTestBase): - def test_slot_behaviour(self, paid_media_info): - inst = paid_media_info - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "star_count": self.star_count, @@ -472,12 +435,6 @@ class PaidMediaPurchasedTestBase: class TestPaidMediaPurchasedWithoutRequest(PaidMediaPurchasedTestBase): - def test_slot_behaviour(self, paid_media_purchased): - inst = paid_media_purchased - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, bot): json_dict = { "from": self.from_user.to_dict(), diff --git a/tests/test_paidmessagepricechanged.py b/tests/test_paidmessagepricechanged.py index 4ee0e39fb3e..d8004fda699 100644 --- a/tests/test_paidmessagepricechanged.py +++ b/tests/test_paidmessagepricechanged.py @@ -20,7 +20,6 @@ import pytest from telegram import Dice, PaidMessagePriceChanged -from tests.auxil.slots import mro_slots class PaidMessagePriceChangedTestBase: @@ -33,15 +32,6 @@ def paid_message_price_changed(): class TestPaidMessagePriceChangedWithoutRequest(PaidMessagePriceChangedTestBase): - def test_slot_behaviour(self, paid_message_price_changed): - for attr in paid_message_price_changed.__slots__: - assert getattr(paid_message_price_changed, attr, "err") != "err", ( - f"got extra slot '{attr}'" - ) - assert len(mro_slots(paid_message_price_changed)) == len( - set(mro_slots(paid_message_price_changed)) - ), "duplicate slot" - def test_to_dict(self, paid_message_price_changed): pmpc_dict = paid_message_price_changed.to_dict() assert isinstance(pmpc_dict, dict) diff --git a/tests/test_poll.py b/tests/test_poll.py index 3b65d622ffa..8c5641d8e00 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -45,7 +45,6 @@ from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import PollType from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -71,13 +70,6 @@ class InputPollOptionTestBase: class TestInputPollOptionWithoutRequest(InputPollOptionTestBase): - def test_slot_behaviour(self, input_poll_option): - for attr in input_poll_option.__slots__: - assert getattr(input_poll_option, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(input_poll_option)) == len(set(mro_slots(input_poll_option))), ( - "duplicate slot" - ) - def test_to_dict(self, input_poll_option): input_poll_option_dict = input_poll_option.to_dict() @@ -167,11 +159,6 @@ class PollMediaTestBase: class TestPollMediaWithoutRequest(PollMediaTestBase): - def test_slot_behaviour(self, poll_media): - for attr in poll_media.__slots__: - assert getattr(poll_media, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(poll_media)) == len(set(mro_slots(poll_media))), "duplicate slot" - def test_de_json(self): json_dict = { "animation": self.animation.to_dict(), @@ -262,11 +249,6 @@ class PollOptionTestBase: class TestPollOptionWithoutRequest(PollOptionTestBase): - def test_slot_behaviour(self, poll_option): - for attr in poll_option.__slots__: - assert getattr(poll_option, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(poll_option)) == len(set(mro_slots(poll_option))), "duplicate slot" - def test_de_json(self): json_dict = { "text": self.text, @@ -827,13 +809,6 @@ class PollOptionAddedTestBase: class TestPollOptionAddedWithoutRequest(PollOptionAddedTestBase): - def test_slot_behaviour(self, poll_option_added): - for attr in poll_option_added.__slots__: - assert getattr(poll_option_added, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(poll_option_added)) == len(set(mro_slots(poll_option_added))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "poll_message": self.poll_message.to_dict(), @@ -946,13 +921,6 @@ class PollOptionDeletedTestBase: class TestPollOptionDeletedWithoutRequest(PollOptionDeletedTestBase): - def test_slot_behaviour(self, poll_option_deleted): - for attr in poll_option_deleted.__slots__: - assert getattr(poll_option_deleted, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(poll_option_deleted)) == len(set(mro_slots(poll_option_deleted))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "poll_message": self.poll_message.to_dict(), diff --git a/tests/test_preparedkeyboardbutton.py b/tests/test_preparedkeyboardbutton.py index 9b62be8445a..b212ef0f265 100644 --- a/tests/test_preparedkeyboardbutton.py +++ b/tests/test_preparedkeyboardbutton.py @@ -19,7 +19,6 @@ import pytest from telegram import PreparedKeyboardButton -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -32,12 +31,6 @@ class PreparedKeyboardButtonTestBase: class TestPreparedKeyboardButtonWithoutRequest(PreparedKeyboardButtonTestBase): - def test_slot_behaviour(self, prepared_keyboard_button): - inst = prepared_keyboard_button - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "id": self.id, diff --git a/tests/test_proximityalerttriggered.py b/tests/test_proximityalerttriggered.py index c8464b284c3..b96fad3571d 100644 --- a/tests/test_proximityalerttriggered.py +++ b/tests/test_proximityalerttriggered.py @@ -19,7 +19,6 @@ import pytest from telegram import BotCommand, ProximityAlertTriggered, User -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -38,12 +37,6 @@ class ProximityAlertTriggeredTestBase: class TestProximityAlertTriggeredWithoutRequest(ProximityAlertTriggeredTestBase): - def test_slot_behaviour(self, proximity_alert_triggered): - inst = proximity_alert_triggered - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "traveler": self.traveler.to_dict(), diff --git a/tests/test_reaction.py b/tests/test_reaction.py index be64ae17a83..fbc4a7d1841 100644 --- a/tests/test_reaction.py +++ b/tests/test_reaction.py @@ -30,7 +30,6 @@ constants, ) from telegram.constants import ReactionEmoji -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -45,12 +44,6 @@ class ReactionTypeTestBase: class TestReactionTypeWithoutRequest(ReactionTypeTestBase): - def test_slot_behaviour(self, reaction_type): - inst = reaction_type - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_type_enum_conversion(self): assert type(ReactionType("emoji").type) is constants.ReactionType assert ReactionType("unknown").type == "unknown" @@ -97,12 +90,6 @@ def reaction_type_emoji(): class TestReactionTypeEmojiWithoutRequest(ReactionTypeTestBase): type = constants.ReactionType.EMOJI - def test_slot_behaviour(self, reaction_type_emoji): - inst = reaction_type_emoji - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"emoji": self.emoji} reaction_type_emoji = ReactionTypeEmoji.de_json(json_dict, offline_bot) @@ -142,12 +129,6 @@ def reaction_type_custom_emoji(): class TestReactionTypeCustomEmojiWithoutRequest(ReactionTypeTestBase): type = constants.ReactionType.CUSTOM_EMOJI - def test_slot_behaviour(self, reaction_type_custom_emoji): - inst = reaction_type_custom_emoji - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"custom_emoji_id": self.custom_emoji_id} reaction_type_custom_emoji = ReactionTypeCustomEmoji.de_json(json_dict, offline_bot) @@ -188,12 +169,6 @@ def reaction_type_paid(): class TestReactionTypePaidWithoutRequest(ReactionTypeTestBase): type = constants.ReactionType.PAID - def test_slot_behaviour(self, reaction_type_paid): - inst = reaction_type_paid - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {} reaction_type_paid = ReactionTypePaid.de_json(json_dict, offline_bot) @@ -218,13 +193,6 @@ class TestReactionCountWithoutRequest: type = ReactionTypeEmoji(ReactionEmoji.THUMBS_UP) total_count = 42 - def test_slot_behaviour(self, reaction_count): - for attr in reaction_count.__slots__: - assert getattr(reaction_count, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(reaction_count)) == len(set(mro_slots(reaction_count))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "type": self.type.to_dict(), diff --git a/tests/test_reply.py b/tests/test_reply.py index fa544845522..a4235348eb0 100644 --- a/tests/test_reply.py +++ b/tests/test_reply.py @@ -38,7 +38,6 @@ TextQuote, User, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -88,13 +87,6 @@ class ExternalReplyInfoTestBase: class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase): - def test_slot_behaviour(self, external_reply_info): - for attr in external_reply_info.__slots__: - assert getattr(external_reply_info, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(external_reply_info)) == len(set(mro_slots(external_reply_info))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "origin": self.origin.to_dict(), @@ -172,11 +164,6 @@ class TextQuoteTestBase: class TestTextQuoteWithoutRequest(TextQuoteTestBase): - def test_slot_behaviour(self, text_quote): - for attr in text_quote.__slots__: - assert getattr(text_quote, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(text_quote)) == len(set(mro_slots(text_quote))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "text": self.text, @@ -254,13 +241,6 @@ class ReplyParametersTestBase: class TestReplyParametersWithoutRequest(ReplyParametersTestBase): - def test_slot_behaviour(self, reply_parameters): - for attr in reply_parameters.__slots__: - assert getattr(reply_parameters, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(reply_parameters)) == len(set(mro_slots(reply_parameters))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "message_id": self.message_id, diff --git a/tests/test_replykeyboardmarkup.py b/tests/test_replykeyboardmarkup.py index 19750cc41bd..87a2ad70084 100644 --- a/tests/test_replykeyboardmarkup.py +++ b/tests/test_replykeyboardmarkup.py @@ -20,7 +20,6 @@ import pytest from telegram import InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -43,12 +42,6 @@ class ReplyKeyboardMarkupTestBase: class TestReplyKeyboardMarkupWithoutRequest(ReplyKeyboardMarkupTestBase): - def test_slot_behaviour(self, reply_keyboard_markup): - inst = reply_keyboard_markup - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, reply_keyboard_markup): assert isinstance(reply_keyboard_markup.keyboard, tuple) assert all(isinstance(row, tuple) for row in reply_keyboard_markup.keyboard) diff --git a/tests/test_replykeyboardremove.py b/tests/test_replykeyboardremove.py index 4c5ada7736c..829b0b7ae3a 100644 --- a/tests/test_replykeyboardremove.py +++ b/tests/test_replykeyboardremove.py @@ -19,7 +19,6 @@ import pytest from telegram import ReplyKeyboardRemove -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -33,12 +32,6 @@ class ReplyKeyboardRemoveTestBase: class TestReplyKeyboardRemoveWithoutRequest(ReplyKeyboardRemoveTestBase): - def test_slot_behaviour(self, reply_keyboard_remove): - inst = reply_keyboard_remove - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, reply_keyboard_remove): assert reply_keyboard_remove.remove_keyboard == self.remove_keyboard assert reply_keyboard_remove.selective == self.selective diff --git a/tests/test_sentguestmessage.py b/tests/test_sentguestmessage.py index 0208342fb57..7d1054eebaf 100644 --- a/tests/test_sentguestmessage.py +++ b/tests/test_sentguestmessage.py @@ -20,7 +20,6 @@ import pytest from telegram import SentGuestMessage -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -33,12 +32,6 @@ class SentGuestMessageTestBase: class TestSentGuestMessageWithoutRequest(SentGuestMessageTestBase): - def test_slot_behaviour(self, sent_guest_message): - inst = sent_guest_message - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_to_dict(self, sent_guest_message): sent_guest_message_dict = sent_guest_message.to_dict() diff --git a/tests/test_sentwebappmessage.py b/tests/test_sentwebappmessage.py index b820ae72892..13cbd8c2ace 100644 --- a/tests/test_sentwebappmessage.py +++ b/tests/test_sentwebappmessage.py @@ -20,7 +20,6 @@ import pytest from telegram import SentWebAppMessage -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -33,12 +32,6 @@ class SentWebAppMessageTestBase: class TestSentWebAppMessageWithoutRequest(SentWebAppMessageTestBase): - def test_slot_behaviour(self, sent_web_app_message): - inst = sent_web_app_message - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_to_dict(self, sent_web_app_message): sent_web_app_message_dict = sent_web_app_message.to_dict() diff --git a/tests/test_shared.py b/tests/test_shared.py index a35e5f4ee6c..1c7d0855536 100644 --- a/tests/test_shared.py +++ b/tests/test_shared.py @@ -20,7 +20,6 @@ import pytest from telegram import ChatShared, PhotoSize, SharedUser, UsersShared -from tests.auxil.slots import mro_slots @pytest.fixture(scope="class") @@ -35,11 +34,6 @@ class UsersSharedTestBase: class TestUsersSharedWithoutRequest(UsersSharedTestBase): - def test_slot_behaviour(self, users_shared): - for attr in users_shared.__slots__: - assert getattr(users_shared, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(users_shared)) == len(set(mro_slots(users_shared))), "duplicate slot" - def test_to_dict(self, users_shared): users_shared_dict = users_shared.to_dict() @@ -94,11 +88,6 @@ class ChatSharedTestBase: class TestChatSharedWithoutRequest(ChatSharedTestBase): - def test_slot_behaviour(self, chat_shared): - for attr in chat_shared.__slots__: - assert getattr(chat_shared, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(chat_shared)) == len(set(mro_slots(chat_shared))), "duplicate slot" - def test_to_dict(self, chat_shared): chat_shared_dict = chat_shared.to_dict() @@ -167,11 +156,6 @@ class SharedUserTestBase: class TestSharedUserWithoutRequest(SharedUserTestBase): - def test_slot_behaviour(self, shared_user): - for attr in shared_user.__slots__: - assert getattr(shared_user, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(shared_user)) == len(set(mro_slots(shared_user))), "duplicate slot" - def test_to_dict(self, shared_user): shared_user_dict = shared_user.to_dict() diff --git a/tests/test_story.py b/tests/test_story.py index a1472a60aef..1f63957000d 100644 --- a/tests/test_story.py +++ b/tests/test_story.py @@ -24,7 +24,6 @@ check_shortcut_call, check_shortcut_signature, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -40,11 +39,6 @@ class StoryTestBase: class TestStoryWithoutRequest(StoryTestBase): - def test_slot_behaviour(self, story): - for attr in story.__slots__: - assert getattr(story, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(story)) == len(set(mro_slots(story))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"chat": self.chat.to_dict(), "id": self.id} story = Story.de_json(json_dict, offline_bot) diff --git a/tests/test_storyarea.py b/tests/test_storyarea.py index fd78c14c318..f1938eae953 100644 --- a/tests/test_storyarea.py +++ b/tests/test_storyarea.py @@ -34,7 +34,6 @@ StoryAreaTypeWeather, ) from telegram.constants import StoryAreaTypeType -from tests.auxil.slots import mro_slots @pytest.fixture @@ -59,12 +58,6 @@ class StoryAreaPositionTestBase: class TestStoryAreaPositionWithoutRequest(StoryAreaPositionTestBase): - def test_slot_behaviour(self, story_area_position): - inst = story_area_position - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, story_area_position): assert story_area_position.x_percentage == self.x_percentage assert story_area_position.y_percentage == self.y_percentage @@ -130,12 +123,6 @@ class LocationAddressTestBase: class TestLocationAddressWithoutRequest(LocationAddressTestBase): - def test_slot_behaviour(self, location_address): - inst = location_address - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, location_address): assert location_address.country_code == self.country_code assert location_address.state == self.state @@ -186,12 +173,6 @@ class StoryAreaTestBase: class TestStoryAreaWithoutRequest(StoryAreaTestBase): - def test_slot_behaviour(self, story_area): - inst = story_area - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, story_area): assert story_area.position == self.position assert story_area.type == self.type @@ -243,12 +224,6 @@ class StoryAreaTypeTestBase: class TestStoryAreaTypeWithoutRequest(StoryAreaTypeTestBase): - def test_slot_behaviour(self, story_area_type): - inst = story_area_type - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, story_area_type): assert story_area_type.type == self.type @@ -287,12 +262,6 @@ def story_area_type_location(): class TestStoryAreaTypeLocationWithoutRequest(StoryAreaTypeTestBase): type = StoryAreaTypeType.LOCATION - def test_slot_behaviour(self, story_area_type_location): - inst = story_area_type_location - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, story_area_type_location): assert story_area_type_location.type == self.type assert story_area_type_location.latitude == self.latitude @@ -336,12 +305,6 @@ def story_area_type_suggested_reaction(): class TestStoryAreaTypeSuggestedReactionWithoutRequest(StoryAreaTypeTestBase): type = StoryAreaTypeType.SUGGESTED_REACTION - def test_slot_behaviour(self, story_area_type_suggested_reaction): - inst = story_area_type_suggested_reaction - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, story_area_type_suggested_reaction): assert story_area_type_suggested_reaction.type == self.type assert story_area_type_suggested_reaction.reaction_type == self.reaction_type @@ -383,12 +346,6 @@ def story_area_type_link(): class TestStoryAreaTypeLinkWithoutRequest(StoryAreaTypeTestBase): type = StoryAreaTypeType.LINK - def test_slot_behaviour(self, story_area_type_link): - inst = story_area_type_link - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, story_area_type_link): assert story_area_type_link.type == self.type assert story_area_type_link.url == self.url @@ -428,12 +385,6 @@ def story_area_type_weather(): class TestStoryAreaTypeWeatherWithoutRequest(StoryAreaTypeTestBase): type = StoryAreaTypeType.WEATHER - def test_slot_behaviour(self, story_area_type_weather): - inst = story_area_type_weather - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, story_area_type_weather): assert story_area_type_weather.type == self.type assert story_area_type_weather.temperature == self.temperature @@ -475,12 +426,6 @@ def story_area_type_unique_gift(): class TestStoryAreaTypeUniqueGiftWithoutRequest(StoryAreaTypeTestBase): type = StoryAreaTypeType.UNIQUE_GIFT - def test_slot_behaviour(self, story_area_type_unique_gift): - inst = story_area_type_unique_gift - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, story_area_type_unique_gift): assert story_area_type_unique_gift.type == self.type assert story_area_type_unique_gift.name == self.name diff --git a/tests/test_suggestedpost.py b/tests/test_suggestedpost.py index b3bc89568ca..99604fb8e36 100644 --- a/tests/test_suggestedpost.py +++ b/tests/test_suggestedpost.py @@ -37,7 +37,6 @@ ) from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import SuggestedPostInfoState -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -54,15 +53,6 @@ class SuggestedPostParametersTestBase: class TestSuggestedPostParametersWithoutRequest(SuggestedPostParametersTestBase): - def test_slot_behaviour(self, suggested_post_parameters): - for attr in suggested_post_parameters.__slots__: - assert getattr(suggested_post_parameters, attr, "err") != "err", ( - f"got extra slot '{attr}'" - ) - assert len(mro_slots(suggested_post_parameters)) == len( - set(mro_slots(suggested_post_parameters)) - ), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "price": self.price.to_dict(), @@ -134,13 +124,6 @@ class SuggestedPostInfoTestBase: class TestSuggestedPostInfoWithoutRequest(SuggestedPostInfoTestBase): - def test_slot_behaviour(self, suggested_post_info): - for attr in suggested_post_info.__slots__: - assert getattr(suggested_post_info, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(suggested_post_info)) == len(set(mro_slots(suggested_post_info))), ( - "duplicate slot" - ) - def test_type_enum_conversion(self): assert type(SuggestedPostInfo("pending").state) is SuggestedPostInfoState assert SuggestedPostInfo("unknown").state == "unknown" @@ -216,13 +199,6 @@ class SuggestedPostPriceTestBase: class TestSuggestedPostPriceWithoutRequest(SuggestedPostPriceTestBase): - def test_slot_behaviour(self, suggested_post_price): - for attr in suggested_post_price.__slots__: - assert getattr(suggested_post_price, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(suggested_post_price)) == len(set(mro_slots(suggested_post_price))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "currency": self.currency, @@ -270,15 +246,6 @@ class SuggestedPostDeclinedTestBase: class TestSuggestedPostDeclinedWithoutRequest(SuggestedPostDeclinedTestBase): - def test_slot_behaviour(self, suggested_post_declined): - for attr in suggested_post_declined.__slots__: - assert getattr(suggested_post_declined, attr, "err") != "err", ( - f"got extra slot '{attr}'" - ) - assert len(mro_slots(suggested_post_declined)) == len( - set(mro_slots(suggested_post_declined)) - ), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "suggested_post_message": self.suggested_post_message.to_dict(), @@ -332,13 +299,6 @@ class SuggestedPostPaidTestBase: class TestSuggestedPostPaidWithoutRequest(SuggestedPostPaidTestBase): - def test_slot_behaviour(self, suggested_post_paid): - for attr in suggested_post_paid.__slots__: - assert getattr(suggested_post_paid, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(suggested_post_paid)) == len(set(mro_slots(suggested_post_paid))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "suggested_post_message": self.suggested_post_message.to_dict(), @@ -402,15 +362,6 @@ class SuggestedPostRefundedTestBase: class TestSuggestedPostRefundedWithoutRequest(SuggestedPostRefundedTestBase): - def test_slot_behaviour(self, suggested_post_refunded): - for attr in suggested_post_refunded.__slots__: - assert getattr(suggested_post_refunded, attr, "err") != "err", ( - f"got extra slot '{attr}'" - ) - assert len(mro_slots(suggested_post_refunded)) == len( - set(mro_slots(suggested_post_refunded)) - ), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "suggested_post_message": self.suggested_post_message.to_dict(), @@ -464,15 +415,6 @@ class SuggestedPostApprovedTestBase: class TestSuggestedPostApprovedWithoutRequest(SuggestedPostApprovedTestBase): - def test_slot_behaviour(self, suggested_post_approved): - for attr in suggested_post_approved.__slots__: - assert getattr(suggested_post_approved, attr, "err") != "err", ( - f"got extra slot '{attr}'" - ) - assert len(mro_slots(suggested_post_approved)) == len( - set(mro_slots(suggested_post_approved)) - ), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "send_date": to_timestamp(self.send_date), @@ -552,15 +494,6 @@ class SuggestedPostApprovalFailedTestBase: class TestSuggestedPostApprovalFailedWithoutRequest(SuggestedPostApprovalFailedTestBase): - def test_slot_behaviour(self, suggested_post_approval_failed): - for attr in suggested_post_approval_failed.__slots__: - assert getattr(suggested_post_approval_failed, attr, "err") != "err", ( - f"got extra slot '{attr}'" - ) - assert len(mro_slots(suggested_post_approval_failed)) == len( - set(mro_slots(suggested_post_approval_failed)) - ), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "price": self.price.to_dict(), diff --git a/tests/test_switchinlinequerychosenchat.py b/tests/test_switchinlinequerychosenchat.py index 10c5de3e33b..e1b99091463 100644 --- a/tests/test_switchinlinequerychosenchat.py +++ b/tests/test_switchinlinequerychosenchat.py @@ -20,7 +20,6 @@ import pytest from telegram import SwitchInlineQueryChosenChat -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -43,12 +42,6 @@ class SwitchInlineQueryChosenChatTestBase: class TestSwitchInlineQueryChosenChat(SwitchInlineQueryChosenChatTestBase): - def test_slot_behaviour(self, switch_inline_query_chosen_chat): - inst = switch_inline_query_chosen_chat - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self, switch_inline_query_chosen_chat): assert switch_inline_query_chosen_chat.query == self.query assert switch_inline_query_chosen_chat.allow_user_chats == self.allow_user_chats diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index 077266da9b4..d4dd8578b38 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -43,7 +43,6 @@ from telegram.ext import PicklePersistence from telegram.warnings import PTBUserWarning from tests.auxil.files import data_file -from tests.auxil.slots import mro_slots def all_subclasses(cls): @@ -504,12 +503,6 @@ def __init__(self): assert "default_none" not in to_dict assert to_dict["default_false"] is False - def test_slot_behaviour(self): - inst = TelegramObject() - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_meaningless_comparison(self, recwarn): expected_warning = "Objects of type TGO can not be meaningfully tested for equivalence." diff --git a/tests/test_uniquegift.py b/tests/test_uniquegift.py index 506b05ec6cd..4134a7bf2f7 100644 --- a/tests/test_uniquegift.py +++ b/tests/test_uniquegift.py @@ -35,7 +35,6 @@ ) from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import UniqueGiftInfoOrigin, UniqueGiftModelRarity -from tests.auxil.slots import mro_slots @pytest.fixture @@ -60,13 +59,6 @@ class UniqueGiftColorsTestBase: class TestUniqueGiftColorsWithoutRequest(UniqueGiftColorsTestBase): - def test_slot_behaviour(self, unique_gift_colors): - for attr in unique_gift_colors.__slots__: - assert getattr(unique_gift_colors, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(unique_gift_colors)) == len(set(mro_slots(unique_gift_colors))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "model_custom_emoji_id": self.model_custom_emoji_id, @@ -177,11 +169,6 @@ class UniqueGiftTestBase: class TestUniqueGiftWithoutRequest(UniqueGiftTestBase): - def test_slot_behaviour(self, unique_gift): - for attr in unique_gift.__slots__: - assert getattr(unique_gift, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(unique_gift)) == len(set(mro_slots(unique_gift))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "gift_id": self.gift_id, @@ -282,13 +269,6 @@ class UniqueGiftModelTestBase: class TestUniqueGiftModelWithoutRequest(UniqueGiftModelTestBase): - def test_slot_behaviour(self, unique_gift_model): - for attr in unique_gift_model.__slots__: - assert getattr(unique_gift_model, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(unique_gift_model)) == len(set(mro_slots(unique_gift_model))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "name": self.name, @@ -346,13 +326,6 @@ class UniqueGiftSymbolTestBase: class TestUniqueGiftSymbolWithoutRequest(UniqueGiftSymbolTestBase): - def test_slot_behaviour(self, unique_gift_symbol): - for attr in unique_gift_symbol.__slots__: - assert getattr(unique_gift_symbol, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(unique_gift_symbol)) == len(set(mro_slots(unique_gift_symbol))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "name": self.name, @@ -403,13 +376,6 @@ class UniqueGiftBackdropTestBase: class TestUniqueGiftBackdropWithoutRequest(UniqueGiftBackdropTestBase): - def test_slot_behaviour(self, unique_gift_backdrop): - for attr in unique_gift_backdrop.__slots__: - assert getattr(unique_gift_backdrop, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(unique_gift_backdrop)) == len(set(mro_slots(unique_gift_backdrop))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "name": self.name, @@ -462,15 +428,6 @@ class UniqueGiftBackdropColorsTestBase: class TestUniqueGiftBackdropColorsWithoutRequest(UniqueGiftBackdropColorsTestBase): - def test_slot_behaviour(self, unique_gift_backdrop_colors): - for attr in unique_gift_backdrop_colors.__slots__: - assert getattr(unique_gift_backdrop_colors, attr, "err") != "err", ( - f"got extra slot '{attr}'" - ) - assert len(mro_slots(unique_gift_backdrop_colors)) == len( - set(mro_slots(unique_gift_backdrop_colors)) - ), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = { "center_color": self.center_color, @@ -562,13 +519,6 @@ class UniqueGiftInfoTestBase: class TestUniqueGiftInfoWithoutRequest(UniqueGiftInfoTestBase): - def test_slot_behaviour(self, unique_gift_info): - for attr in unique_gift_info.__slots__: - assert getattr(unique_gift_info, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(unique_gift_info)) == len(set(mro_slots(unique_gift_info))), ( - "duplicate slot" - ) - def test_de_json(self, offline_bot): json_dict = { "gift": self.gift.to_dict(), diff --git a/tests/test_update.py b/tests/test_update.py index cbaa1d4b66a..dbde30a3205 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -56,7 +56,6 @@ ) from telegram._utils.datetime import from_timestamp from telegram.warnings import PTBUserWarning -from tests.auxil.slots import mro_slots message = Message( 1, @@ -265,12 +264,6 @@ class UpdateTestBase: class TestUpdateWithoutRequest(UpdateTestBase): - def test_slot_behaviour(self): - update = Update(self.update_id) - for attr in update.__slots__: - assert getattr(update, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(update)) == len(set(mro_slots(update))), "duplicate slot" - @pytest.mark.parametrize("paramdict", argvalues=params, ids=ids) def test_de_json(self, offline_bot, paramdict): json_dict = {"update_id": self.update_id} diff --git a/tests/test_user.py b/tests/test_user.py index 197dbbe3967..f67bfed2125 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -25,7 +25,6 @@ check_shortcut_call, check_shortcut_signature, ) -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -98,11 +97,6 @@ class UserTestBase: class TestUserWithoutRequest(UserTestBase): - def test_slot_behaviour(self, user): - for attr in user.__slots__: - assert getattr(user, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(user)) == len(set(mro_slots(user))), "duplicate slot" - def test_de_json(self, json_dict, offline_bot): user = User.de_json(json_dict, offline_bot) assert user.api_kwargs == {} diff --git a/tests/test_userprofileaudios.py b/tests/test_userprofileaudios.py index 8ea80fdb7ed..dab31b95fec 100644 --- a/tests/test_userprofileaudios.py +++ b/tests/test_userprofileaudios.py @@ -17,7 +17,6 @@ # 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 telegram import Audio, UserProfileAudios -from tests.auxil.slots import mro_slots class UserProfileAudiosTestBase: @@ -29,12 +28,6 @@ class UserProfileAudiosTestBase: class TestUserProfileAudiosWithoutRequest(UserProfileAudiosTestBase): - def test_slot_behaviour(self): - inst = UserProfileAudios(self.total_count, self.audios) - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"total_count": 2, "audios": [x.to_dict() for x in self.audios]} user_profile_audios = UserProfileAudios.de_json(json_dict, offline_bot) diff --git a/tests/test_userprofilephotos.py b/tests/test_userprofilephotos.py index 973d5158841..387e2e72169 100644 --- a/tests/test_userprofilephotos.py +++ b/tests/test_userprofilephotos.py @@ -17,7 +17,6 @@ # 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 telegram import PhotoSize, UserProfilePhotos -from tests.auxil.slots import mro_slots class UserProfilePhotosTestBase: @@ -35,12 +34,6 @@ class UserProfilePhotosTestBase: class TestUserProfilePhotosWithoutRequest(UserProfilePhotosTestBase): - def test_slot_behaviour(self): - inst = UserProfilePhotos(self.total_count, self.photos) - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_de_json(self, offline_bot): json_dict = {"total_count": 2, "photos": [[y.to_dict() for y in x] for x in self.photos]} user_profile_photos = UserProfilePhotos.de_json(json_dict, offline_bot) diff --git a/tests/test_userrating.py b/tests/test_userrating.py index effcdebc68b..d656048015f 100644 --- a/tests/test_userrating.py +++ b/tests/test_userrating.py @@ -20,7 +20,6 @@ import pytest from telegram import BotCommand, UserRating -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -41,11 +40,6 @@ class UserRatingTestBase: class TestUserRatingWithoutRequest(UserRatingTestBase): - def test_slot_behaviour(self, user_rating): - for attr in user_rating.__slots__: - assert getattr(user_rating, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(user_rating)) == len(set(mro_slots(user_rating))), "duplicate slot" - def test_de_json_with_next(self, offline_bot): json_dict = { "level": self.level, diff --git a/tests/test_videochat.py b/tests/test_videochat.py index 325c8ac8e5e..fe805c66cce 100644 --- a/tests/test_videochat.py +++ b/tests/test_videochat.py @@ -29,7 +29,6 @@ ) from telegram._utils.datetime import UTC, to_timestamp from telegram.warnings import PTBDeprecationWarning -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -43,12 +42,6 @@ def user2(): class TestVideoChatStartedWithoutRequest: - def test_slot_behaviour(self): - action = VideoChatStarted() - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self): video_chat_started = VideoChatStarted.de_json({}, None) assert video_chat_started.api_kwargs == {} @@ -63,12 +56,6 @@ def test_to_dict(self): class TestVideoChatEndedWithoutRequest: duration = dtm.timedelta(seconds=100) - def test_slot_behaviour(self): - action = VideoChatEnded(8) - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self): json_dict = {"duration": int(self.duration.total_seconds())} video_chat_ended = VideoChatEnded.de_json(json_dict, None) @@ -123,12 +110,6 @@ def test_equality(self): class TestVideoChatParticipantsInvitedWithoutRequest: - def test_slot_behaviour(self, user1): - action = VideoChatParticipantsInvited([user1]) - for attr in action.__slots__: - assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot" - def test_de_json(self, user1, user2, offline_bot): json_data = {"users": [user1.to_dict(), user2.to_dict()]} video_chat_participants = VideoChatParticipantsInvited.de_json(json_data, offline_bot) @@ -176,12 +157,6 @@ def test_equality(self, user1, user2): class TestVideoChatScheduledWithoutRequest: start_date = dtm.datetime.now(dtm.timezone.utc) - def test_slot_behaviour(self): - inst = VideoChatScheduled(self.start_date) - for attr in inst.__slots__: - assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_expected_values(self): assert VideoChatScheduled(self.start_date).start_date == self.start_date diff --git a/tests/test_webappdata.py b/tests/test_webappdata.py index a52e49275a9..eeb59a508d2 100644 --- a/tests/test_webappdata.py +++ b/tests/test_webappdata.py @@ -20,7 +20,6 @@ import pytest from telegram import WebAppData -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -34,11 +33,6 @@ class WebAppDataTestBase: class TestWebAppDataWithoutRequest(WebAppDataTestBase): - def test_slot_behaviour(self, web_app_data): - for attr in web_app_data.__slots__: - assert getattr(web_app_data, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(web_app_data)) == len(set(mro_slots(web_app_data))), "duplicate slot" - def test_to_dict(self, web_app_data): web_app_data_dict = web_app_data.to_dict() diff --git a/tests/test_webappinfo.py b/tests/test_webappinfo.py index d831945696e..2846d46a3a6 100644 --- a/tests/test_webappinfo.py +++ b/tests/test_webappinfo.py @@ -20,7 +20,6 @@ import pytest from telegram import WebAppInfo -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -33,11 +32,6 @@ class WebAppInfoTestBase: class TestWebAppInfoWithoutRequest(WebAppInfoTestBase): - def test_slot_behaviour(self, web_app_info): - for attr in web_app_info.__slots__: - assert getattr(web_app_info, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(web_app_info)) == len(set(mro_slots(web_app_info))), "duplicate slot" - def test_to_dict(self, web_app_info): web_app_info_dict = web_app_info.to_dict() diff --git a/tests/test_webhookinfo.py b/tests/test_webhookinfo.py index 857fc942704..ee6f5b08c7e 100644 --- a/tests/test_webhookinfo.py +++ b/tests/test_webhookinfo.py @@ -23,7 +23,6 @@ from telegram import LoginUrl, WebhookInfo from telegram._utils.datetime import UTC, from_timestamp -from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") @@ -52,11 +51,6 @@ class WebhookInfoTestBase: class TestWebhookInfoWithoutRequest(WebhookInfoTestBase): - def test_slot_behaviour(self, webhook_info): - for attr in webhook_info.__slots__: - assert getattr(webhook_info, attr, "err") != "err", f"got extra slot '{attr}'" - assert len(mro_slots(webhook_info)) == len(set(mro_slots(webhook_info))), "duplicate slot" - def test_to_dict(self, webhook_info): webhook_info_dict = webhook_info.to_dict() From cf8bea063f3f9dc63e72eb28f1a24f433022d010 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 13 Sep 2026 22:01:53 +0300 Subject: [PATCH 12/31] Add missing default `None` to optional fields --- src/telegram/_chatmember.py | 4 ++-- src/telegram/_loginurl.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/telegram/_chatmember.py b/src/telegram/_chatmember.py index 9fbea7102ab..d5df16997d6 100644 --- a/src/telegram/_chatmember.py +++ b/src/telegram/_chatmember.py @@ -353,8 +353,8 @@ class ChatMemberMember(ChatMember): # Attribute only (init=False) status: str = tg_field(init=False, default=ChatMember.MEMBER) - until_date: dtm.datetime | None = tg_field() - tag: str | None = tg_field() + until_date: dtm.datetime | None = tg_field(default=None) + tag: str | None = tg_field(default=None) @tg_dataclass() diff --git a/src/telegram/_loginurl.py b/src/telegram/_loginurl.py index 9f9276cd717..2d01f676c44 100644 --- a/src/telegram/_loginurl.py +++ b/src/telegram/_loginurl.py @@ -84,6 +84,6 @@ class LoginUrl(TelegramObject): # Required url: str = tg_field(compare=True) # Optional - forward_text: str | None = tg_field() - bot_username: str | None = tg_field() - request_write_access: bool | None = tg_field() + forward_text: str | None = tg_field(default=None) + bot_username: str | None = tg_field(default=None) + request_write_access: bool | None = tg_field(default=None) From edb225eec375bbfc81b5508d3dbd923b6625d1e5 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Sun, 13 Sep 2026 22:35:06 +0300 Subject: [PATCH 13/31] Fix some enum copy-paste typos --- src/telegram/_chatboost.py | 2 +- src/telegram/_chatmember.py | 2 +- src/telegram/_paidmedia.py | 2 +- src/telegram/_reaction.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/telegram/_chatboost.py b/src/telegram/_chatboost.py index 879a94e2b50..5f4b03f1567 100644 --- a/src/telegram/_chatboost.py +++ b/src/telegram/_chatboost.py @@ -142,7 +142,7 @@ class ChatBoostSourceGiftCode(ChatBoostSource): """ # Attribute only (init=False) - source: str = tg_field(init=False, default=ChatBoostSource.PREMIUM) + source: str = tg_field(init=False, default=ChatBoostSource.GIFT_CODE) user: User = tg_field() diff --git a/src/telegram/_chatmember.py b/src/telegram/_chatmember.py index d5df16997d6..bf7aaf59f7f 100644 --- a/src/telegram/_chatmember.py +++ b/src/telegram/_chatmember.py @@ -98,7 +98,7 @@ class ChatMember(TelegramObject): @staticmethod def _status_converter(value: str) -> str: - return enum.get_member(constants.BotCommandScopeType, value, value) + return enum.get_member(constants.ChatMemberStatus, value, value) # Required by all subclasses user: User = tg_field(compare=True) diff --git a/src/telegram/_paidmedia.py b/src/telegram/_paidmedia.py index 00dcf6144ec..b839d45ac3b 100644 --- a/src/telegram/_paidmedia.py +++ b/src/telegram/_paidmedia.py @@ -204,7 +204,7 @@ class PaidMediaLivePhoto(PaidMedia): """ # Attribute only (init=False) - type: str = tg_field(compare=True, init=False, default=PaidMedia.VIDEO) + type: str = tg_field(compare=True, init=False, default=PaidMedia.LIVE_PHOTO) live_photo: LivePhoto = tg_field(compare=True) diff --git a/src/telegram/_reaction.py b/src/telegram/_reaction.py index 7bf39c4ef7f..60b91840d47 100644 --- a/src/telegram/_reaction.py +++ b/src/telegram/_reaction.py @@ -71,7 +71,7 @@ class ReactionType(TelegramObject): def _type_converter( value: Literal["emoji", "custom_emoji", "paid"] | constants.ReactionType, ) -> str: - return enum.get_member(constants.MessageOriginType, value, value) + return enum.get_member(constants.ReactionType, value, value) # Required by all subclasses type: str = tg_field(converter=_type_converter) From 9e6d55874725a4135063b21d1bc91b3b5787f47f Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:15:20 +0300 Subject: [PATCH 14/31] Override dataclasses generated `__hash__()` dataclasses generated `hash()` does not include `self.__class__` so this is to preserve old behaviour of different classes, having same field level hashes, not being equal --- src/telegram/_utils/dataclass.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/telegram/_utils/dataclass.py b/src/telegram/_utils/dataclass.py index 188bc27be46..0fcd9b862ee 100644 --- a/src/telegram/_utils/dataclass.py +++ b/src/telegram/_utils/dataclass.py @@ -158,6 +158,21 @@ def decorate(cls: type[_T]) -> type[_T]: match_args=False, eq=eq, ) - return _apply_aliases(transformed_cls) + transformed_cls = _apply_aliases(transformed_cls) + + if eq: + compare_fields = tuple( + dataclass_field.name + for dataclass_field in dataclasses.fields(transformed_cls) + if dataclass_field.compare + ) + + def __hash__(self: object) -> int: + values = tuple(getattr(self, name) for name in compare_fields) + return hash((self.__class__, values)) + + transformed_cls.__hash__ = __hash__ # type: ignore[method-assign] + + return transformed_cls return decorate From 2d88fd0903a887aed25cddc3ee17600292bb00c3 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:48:19 +0300 Subject: [PATCH 15/31] Handle annotation replacements in generated` __init__` --- src/telegram/_telegramobject.py | 8 +-- src/telegram/_utils/dataclass.py | 113 ++++++++++++++++++++++--------- src/telegram/_utils/de_json.py | 4 +- 3 files changed, 87 insertions(+), 38 deletions(-) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 487ce1f6951..97119483290 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -23,7 +23,7 @@ import datetime as dtm import inspect import json -from collections.abc import Iterator, Mapping, Sized +from collections.abc import Iterator, Mapping, Sequence, Sized from copy import deepcopy from itertools import chain from types import MappingProxyType @@ -379,15 +379,13 @@ def _build_plan(cls) -> dict[str, Any]: origin = get_origin(inner) if param.default is inspect.Parameter.empty: - # We check for `is tuple` instead of `Sequence` - # because the inspected annotation returns the type declared in the field - compatibility_defaults[name] = () if origin is tuple else None + compatibility_defaults[name] = () if origin is Sequence else None if inner is dtm.datetime: plan[name] = _DATETIME_FIELD elif isinstance(inner, type) and issubclass(inner, TelegramObject): plan[name] = inner - elif origin is tuple: + elif origin is Sequence: args = get_args(inner) if not args: continue diff --git a/src/telegram/_utils/dataclass.py b/src/telegram/_utils/dataclass.py index 0fcd9b862ee..d3f90f21413 100644 --- a/src/telegram/_utils/dataclass.py +++ b/src/telegram/_utils/dataclass.py @@ -27,34 +27,53 @@ import dataclasses import functools import inspect -from collections.abc import Callable +from collections.abc import Callable, Sequence from dataclasses import MISSING, dataclass, field, is_dataclass -from typing import Any, TypeVar +from typing import Any, TypeVar, get_args, get_origin from typing_extensions import dataclass_transform +from telegram._utils.de_json import unwrap_optional + _T = TypeVar("_T") CONVERTER_KEY = object() +"""Special marker indicating that an entry was deleted.""" ALIAS_KEY = object() +"""Special marker indicating that an entry was deleted.""" + + +def to_sequence_annotation(annotation: object) -> object: + item_type = get_args(annotation)[0] + + if get_origin(item_type) is tuple: + item_type = to_sequence_annotation(item_type) + + return Sequence[item_type] -def _apply_aliases(cls: type[_T]) -> type[_T]: - """Provides runtime support for the `alias` parameter of a field specifier (e.g tg_field). +def process_init(cls: type[_T]) -> type[_T]: + """Processes the ``__init__`` of a class transformed with :func:`dataclasses.dataclass`. Args: - cls (:obj:`type`): A class transformed with `dataclasses.dataclass` + cls (:obj:`type`): A class transformed with :func:`dataclasses.dataclass`. - The function does two things to the provided class generated `__init__`: + The function updates the exposed signature of the provided class' ``__init__``: - 1) Wraps the generated __init__ with a generic one (*args: object, **kwargs: object) that - rejects having the unalised field_name in kwargs. + 1) Replaces field names with their aliases. - 2) Updates the signature of the wrapper __init__ to replace parameter names from raw - to aliased. + 2) Replaces the annotations of fields with converters with the input annotations accepted by + those converters. + + - Fields stored as tuples expose the corresponding :class:`collections.abc.Sequence` + annotation. + + - Parameter required/optional status is calculated based on :param:`tg_field.default` + + It also provides runtime support for :param:`tg_field.alias` Note: - The aliased name is assumed to exist under the field metadata with - the sentinel key `ALIAS_KEY` + The aliased name and converter function are assumed to exist under the field metadata with + the sentinel keys `ALIAS_KEY`, `CONVERTER_KEY` repectively Returns: :obj:`type`: @@ -66,44 +85,76 @@ def _apply_aliases(cls: type[_T]) -> type[_T]: if not is_dataclass(cls): raise TypeError(f"{cls!r} is not a dataclass") - aliases = { - dataclass_field.name: alias - for dataclass_field in dataclasses.fields(cls) - if dataclass_field.init and (alias := dataclass_field.metadata.get(ALIAS_KEY)) is not None - } - - if not aliases: - return cls + fields = {dataclass_field.name: dataclass_field for dataclass_field in dataclasses.fields(cls)} + has_aliases = any( + dataclass_field.init and dataclass_field.metadata.get(ALIAS_KEY) is not None + for dataclass_field in fields.values() + ) generated_init = cls.__init__ generated_signature = inspect.signature(generated_init) @functools.wraps(generated_init) def aliased_init(self: object, *args: object, **kwargs: object) -> None: - for field_name, alias in aliases.items(): - # 1.1) Reject kwargs keys using the raw (unaliased) field name + for field_name, dataclass_field in fields.items(): + alias = dataclass_field.metadata.get(ALIAS_KEY) + + if not dataclass_field.init or dataclass_field.metadata.get(ALIAS_KEY) is None: + continue + + # Reject kwargs keys using the raw (unaliased) field name if field_name in kwargs: raise TypeError( f"{cls.__name__}() got an unexpected keyword argument {field_name!r}" ) - # 1.2) Swap the kwargs key from alias to field_name to satisfy runtime signature + # Swap the kwargs key from alias to field_name to satisfy runtime signature # of generated_init if alias in kwargs: kwargs[field_name] = kwargs.pop(alias) generated_init(self, *args, **kwargs) - # 2) Now and since inspect.signature(generated_init/aliased_init) would still return parameters - # with raw field_names instead of aliased ones, we update those parameters in the signature - # This is neccessary because TO._build_plan expect field names to match what + # Now we update parameter names (if aliased) and annotations (if a converter is present) + # This is neccessary because TO._build_plan expects field names to match what # Telegram API returns - parameters = [ - parameter.replace(name=aliases.get(parameter.name) or parameter.name) - for parameter in generated_signature.parameters.values() - ] + parameters = [] + for param in generated_signature.parameters.values(): + dataclass_field = fields.get(param.name) + + if dataclass_field is None: + parameters.append(param) + continue + + alias = dataclass_field.metadata.get(ALIAS_KEY) + converter = dataclass_field.metadata.get(CONVERTER_KEY) + annotation = param.annotation + + if converter is not None: + field_annotation = unwrap_optional(param.annotation) + + if get_origin(field_annotation) is tuple: + annotation = to_sequence_annotation(field_annotation) + + if dataclass_field.default is not MISSING: + annotation |= None + else: + converter_param = next(iter(inspect.signature(converter).parameters.values())) + annotation = converter_param.annotation + + parameters.append( + param.replace( + name=alias or param.name, + annotation=annotation, + ) + ) + signature = generated_signature.replace(parameters=parameters) + if not has_aliases: + generated_init.__signature__ = signature # type: ignore[attr-defined] + return cls + aliased_init.__signature__ = signature # type: ignore[attr-defined] cls.__init__ = aliased_init # type: ignore[method-assign] return cls @@ -158,7 +209,7 @@ def decorate(cls: type[_T]) -> type[_T]: match_args=False, eq=eq, ) - transformed_cls = _apply_aliases(transformed_cls) + transformed_cls = process_init(transformed_cls) if eq: compare_fields = tuple( diff --git a/src/telegram/_utils/de_json.py b/src/telegram/_utils/de_json.py index 41f0d3c1418..e412f856fb6 100644 --- a/src/telegram/_utils/de_json.py +++ b/src/telegram/_utils/de_json.py @@ -25,7 +25,7 @@ """ import importlib -from collections.abc import Callable +from collections.abc import Callable, Sequence from functools import lru_cache from types import UnionType from typing import TYPE_CHECKING, TypeAlias, Union, cast, get_args, get_origin @@ -91,7 +91,7 @@ def build_sequence_transformer( ) item_origin = get_origin(resolved_item_annotation) - if item_origin is tuple: + if item_origin is Sequence: nested_item_annotations = get_args(resolved_item_annotation) if not nested_item_annotations: return None From b17b6b810e3cafd39ccc42a714d394f16cdb4ccc Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:01:26 +0300 Subject: [PATCH 16/31] Remove `__[get/set]state__` that dataclasses generate --- src/telegram/_utils/dataclass.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/telegram/_utils/dataclass.py b/src/telegram/_utils/dataclass.py index d3f90f21413..9d8ac5778de 100644 --- a/src/telegram/_utils/dataclass.py +++ b/src/telegram/_utils/dataclass.py @@ -211,6 +211,11 @@ def decorate(cls: type[_T]) -> type[_T]: ) transformed_cls = process_init(transformed_cls) + # `dataclass()` generates __[get/set]state__ functions that override our custom ones + if cls.__name__ != "TelegramObject": + delattr(transformed_cls, "__getstate__") + delattr(transformed_cls, "__setstate__") + if eq: compare_fields = tuple( dataclass_field.name From bfc78127bcd2460265aa34e25a2fc470e2d4bc12 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:21:02 +0300 Subject: [PATCH 17/31] Update two tests that relied on uneeded type coercion --- tests/_passport/test_passportfile.py | 2 +- tests/test_chatinvitelink.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/_passport/test_passportfile.py b/tests/_passport/test_passportfile.py index e099cbcc2da..2f8547197fc 100644 --- a/tests/_passport/test_passportfile.py +++ b/tests/_passport/test_passportfile.py @@ -134,4 +134,4 @@ async def make_assertion(*_, **kwargs): assert await check_defaults_handling(passport_file.get_file, passport_file.get_bot()) monkeypatch.setattr(passport_file.get_bot(), "get_file", make_assertion) - assert (await passport_file.get_file()).file_id == "True" + assert (await passport_file.get_file()).file_id is True diff --git a/tests/test_chatinvitelink.py b/tests/test_chatinvitelink.py index 3ef08dbf0d4..36186d1e782 100644 --- a/tests/test_chatinvitelink.py +++ b/tests/test_chatinvitelink.py @@ -89,7 +89,7 @@ def test_de_json_all_args(self, offline_bot, creator): "expire_date": to_timestamp(self.expire_date), "member_limit": self.member_limit, "name": self.name, - "pending_join_request_count": str(self.pending_join_request_count), + "pending_join_request_count": self.pending_join_request_count, "subscription_period": int(self.subscription_period.total_seconds()), "subscription_price": self.subscription_price, } @@ -120,7 +120,7 @@ def test_de_json_localization(self, tz_bot, offline_bot, raw_bot, creator): "expire_date": to_timestamp(self.expire_date), "member_limit": self.member_limit, "name": self.name, - "pending_join_request_count": str(self.pending_join_request_count), + "pending_join_request_count": self.pending_join_request_count, } invite_link_raw = ChatInviteLink.de_json(json_dict, raw_bot) From 9c53a3965823f9504d9f30d995a0a0532631b118 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 15 Sep 2026 19:58:32 +0300 Subject: [PATCH 18/31] Fix a `test_chatmemberupdated` failure Note: this failure revealed a minor breaking change in how `__eq__()` works, worth documenting and adding to changelog later --- tests/test_chatmemberupdated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_chatmemberupdated.py b/tests/test_chatmemberupdated.py index 512fad13042..c378e7534e3 100644 --- a/tests/test_chatmemberupdated.py +++ b/tests/test_chatmemberupdated.py @@ -27,6 +27,7 @@ ChatMember, ChatMemberAdministrator, ChatMemberBanned, + ChatMemberMember, ChatMemberOwner, ChatMemberUpdated, User, @@ -46,7 +47,7 @@ def chat(): @pytest.fixture(scope="module") def old_chat_member(user): - return ChatMember(user, ChatMemberUpdatedTestBase.old_status) + return ChatMemberMember(user) @pytest.fixture(scope="module") @@ -87,7 +88,6 @@ def chat_member_updated(user, chat, old_chat_member, new_chat_member, invite_lin class ChatMemberUpdatedTestBase: - old_status = ChatMember.MEMBER new_status = ChatMember.ADMINISTRATOR From 0a33d4969b9d0ef9a20cc151bfa690c7487abd7c Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:08:37 +0300 Subject: [PATCH 19/31] Update `test_telegramobject` to use `@tg_dataclass()` --- tests/test_telegramobject.py | 304 ++++++++++++++--------------------- 1 file changed, 125 insertions(+), 179 deletions(-) diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index d4dd8578b38..d0d7b104197 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -16,6 +16,7 @@ # # 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 dataclasses import datetime as dtm import inspect import pickle @@ -24,6 +25,7 @@ from copy import deepcopy from pathlib import Path from types import MappingProxyType +from typing import Any import pytest @@ -38,8 +40,9 @@ TelegramObject, User, ) +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.defaultvalue import DEFAULT_FALSE, DEFAULT_NONE, DefaultValue -from telegram._utils.types import JSONDict +from telegram._utils.types import ODVInput from telegram.ext import PicklePersistence from telegram.warnings import PTBUserWarning from tests.auxil.files import data_file @@ -49,38 +52,33 @@ def all_subclasses(cls): # Gets all subclasses of the specified object, recursively. from # https://stackoverflow.com/a/3862957/9706202 # also includes the class itself - return ( + classes = ( set(cls.__subclasses__()) .union([s for c in cls.__subclasses__() for s in all_subclasses(c)]) .union({cls}) ) + # dataclass(slots=True) creates a replacemnt class, both are present in __subclasses__() + # Keep the replacement and remove the original one + return [c for c in classes if "__slots__" in vars(c)] + TO_SUBCLASSES = sorted(all_subclasses(TelegramObject), key=lambda cls: cls.__name__) +@tg_dataclass() class _DeJsonTestObject(TelegramObject): """Small subclass.""" - __slots__ = ("value",) - - def __init__(self, value: int, *, api_kwargs: JSONDict | None = None) -> None: - super().__init__(api_kwargs=api_kwargs) - self.value = value - self._freeze() + value: int = tg_field() class TestDeJsonWithoutRequest: def test_timestamp_is_converted_to_datetime(self): + + @tg_dataclass() class TimestampedObject(TelegramObject): - def __init__( - self, - created_at: dtm.datetime, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.created_at = created_at + created_at: dtm.datetime = tg_field() telegram_object = TimestampedObject.de_json({"created_at": 1_700_000_000}) @@ -89,15 +87,10 @@ def __init__( ) def test_nested_object_is_deserialized_and_receives_bot(self, offline_bot): + + @tg_dataclass() class Container(TelegramObject): - def __init__( - self, - child: _DeJsonTestObject, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.child = child + child: _DeJsonTestObject = tg_field() container = Container.de_json({"child": {"value": 1}}, bot=offline_bot) @@ -106,15 +99,13 @@ def __init__( assert container.child.get_bot() is offline_bot def test_sequence_of_objects_is_deserialized(self): + + @tg_dataclass() class Container(TelegramObject): - def __init__( - self, - children: Sequence[_DeJsonTestObject], - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.children = tuple(children) + def to_tuple(v: Sequence[_DeJsonTestObject]): + return tuple(v) + + children: tuple[_DeJsonTestObject, ...] = tg_field(converter=to_tuple) container = Container.de_json({"children": [{"value": 1}, {"value": 2}]}) @@ -122,58 +113,44 @@ def __init__( assert all(isinstance(child, _DeJsonTestObject) for child in container.children) def test_nested_sequences_of_objects_are_deserialized(self): + + @tg_dataclass() class Container(TelegramObject): - def __init__( - self, - rows: Sequence[Sequence[_DeJsonTestObject]], - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.rows = tuple(tuple(row) for row in rows) + def to_tuple(v: Sequence[Sequence[_DeJsonTestObject]]): + return tuple(tuple(v_i) for v_i in v) + + rows: tuple[tuple[_DeJsonTestObject, ...], ...] = tg_field(converter=to_tuple) container = Container.de_json({"rows": [[{"value": 1}], [{"value": 2}, {"value": 3}]]}) assert [[item.value for item in row] for row in container.rows] == [[1], [2, 3]] def test_unparameterized_sequence_is_left_unchanged(self): + + @tg_dataclass() class Container(TelegramObject): - def __init__( - self, - values: Sequence, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.values = values + values: Sequence = tg_field() values = [1, 2] assert Container.de_json({"values": values}).values is values def test_none_is_not_deserialized_as_nested_object(self): + + @tg_dataclass() class Container(TelegramObject): - def __init__( - self, - child: _DeJsonTestObject | None, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.child = child + child: _DeJsonTestObject | None = tg_field() assert Container.de_json({"child": None}).child is None def test_non_list_sequence_is_not_retransformed(self): + + @tg_dataclass() class Container(TelegramObject): - def __init__( - self, - children: Sequence[_DeJsonTestObject], - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.children = tuple(children) + def to_tuple(v: Sequence[_DeJsonTestObject]): + return tuple(v) + + children: tuple[_DeJsonTestObject, ...] = tg_field(converter=to_tuple) prepared_child = _DeJsonTestObject(value=1) container = Container.de_json({"children": (prepared_child,)}) @@ -181,15 +158,10 @@ def __init__( assert container.children[0] is prepared_child def test_from_is_renamed_without_mutating_input(self): + + @tg_dataclass() class Container(TelegramObject): - def __init__( - self, - from_user: _DeJsonTestObject, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.from_user = from_user + from_user: _DeJsonTestObject = tg_field() api_response = {"from": {"value": 1}} @@ -199,19 +171,16 @@ def __init__( assert api_response == {"from": {"value": 1}} def test_subclass_without_constructor_inherits_parent_plan(self): + + @tg_dataclass() class Parent(TelegramObject): - def __init__( - self, - child: _DeJsonTestObject, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.child = child + child: _DeJsonTestObject = tg_field() + @tg_dataclass() class FirstChild(Parent): pass + @tg_dataclass() class SecondChild(Parent): pass @@ -224,17 +193,11 @@ class SecondChild(Parent): assert second_child.child.value == 2 def test_removed_field_is_preserved_in_api_kwargs(self): + + @tg_dataclass() class ObjectWithRemovedField(TelegramObject): __REMOVED_API_FIELDS__ = frozenset({"removed_field"}) - - def __init__( - self, - current_field: int, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.current_field = current_field + current_field: int = tg_field() telegram_object = ObjectWithRemovedField.de_json({"current_field": 1, "removed_field": 2}) @@ -254,17 +217,11 @@ def test_unknown_field_is_preserved_in_api_kwargs(self): assert user.api_kwargs == {"future_field": "future value"} def test_unknown_field_does_not_replace_constructor_default(self): + + @tg_dataclass() class ObjectWithDefault(TelegramObject): - def __init__( - self, - required_field: int, - defaulted_field: str = "constructor default", - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.required_field = required_field - self.defaulted_field = defaulted_field + required_field: int = tg_field() + defaulted_field: str = tg_field(default="constructor default") telegram_object = ObjectWithDefault.de_json({"required_field": 1, "future_field": 2}) @@ -272,30 +229,23 @@ def __init__( assert telegram_object.api_kwargs == {"future_field": 2} def test_missing_required_scalar_defaults_to_none(self): + + @tg_dataclass() class ObjectWithRequiredField(TelegramObject): - def __init__( - self, - required_field: int, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.required_field = required_field + required_field: int = tg_field() telegram_object = ObjectWithRequiredField.de_json({}) assert telegram_object.required_field is None def test_missing_required_sequence_defaults_to_empty_tuple(self): + + @tg_dataclass() class ObjectWithRequiredSequence(TelegramObject): - def __init__( - self, - required_values: Sequence[int], - *, - api_kwargs: JSONDict | None = None, - ) -> None: - super().__init__(api_kwargs=api_kwargs) - self.required_values = tuple(required_values) + def to_tuple(v: Sequence[int]): + return tuple(v) + + required_values: tuple[int, ...] = tg_field(converter=to_tuple) telegram_object = ObjectWithRequiredSequence.de_json({}) @@ -330,26 +280,27 @@ def test_de_list_returns_deserialized_tuple(self): class TestTelegramObject: + @tg_dataclass() class Sub(TelegramObject): - def __init__(self, private, normal, b): - super().__init__() - self._private = private - self.normal = normal - self._bot = b + _private: Any = tg_field(alias="private") + normal: Any = tg_field() + _bot: Any = tg_field(alias="b") + @dataclasses.dataclass(frozen=True, repr=False, eq=False) class ChangingTO(TelegramObject): - # Don't use in any tests, this is just for testing the pickle behaviour and the - # class is altered during the test procedure + # Don't use in any tests, this is just for testing the pickle behaviour and + # uses standard unsloted `@dataclass` so that its __dict__ can be altered during the + # test procedure pass def test_to_json(self, monkeypatch): + + @tg_dataclass() class Subclass(TelegramObject): - def __init__(self): - super().__init__() - self.arg = "arg" - self.arg2 = ["arg2", "arg2"] - self.arg3 = {"arg3": "arg3"} - self.empty_tuple = () + arg: str = tg_field(default="arg", init=False) + arg2: list[str] = tg_field(default_factory=lambda: ["arg2", "arg2"], init=False) + arg3: dict[str, str] = tg_field(default_factory=lambda: {"arg3": "arg3"}, init=False) + empty_tuple: tuple[Any, ...] = tg_field(default_factory=tuple, init=False) json = Subclass().to_json() # Order isn't guarantied @@ -377,12 +328,10 @@ def test_de_json_optional_bot(self): to.get_bot() def test_de_list(self, bot): - class SubClass(TelegramObject): - def __init__(self, arg: int, **kwargs): - super().__init__(**kwargs) - self.arg = arg - self._id_attrs = (self.arg,) + @tg_dataclass() + class SubClass(TelegramObject): + arg: int = tg_field(compare=True) assert SubClass.de_list([{"arg": 1}, {"arg": 2}], bot) == ( SubClass(1), @@ -398,6 +347,8 @@ def test_api_kwargs_read_only(self): with pytest.raises(AttributeError, match="can't be set"): tg_object.api_kwargs = {"foo": "baz"} + # tags: deprecated NEXT.VERSION + # No subclasses other that Bot will have their own init @pytest.mark.parametrize("cls", TO_SUBCLASSES, ids=[cls.__name__ for cls in TO_SUBCLASSES]) def test_subclasses_have_api_kwargs(self, cls): """Checks that all subclasses of TelegramObject have an api_kwargs argument that is @@ -432,22 +383,21 @@ def test_subclasses_have_api_kwargs(self, cls): ) def test_de_json_arbitrary_exceptions(self, bot): + + @tg_dataclass() class SubClass(TelegramObject): def __init__(self, **kwargs): - super().__init__(**kwargs) + TelegramObject.__init__(self, **kwargs) raise TypeError("This is a test") with pytest.raises(TypeError, match="This is a test"): SubClass.de_json({}, bot) def test_to_dict_private_attribute(self): + @tg_dataclass() class TelegramObjectSubclass(TelegramObject): - __slots__ = ("_b", "a") # Added slots so that the attrs are converted to dict - - def __init__(self): - super().__init__() - self.a = 1 - self._b = 2 + a: int = tg_field(default=1, init=False) + _b: int = tg_field(default=2, init=False) subclass_instance = TelegramObjectSubclass() assert subclass_instance.to_dict() == {"a": 1} @@ -468,19 +418,16 @@ def test_to_dict_missing_attribute(self): assert message_dict["chat"] is None def test_to_dict_recursion(self): + @tg_dataclass() class Recursive(TelegramObject): - __slots__ = ("recursive",) - - def __init__(self): - super().__init__() - self.recursive = "recursive" + recursive: str = tg_field(default="recursive", init=False) + @dataclasses.dataclass(frozen=True, slots=False, repr=False, eq=False) class SubClass(TelegramObject): """This class doesn't have `__slots__`, so has `__dict__` instead.""" - def __init__(self): - super().__init__() - self.subclass = Recursive() + # default_factory to store the attribute in inst.__dict__, not on the class + subclass: Recursive = dataclasses.field(default_factory=Recursive, init=False) to = SubClass() to_dict_no_recurse = to.to_dict(recursive=False) @@ -492,11 +439,11 @@ def __init__(self): assert to_dict_recurse["subclass"]["recursive"] == "recursive" def test_to_dict_default_value(self): + + @tg_dataclass() class SubClass(TelegramObject): - def __init__(self): - super().__init__() - self.default_none = DEFAULT_NONE - self.default_false = DEFAULT_FALSE + default_none: ODVInput[Any] = tg_field(default=DEFAULT_NONE, init=False) + default_false: ODVInput[bool] = tg_field(default=DEFAULT_FALSE, init=False) to = SubClass() to_dict = to.to_dict() @@ -647,13 +594,11 @@ def test_pickle_handle_properties(self): # to test this. # Original class: v1 = """ +@tg_dataclass() class PicklePropertyTest(TelegramObject): - __slots__ = ("forward_from", "to_be_removed", "forward_date") - def __init__(self, forward_from=None, forward_date=None, api_kwargs=None): - super().__init__(api_kwargs=api_kwargs) - self.forward_from = forward_from - self.forward_date = forward_date - self.to_be_removed = "to_be_removed" + forward_from: Any = tg_field(default=None) + forward_date: Any = tg_field(default=None) + to_be_removed: Any = tg_field(init=False, default="to_be_removed") """ exec(v1, globals(), None) old = PicklePropertyTest("old_val", "date", api_kwargs={"new_attr": 1}) # noqa: F821 @@ -661,13 +606,11 @@ def __init__(self, forward_from=None, forward_date=None, api_kwargs=None): # After some API changes: v2 = """ +@tg_dataclass() class PicklePropertyTest(TelegramObject): - __slots__ = ("_forward_from", "_date", "_new_attr") - def __init__(self, forward_from=None, f_date=None, new_attr=None, api_kwargs=None): - super().__init__(api_kwargs=api_kwargs) - self._forward_from = forward_from - self.f_date = f_date - self._new_attr = new_attr + _forward_from: Any = tg_field(default=None, alias="forward_from") + _date: Any = tg_field(default=None, alias="f_date") + _new_attr: Any = tg_field(default=None, alias="new_attr") @property def forward_from(self): return self._forward_from @@ -691,10 +634,9 @@ def new_attr(self): # After PTB removes the property and the attribute: v3 = """ +@tg_dataclass() class PicklePropertyTest(TelegramObject): - __slots__ = () - def __init__(self, api_kwargs=None): - super().__init__(api_kwargs=api_kwargs) + pass """ exec(v3, globals(), None) v3_unpickle = pickle.loads(pickled_v2) @@ -745,7 +687,7 @@ def test_deepcopy_telegram_obj(self, bot): assert new_message.text == "new text" def test_deepcopy_subclass_telegram_obj(self, bot): - s = self.Sub("private", "normal", bot) + s = self.Sub(private="private", normal="normal", b=bot) d = deepcopy(s) assert d is not s assert d._private == s._private # Can't test for identity since two equal strings is True @@ -754,25 +696,29 @@ def test_deepcopy_subclass_telegram_obj(self, bot): assert d.normal == s.normal def test_string_representation(self): + + @tg_dataclass() class TGO(TelegramObject): - def __init__(self, api_kwargs=None): - super().__init__(api_kwargs=api_kwargs) - self.string_attr = "string" - self.int_attr = 42 - self.to_attr = BotCommand("command", "description") - self.list_attr = [ + string_attr: str = tg_field(default="string") + int_attr: int = tg_field(default=42) + to_attr: BotCommand = tg_field(default=BotCommand("command", "description")) + list_attr: list[BotCommand] = tg_field( + default_factory=lambda: [ BotCommand("command_1", "description_1"), BotCommand("command_2", "description_2"), ] - self.dict_attr = { + ) + dict_attr: dict[BotCommand, BotCommand] = tg_field( + default_factory=lambda: { BotCommand("command_1", "description_1"): BotCommand( "command_2", "description_2" ) } - self.empty_tuple_attrs = () - self.empty_str_attribute = "" - # Should not be included in string representation - self.none_attr = None + ) + empty_tuple_attrs: tuple[Any, ...] = tg_field(default_factory=tuple) + empty_str_attribute: str = tg_field(default="") + # Should not be included in string representation + none_attr: Any = tg_field(default=None) expected_without_api_kwargs = ( "TGO(dict_attr={BotCommand(command='command_1', description='description_1'): " From 37705e0fffbcb7c5eeaa78f07054b57c6f2d618d Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:50:49 +0300 Subject: [PATCH 20/31] Remove old "deepcopy preserves freezing" test Old test for a now breaking change unfortunately. --- tests/test_telegramobject.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index d0d7b104197..6dd12b61552 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -675,17 +675,6 @@ def test_deepcopy_telegram_obj(self, bot): assert new_msg.api_kwargs == {"foo": "bar"} assert new_msg.api_kwargs is not msg.api_kwargs - # check that deepcopy preserves the freezing status - with pytest.raises( - AttributeError, match="Attribute `text` of class `Message` can't be set!" - ): - new_msg.text = "new text" - - msg._unfreeze() - new_message = deepcopy(msg) - new_message.text = "new text" - assert new_message.text == "new text" - def test_deepcopy_subclass_telegram_obj(self, bot): s = self.Sub(private="private", normal="normal", b=bot) d = deepcopy(s) From 92dd955ceb05198ca66dd09a66aef5b84aa0255a Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:54:35 +0300 Subject: [PATCH 21/31] Deprecate test_telegramobject `test_subclasses_have_api_kwargs` --- tests/test_telegramobject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index 6dd12b61552..4dafec0bf73 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -347,8 +347,8 @@ def test_api_kwargs_read_only(self): with pytest.raises(AttributeError, match="can't be set"): tg_object.api_kwargs = {"foo": "baz"} - # tags: deprecated NEXT.VERSION - # No subclasses other that Bot will have their own init + # tags: deprecated next.version + # no subclasses other that bot will have their own init @pytest.mark.parametrize("cls", TO_SUBCLASSES, ids=[cls.__name__ for cls in TO_SUBCLASSES]) def test_subclasses_have_api_kwargs(self, cls): """Checks that all subclasses of TelegramObject have an api_kwargs argument that is From 057fde26406f5d84d062efa083d090b4c8917dfc Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:51:38 +0300 Subject: [PATCH 22/31] Define `_CredentialsBase.*_hash` aliases as fields --- src/telegram/_passport/credentials.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/telegram/_passport/credentials.py b/src/telegram/_passport/credentials.py index 098becf4272..95421458469 100644 --- a/src/telegram/_passport/credentials.py +++ b/src/telegram/_passport/credentials.py @@ -343,14 +343,14 @@ class _CredentialsBase(TelegramObject): hash: str = tg_field() secret: str = tg_field() - # Aliases just to be sure - @property - def file_hash(self) -> str: - return self.hash - - @property - def data_hash(self) -> str: - return self.hash + data_hash: str = tg_field(init=False) + file_hash: str = tg_field(init=False) + + def __post_init__(self) -> None: + # Aliases just to be sure + object.__setattr__(self, "data_hash", self.hash) + object.__setattr__(self, "file_hash", self.hash) + TelegramObject.__post_init__(self) @tg_dataclass() From fbd5b8bf7c4e90bfdb117750c5b0378f65e789bc Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:12:57 +0300 Subject: [PATCH 23/31] Handle all the `[un]freezing` in test suite --- tests/_files/conftest.py | 3 +- tests/_files/test_file.py | 5 +- tests/_games/test_game.py | 5 +- tests/_inline/test_inlinekeyboardmarkup.py | 13 +- tests/_passport/test_passport.py | 9 +- tests/auxil/pytest_classes.py | 12 +- tests/conftest.py | 28 ++ tests/ext/test_application.py | 6 - tests/ext/test_businessconnectionhandler.py | 7 +- .../test_businessmessagesdeletedhandler.py | 7 +- tests/ext/test_callbackdatacache.py | 7 +- tests/ext/test_callbackqueryhandler.py | 6 +- tests/ext/test_chatboosthandler.py | 7 +- tests/ext/test_chatjoinrequesthandler.py | 7 +- tests/ext/test_chatmemberhandler.py | 5 +- tests/ext/test_choseninlineresulthandler.py | 6 +- tests/ext/test_conversationhandler.py | 378 ++++++++---------- tests/ext/test_filters.py | 109 ++--- tests/ext/test_inlinequeryhandler.py | 14 +- tests/ext/test_managedbotupdatedhandler.py | 7 +- tests/ext/test_messagehandler.py | 6 +- tests/ext/test_messagereactionhandler.py | 12 +- tests/ext/test_paidmediapurchasedhandler.py | 7 +- tests/ext/test_picklepersistence.py | 26 +- tests/ext/test_precheckoutqueryhandler.py | 6 +- tests/test_bot.py | 32 +- tests/test_callbackquery.py | 25 +- tests/test_chat.py | 5 +- tests/test_chatfullinfo.py | 5 +- tests/test_choseninlineresult.py | 5 +- tests/test_directmessagestopic.py | 5 +- tests/test_message.py | 10 +- tests/test_poll.py | 21 +- tests/test_telegramobject.py | 67 +--- tests/test_update.py | 18 +- tests/test_user.py | 5 +- 36 files changed, 416 insertions(+), 480 deletions(-) diff --git a/tests/_files/conftest.py b/tests/_files/conftest.py index 9e8e5212e11..971d72364ae 100644 --- a/tests/_files/conftest.py +++ b/tests/_files/conftest.py @@ -101,8 +101,7 @@ async def sticker(bot, chat_id): with data_file("telegram.webp").open("rb") as f: sticker = (await bot.send_sticker(chat_id, sticker=f, read_timeout=50)).sticker # necessary to properly test needs_repainting - with sticker._unfrozen(): - sticker.needs_repainting = True + object.__setattr__(sticker, "needs_repainting", True) return sticker diff --git a/tests/_files/test_file.py b/tests/_files/test_file.py index 1ecdff98f16..f1dd9e1af21 100644 --- a/tests/_files/test_file.py +++ b/tests/_files/test_file.py @@ -26,6 +26,7 @@ from telegram import File, FileCredentials, Voice from telegram.error import TelegramError from tests.auxil.files import data_file +from tests.conftest import unfrozen @pytest.fixture(scope="module") @@ -37,8 +38,8 @@ def file(bot): file_size=FileTestBase.file_size, ) file.set_bot(bot) - file._unfreeze() - return file + with unfrozen(file): + yield file @pytest.fixture(scope="module") diff --git a/tests/_games/test_game.py b/tests/_games/test_game.py index de08d9969c3..576466bc5a9 100644 --- a/tests/_games/test_game.py +++ b/tests/_games/test_game.py @@ -20,6 +20,7 @@ import pytest from telegram import Animation, Game, MessageEntity, PhotoSize +from tests.conftest import unfrozen @pytest.fixture(scope="module") @@ -32,8 +33,8 @@ def game(): text_entities=GameTestBase.text_entities, animation=GameTestBase.animation, ) - game._unfreeze() - return game + with unfrozen(game): + yield game class GameTestBase: diff --git a/tests/_inline/test_inlinekeyboardmarkup.py b/tests/_inline/test_inlinekeyboardmarkup.py index c752763ac67..657e55f1380 100644 --- a/tests/_inline/test_inlinekeyboardmarkup.py +++ b/tests/_inline/test_inlinekeyboardmarkup.py @@ -27,6 +27,7 @@ ReplyKeyboardRemove, ) from telegram.constants import KeyboardButtonStyle +from tests.conftest import unfrozen @pytest.fixture(scope="module") @@ -213,12 +214,12 @@ async def make_assertion( assert bool("'switch_inline_query': ''" in str(data["reply_markup"])) assert bool("'switch_inline_query_current_chat': ''" in str(data["reply_markup"])) - inline_keyboard_markup.inline_keyboard[0][0]._unfreeze() - inline_keyboard_markup.inline_keyboard[0][0].callback_data = None - inline_keyboard_markup.inline_keyboard[0][0].switch_inline_query = "" - inline_keyboard_markup.inline_keyboard[0][1]._unfreeze() - inline_keyboard_markup.inline_keyboard[0][1].callback_data = None - inline_keyboard_markup.inline_keyboard[0][1].switch_inline_query_current_chat = "" + with unfrozen(inline_keyboard_markup.inline_keyboard[0][0]): + inline_keyboard_markup.inline_keyboard[0][0].callback_data = None + inline_keyboard_markup.inline_keyboard[0][0].switch_inline_query = "" + with unfrozen(inline_keyboard_markup.inline_keyboard[0][1]): + inline_keyboard_markup.inline_keyboard[0][1].callback_data = None + inline_keyboard_markup.inline_keyboard[0][1].switch_inline_query_current_chat = "" monkeypatch.setattr(offline_bot, "_send_message", make_assertion) await offline_bot.send_message(123, "test", reply_markup=inline_keyboard_markup) diff --git a/tests/_passport/test_passport.py b/tests/_passport/test_passport.py index 1735a17d8cd..df14d69e7d3 100644 --- a/tests/_passport/test_passport.py +++ b/tests/_passport/test_passport.py @@ -36,6 +36,7 @@ # worth it. from telegram.request import RequestData from tests.auxil.pytest_classes import make_bot +from tests.conftest import unfrozen RAW_PASSPORT_DATA = { "credentials": { @@ -400,8 +401,8 @@ def test_equality(self, passport_data): assert a is not b new_pp_data = deepcopy(passport_data) - new_pp_data.credentials._unfreeze() - new_pp_data.credentials.hash = "NOTAPROPERHASH" + with unfrozen(new_pp_data.credentials): + new_pp_data.credentials.hash = "NOTAPROPERHASH" c = PassportData(new_pp_data.data, new_pp_data.credentials) assert a != c @@ -446,7 +447,9 @@ def test_all_types(self, passport_data, offline_bot, all_passport_data): ) assert new.api_kwargs == {} - new.credentials._decrypted_data = Credentials.de_json(credentials, offline_bot) + object.__setattr__( + new.credentials, "_decrypted_data", Credentials.de_json(credentials, offline_bot) + ) assert new.credentials.api_kwargs == {} assert isinstance(new, PassportData) diff --git a/tests/auxil/pytest_classes.py b/tests/auxil/pytest_classes.py index 765e91592e2..a1b49130717 100644 --- a/tests/auxil/pytest_classes.py +++ b/tests/auxil/pytest_classes.py @@ -64,10 +64,12 @@ async def _mocked_get_me(bot: Bot): class PytestExtBot(ExtBot): + # Makes it easier to work with the bot in tests + __setattr__ = object.__setattr__ + __delattr__ = object.__delattr__ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Makes it easier to work with the bot in tests - self._unfreeze() def __repr__(self) -> str: return build_repr_with_selected_attrs(self, token=_REDACTED_TOKEN) @@ -78,10 +80,12 @@ async def get_me(self, *args, **kwargs) -> User: class PytestBot(Bot): + # Makes it easier to work with the bot in tests + __setattr__ = object.__setattr__ + __delattr__ = object.__delattr__ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Makes it easier to work with the bot in tests - self._unfreeze() def __repr__(self) -> str: return build_repr_with_selected_attrs(self, token=_REDACTED_TOKEN) diff --git a/tests/conftest.py b/tests/conftest.py index ed0c3f64231..f371f06a969 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ import os import sys import zoneinfo +from contextlib import contextmanager from pathlib import Path from uuid import uuid4 @@ -344,3 +345,30 @@ def dummy_message(): @pytest.fixture(scope="session") def dummy_message_dict(dummy_message): return dummy_message.to_dict() + + +@contextmanager +def unfrozen(obj): + """Helper for mutating TelegramObjects duration tests""" + cls = type(obj) + original_setattr = cls.__setattr__ + original_delattr = cls.__delattr__ + + def set_attribute(instance, name, value): + # Identity check is needed so that only that only `obj` is affected + if instance is obj: + object.__setattr__(instance, name, value) + else: + original_setattr(instance, name, value) + + def delete_attribute(instance, name): + # Identity check is needed so that only that only `obj` is affected + if instance is obj: + object.__delattr__(instance, name) + else: + original_delattr(instance, name) + + with pytest.MonkeyPatch.context() as patch: + patch.setattr(cls, "__setattr__", set_attribute) + patch.setattr(cls, "__delattr__", delete_attribute) + yield obj diff --git a/tests/ext/test_application.py b/tests/ext/test_application.py index c26e8b3f32a..8d615570692 100644 --- a/tests/ext/test_application.py +++ b/tests/ext/test_application.py @@ -1542,7 +1542,6 @@ async def post_init(app: Application) -> None: .post_init(post_init) .build() ) - app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) monkeypatch.setattr( app, "initialize", call_after(app.initialize, lambda _: events.append("init")) @@ -1587,7 +1586,6 @@ async def post_shutdown(app: Application) -> None: .post_shutdown(post_shutdown) .build() ) - app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) monkeypatch.setattr( app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown")) @@ -1636,7 +1634,6 @@ async def post_stop(app: Application) -> None: .post_stop(post_stop) .build() ) - app.bot._unfreeze() monkeypatch.setattr(app.bot, "get_updates", empty_get_updates) monkeypatch.setattr(app, "stop", call_after(app.stop, lambda _: events.append("stop"))) monkeypatch.setattr( @@ -1817,7 +1814,6 @@ async def post_init(app: Application) -> None: .updater(PytestUpdater(one_time_bot, asyncio.Queue())) .build() ) - app.bot._unfreeze() monkeypatch.setattr( app, "initialize", call_after(app.initialize, lambda _: events.append("init")) @@ -1873,7 +1869,6 @@ async def post_shutdown(app: Application) -> None: .post_shutdown(post_shutdown) .build() ) - app.bot._unfreeze() monkeypatch.setattr( app, "shutdown", call_after(app.shutdown, lambda _: events.append("shutdown")) @@ -1933,7 +1928,6 @@ async def post_stop(app: Application) -> None: .post_stop(post_stop) .build() ) - app.bot._unfreeze() monkeypatch.setattr(app, "stop", call_after(app.stop, lambda _: events.append("stop"))) monkeypatch.setattr( diff --git a/tests/ext/test_businessconnectionhandler.py b/tests/ext/test_businessconnectionhandler.py index 07ac3417c34..ca2d23d24e5 100644 --- a/tests/ext/test_businessconnectionhandler.py +++ b/tests/ext/test_businessconnectionhandler.py @@ -37,6 +37,7 @@ from telegram._utils.datetime import UTC from telegram.ext import BusinessConnectionHandler, CallbackContext, JobQueue from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -156,9 +157,9 @@ def test_with_username(self, business_connection_update): handler = BusinessConnectionHandler(self.callback, username=["@user_b"]) assert not handler.check_update(business_connection_update) - business_connection_update.business_connection.user._unfreeze() - business_connection_update.business_connection.user.username = None - assert not handler.check_update(business_connection_update) + with unfrozen(business_connection_update.business_connection.user): + business_connection_update.business_connection.user.username = None + assert not handler.check_update(business_connection_update) def test_other_update_types(self, false_update): handler = BusinessConnectionHandler(self.callback) diff --git a/tests/ext/test_businessmessagesdeletedhandler.py b/tests/ext/test_businessmessagesdeletedhandler.py index f002f50c55f..1e944e6237c 100644 --- a/tests/ext/test_businessmessagesdeletedhandler.py +++ b/tests/ext/test_businessmessagesdeletedhandler.py @@ -36,6 +36,7 @@ from telegram._utils.datetime import UTC from telegram.ext import BusinessMessagesDeletedHandler, CallbackContext, JobQueue from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -152,9 +153,9 @@ def test_with_username(self, business_messages_deleted_update): handler = BusinessMessagesDeletedHandler(self.callback, username=["@user_b"]) assert not handler.check_update(business_messages_deleted_update) - business_messages_deleted_update.deleted_business_messages.chat._unfreeze() - business_messages_deleted_update.deleted_business_messages.chat.username = None - assert not handler.check_update(business_messages_deleted_update) + with unfrozen(business_messages_deleted_update.deleted_business_messages.chat): + business_messages_deleted_update.deleted_business_messages.chat.username = None + assert not handler.check_update(business_messages_deleted_update) def test_other_update_types(self, false_update): handler = BusinessMessagesDeletedHandler(self.callback) diff --git a/tests/ext/test_callbackdatacache.py b/tests/ext/test_callbackdatacache.py index a420da8c9dd..3edf3be71d0 100644 --- a/tests/ext/test_callbackdatacache.py +++ b/tests/ext/test_callbackdatacache.py @@ -29,6 +29,7 @@ from telegram.ext._callbackdatacache import CallbackDataCache, InvalidCallbackData, _KeyboardData from tests.auxil.envvars import TEST_WITH_OPT_DEPS from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen @pytest.fixture @@ -195,9 +196,9 @@ def test_process_callback_query(self, callback_data_cache, data, message, invali effective_message = Message( message_id=1, date=dtm.datetime.now(), chat=chat, reply_markup=out ) - effective_message._unfreeze() - effective_message.reply_to_message = deepcopy(effective_message) - effective_message.pinned_message = deepcopy(effective_message) + with unfrozen(effective_message): + effective_message.reply_to_message = deepcopy(effective_message) + effective_message.pinned_message = deepcopy(effective_message) cq_id = uuid4().hex callback_query = CallbackQuery( cq_id, diff --git a/tests/ext/test_callbackqueryhandler.py b/tests/ext/test_callbackqueryhandler.py index 9d998b6e1c4..b0d09b53fea 100644 --- a/tests/ext/test_callbackqueryhandler.py +++ b/tests/ext/test_callbackqueryhandler.py @@ -34,6 +34,7 @@ ) from telegram.ext import CallbackContext, CallbackQueryHandler, InvalidCallbackData, JobQueue from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -68,9 +69,8 @@ def false_update(request): @pytest.fixture def callback_query(bot): update = Update(0, callback_query=CallbackQuery(2, User(1, "", False), None, data="test data")) - update._unfreeze() - update.callback_query._unfreeze() - return update + with unfrozen(update), unfrozen(update.callback_query): + yield update class TestCallbackQueryHandler: diff --git a/tests/ext/test_chatboosthandler.py b/tests/ext/test_chatboosthandler.py index 8b567415d6d..a6b90f13431 100644 --- a/tests/ext/test_chatboosthandler.py +++ b/tests/ext/test_chatboosthandler.py @@ -32,6 +32,7 @@ from telegram._utils.datetime import from_timestamp from telegram.ext import CallbackContext, ChatBoostHandler from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen from tests.test_update import all_types as really_all_types from tests.test_update import params as all_params @@ -212,6 +213,6 @@ def test_with_username(self): handler = ChatBoostHandler(cb, chat_boost_types=0, chat_username=["@chat_b"]) assert not handler.check_update(update) - update.removed_chat_boost.chat._unfreeze() - update.removed_chat_boost.chat.username = None - assert not handler.check_update(update) + with unfrozen(update.removed_chat_boost.chat): + update.removed_chat_boost.chat.username = None + assert not handler.check_update(update) diff --git a/tests/ext/test_chatjoinrequesthandler.py b/tests/ext/test_chatjoinrequesthandler.py index 4a5ea46bcea..860685c1f0e 100644 --- a/tests/ext/test_chatjoinrequesthandler.py +++ b/tests/ext/test_chatjoinrequesthandler.py @@ -37,6 +37,7 @@ from telegram._utils.datetime import UTC from telegram.ext import CallbackContext, ChatJoinRequestHandler, JobQueue from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -164,9 +165,9 @@ def test_with_username(self, chat_join_request_update): handler = ChatJoinRequestHandler(self.callback, username=["@user_b"]) assert not handler.check_update(chat_join_request_update) - chat_join_request_update.chat_join_request.from_user._unfreeze() - chat_join_request_update.chat_join_request.from_user.username = None - assert not handler.check_update(chat_join_request_update) + with unfrozen(chat_join_request_update.chat_join_request.from_user): + chat_join_request_update.chat_join_request.from_user.username = None + assert not handler.check_update(chat_join_request_update) def test_other_update_types(self, false_update): handler = ChatJoinRequestHandler(self.callback) diff --git a/tests/ext/test_chatmemberhandler.py b/tests/ext/test_chatmemberhandler.py index addaeabacc4..d8487c91a3c 100644 --- a/tests/ext/test_chatmemberhandler.py +++ b/tests/ext/test_chatmemberhandler.py @@ -37,6 +37,7 @@ from telegram._utils.datetime import from_timestamp from telegram.ext import CallbackContext, ChatMemberHandler, JobQueue from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -84,8 +85,8 @@ def chat_member_updated(): @pytest.fixture def chat_member(bot, chat_member_updated): update = Update(0, my_chat_member=chat_member_updated) - update._unfreeze() - return update + with unfrozen(update): + yield update class TestChatMemberHandler: diff --git a/tests/ext/test_choseninlineresulthandler.py b/tests/ext/test_choseninlineresulthandler.py index 24984e380e5..9a2353ac092 100644 --- a/tests/ext/test_choseninlineresulthandler.py +++ b/tests/ext/test_choseninlineresulthandler.py @@ -34,6 +34,7 @@ ) from telegram.ext import CallbackContext, ChosenInlineResultHandler, JobQueue from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -73,9 +74,8 @@ def chosen_inline_result(): 1, chosen_inline_result=ChosenInlineResult("result_id", User(1, "test_user", False), "query"), ) - out._unfreeze() - out.chosen_inline_result._unfreeze() - return out + with unfrozen(out), unfrozen(out.chosen_inline_result): + yield out class TestChosenInlineResultHandler: diff --git a/tests/ext/test_conversationhandler.py b/tests/ext/test_conversationhandler.py index ea0dc580754..ca0252e9673 100644 --- a/tests/ext/test_conversationhandler.py +++ b/tests/ext/test_conversationhandler.py @@ -564,38 +564,36 @@ async def callback(_, __): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY assert self.test_flag == (not raise_ahs) # The user is thirsty and wants to brew coffee. - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING assert self.test_flag == (not raise_ahs) # Lets see if an invalid command makes sure, no state is changed. - message.text = "/nothing" - message.entities[0].length = len("/nothing") + object.__setattr__(message, "text", "/nothing") + object.__setattr__(message.entities[0], "length", len("/nothing")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING assert self.test_flag is True self.test_flag = False # Lets see if the state machine still works by pouring coffee. - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING assert self.test_flag == (not raise_ahs) # Let's now verify that for another user, who did not start yet, # the state has not been changed. - message.from_user = user2 + object.__setattr__(message, "from_user", user2) await app.process_update(Update(update_id=0, message=message)) with pytest.raises(KeyError): self.current_state[user2.id] @@ -617,19 +615,17 @@ async def test_conversation_handler_end(self, caplog, app, bot, user1): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) await app.process_update(Update(update_id=0, message=message)) - message.text = "/end" - message.entities[0].length = len("/end") + object.__setattr__(message, "text", "/end") + object.__setattr__(message.entities[0], "length", len("/end")) caplog.clear() with caplog.at_level(logging.ERROR): await app.process_update(Update(update_id=0, message=message)) @@ -638,8 +634,8 @@ async def test_conversation_handler_end(self, caplog, app, bot, user1): # make sure that the conversation has ended by checking that the start command is # accepted again - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert handler.check_update(Update(update_id=0, message=message)) async def test_conversation_handler_fallback(self, app, bot, user1, user2): @@ -658,8 +654,6 @@ async def test_conversation_handler_fallback(self, app, bot, user1, user2): entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len("/eat"))], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) @@ -667,20 +661,20 @@ async def test_conversation_handler_fallback(self, app, bot, user1, user2): self.current_state[user1.id] # User starts the state machine. - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY # The user is thirsty and wants to brew coffee. - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING # Now a fallback command is issued - message.text = "/eat" - message.entities[0].length = len("/eat") + object.__setattr__(message, "text", "/eat") + object.__setattr__(message.entities[0], "length", len("/eat")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY @@ -712,8 +706,6 @@ async def callback(_, __): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) try: @@ -753,31 +745,29 @@ async def test_conversation_handler_per_chat(self, app, bot, user1, user2): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # The user is thirsty and wants to brew coffee. - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) # Let's now verify that for another user, who did not start yet, # the state will be changed because they are in the same group. - message.from_user = user2 - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "from_user", user2) + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) await app.process_update(Update(update_id=0, message=message)) # Check that we're in the DRINKING state by checking that the corresponding command # is accepted - message.from_user = user1 - message.text = "/startCoding" - message.entities[0].length = len("/startCoding") + object.__setattr__(message, "from_user", user1) + object.__setattr__(message, "text", "/startCoding") + object.__setattr__(message.entities[0], "length", len("/startCoding")) assert handler.check_update(Update(update_id=0, message=message)) - message.from_user = user2 + object.__setattr__(message, "from_user", user2) assert handler.check_update(Update(update_id=0, message=message)) async def test_conversation_handler_per_user(self, app, bot, user1): @@ -801,36 +791,34 @@ async def test_conversation_handler_per_user(self, app, bot, user1): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() # First check that updates without user won't be handled - message.from_user = None + object.__setattr__(message, "from_user", None) assert not handler.check_update(Update(update_id=0, message=message)) - message.from_user = user1 + object.__setattr__(message, "from_user", user1) async with app: await app.process_update(Update(update_id=0, message=message)) # The user is thirsty and wants to brew coffee. - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) # Let's now verify that for the same user in a different group, the state will still be # updated - message.chat = self.second_group - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "chat", self.second_group) + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) await app.process_update(Update(update_id=0, message=message)) # Check that we're in the DRINKING state by checking that the corresponding command # is accepted - message.chat = self.group - message.text = "/startCoding" - message.entities[0].length = len("/startCoding") + object.__setattr__(message, "chat", self.group) + object.__setattr__(message, "text", "/startCoding") + object.__setattr__(message.entities[0], "length", len("/startCoding")) assert handler.check_update(Update(update_id=0, message=message)) - message.chat = self.second_group + object.__setattr__(message, "chat", self.second_group) assert handler.check_update(Update(update_id=0, message=message)) @pytest.mark.parametrize("inline", [True, False]) @@ -865,7 +853,6 @@ async def two(update, context): ) if message: message.set_bot(bot) - message._unfreeze() inline_message_id = "42" if inline else None async with app: @@ -887,7 +874,6 @@ async def two(update, context): inline_message_id=inline_message_id, ) cbq_2.set_bot(bot) - cbq_2._unfreeze() await app.process_update(Update(update_id=0, callback_query=cbq_1)) # Make sure that we're in the correct state @@ -902,10 +888,10 @@ async def two(update, context): # Let's now verify that for a different user in the same group, the state will not be # updated - cbq_2.from_user = user2 + object.__setattr__(cbq_2, "from_user", user2) await app.process_update(Update(update_id=0, callback_query=cbq_2)) - cbq_2.from_user = user1 + object.__setattr__(cbq_2, "from_user", user1) assert not handler.check_update(Update(0, callback_query=cbq_1)) assert handler.check_update(Update(0, callback_query=cbq_2)) @@ -927,8 +913,6 @@ async def test_end_on_first_message(self, app, bot, user1): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert handler.check_update(Update(update_id=0, message=message)) @@ -955,8 +939,6 @@ async def test_end_on_first_message_non_blocking_handler(self, app, bot, user1): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # give the task a chance to finish @@ -976,7 +958,6 @@ async def test_none_on_first_message(self, app, bot, user1): # User starts the state machine and a callback function returns None message = Message(0, None, self.group, from_user=user1, text="/start") message.set_bot(bot) - message._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # Check that the same message is accepted again, i.e. the conversation immediately @@ -1004,8 +985,6 @@ async def test_none_on_first_message_non_blocking_handler(self, app, bot, user1) ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) # Give the task a chance to finish @@ -1031,7 +1010,6 @@ async def test_channel_message_without_chat(self, bot): ) message = Message(0, date=None, chat=Chat(0, Chat.CHANNEL, "Misses Test")) message.set_bot(bot) - message._unfreeze() update = Update(0, channel_post=message) assert not handler.check_update(update) @@ -1045,7 +1023,6 @@ async def test_all_update_types(self, app, bot, user1): ) message = Message(0, None, self.group, from_user=user1, text="ignore") message.set_bot(bot) - message._unfreeze() callback_query = CallbackQuery(0, user1, None, message=message, data="data") callback_query.set_bot(bot) chosen_inline_result = ChosenInlineResult(0, user1, "query") @@ -1086,8 +1063,6 @@ async def test_no_running_job_queue_warning(self, app, bot, user1, recwarn, jq): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) @@ -1137,8 +1112,6 @@ class DictJB(JobQueue): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.start() @@ -1188,13 +1161,11 @@ async def raise_error(*a, **kw): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() # start the conversation async with app: await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.1) - message.text = "error" + object.__setattr__(message, "text", "error") await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.1) caplog.clear() @@ -1239,8 +1210,6 @@ async def raise_error(*a, **kw): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() # start the conversation async with app: await app.process_update(Update(update_id=0, message=message)) @@ -1348,22 +1317,20 @@ def timeout(*a, **kw): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: # start the conversation await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.1) - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=1, message=message)) await asyncio.sleep(0.1) - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) await app.process_update(Update(update_id=2, message=message)) await asyncio.sleep(0.1) - message.text = "/end" - message.entities[0].length = len("/end") + object.__setattr__(message, "text", "/end") + object.__setattr__(message.entities[0], "length", len("/end")) await app.process_update(Update(update_id=3, message=message)) await asyncio.sleep(1) # assert timeout handler didn't get called @@ -1395,8 +1362,6 @@ def timeout(*args, **kwargs): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() brew_message = Message( 0, None, @@ -1449,8 +1414,6 @@ async def start_callback(u, c): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() update = Update(update_id=0, message=message) async def timeout_callback(u, c): @@ -1509,34 +1472,32 @@ async def test_conversation_timeout_keeps_extending(self, app, bot, user1): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.35) # t=.35 assert handler.check_update(Update(0, message=message)) await app.process_update(Update(update_id=0, message=message)) - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.25) # t=.6 assert handler.check_update(Update(0, message=message)) await app.process_update(Update(update_id=0, message=message)) - message.text = "/startCoding" - message.entities[0].length = len("/startCoding") + object.__setattr__(message, "text", "/startCoding") + object.__setattr__(message.entities[0], "length", len("/startCoding")) assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.4) # t=1.0 assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.3) # t=1.3 assert not handler.check_update(Update(0, message=message)) - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert handler.check_update(Update(0, message=message)) await app.stop() @@ -1562,34 +1523,32 @@ async def test_conversation_timeout_two_users(self, app, bot, user1, user2): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) assert handler.check_update(Update(0, message=message)) - message.from_user = user2 + object.__setattr__(message, "from_user", user2) await app.process_update(Update(update_id=0, message=message)) - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) # Make sure that user2s conversation has not yet started assert handler.check_update(Update(0, message=message)) await app.process_update(Update(update_id=0, message=message)) - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) assert handler.check_update(Update(0, message=message)) await asyncio.sleep(0.7) # check that both conversations have ended by checking that the start message is # accepted again - message.text = "/start" - message.entities[0].length = len("/start") - message.from_user = user1 + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) + object.__setattr__(message, "from_user", user1) assert handler.check_update(Update(0, message=message)) - message.from_user = user2 + object.__setattr__(message, "from_user", user2) assert handler.check_update(Update(0, message=message)) await app.stop() @@ -1624,27 +1583,25 @@ async def test_conversation_handler_timeout_state(self, app, bot, user1): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert handler.check_update(Update(0, message=message)) assert self.is_timeout # MessageHandler timeout self.is_timeout = False - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) await app.process_update(Update(update_id=1, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again @@ -1654,16 +1611,16 @@ async def test_conversation_handler_timeout_state(self, app, bot, user1): # Timeout but no valid handler self.is_timeout = False await app.process_update(Update(update_id=0, message=message)) - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) - message.text = "/startCoding" - message.entities[0].length = len("/startCoding") + object.__setattr__(message, "text", "/startCoding") + object.__setattr__(message.entities[0], "length", len("/startCoding")) await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert handler.check_update(Update(0, message=message)) assert not self.is_timeout @@ -1699,26 +1656,24 @@ async def test_conversation_handler_timeout_state_context(self, app, bot, user1) ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert handler.check_update(Update(0, message=message)) assert self.is_timeout # MessageHandler timeout self.is_timeout = False - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) await app.process_update(Update(update_id=1, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again @@ -1728,16 +1683,16 @@ async def test_conversation_handler_timeout_state_context(self, app, bot, user1) # Timeout but no valid handler self.is_timeout = False await app.process_update(Update(update_id=0, message=message)) - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) - message.text = "/startCoding" - message.entities[0].length = len("/startCoding") + object.__setattr__(message, "text", "/startCoding") + object.__setattr__(message.entities[0], "length", len("/startCoding")) await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.7) # check that conversation has ended by checking that start cmd is accepted again - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert handler.check_update(Update(0, message=message)) assert not self.is_timeout @@ -1783,26 +1738,24 @@ async def slowbrew(_update, context): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.start() await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.25) - message.text = "/slowbrew" - message.entities[0].length = len("/slowbrew") + object.__setattr__(message, "text", "/slowbrew") + object.__setattr__(message.entities[0], "length", len("/slowbrew")) await app.process_update(Update(update_id=0, message=message)) # Check that conversation has not ended by checking that start cmd is not accepted - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert not handler.check_update(Update(0, message=message)) assert not self.is_timeout await asyncio.sleep(0.7) # Check that conversation has ended by checking that start cmd is accepted again - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert handler.check_update(Update(0, message=message)) assert self.is_timeout @@ -1834,56 +1787,54 @@ async def test_nested_conversation_handler(self, app, bot, user1, user2): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY # The user is thirsty and wants to brew coffee. - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING # Lets pour some coffee. - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING # The user is holding the cup - message.text = "/hold" - message.entities[0].length = len("/hold") + object.__setattr__(message, "text", "/hold") + object.__setattr__(message.entities[0], "length", len("/hold")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING # The user is sipping coffee - message.text = "/sip" - message.entities[0].length = len("/sip") + object.__setattr__(message, "text", "/sip") + object.__setattr__(message.entities[0], "length", len("/sip")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.SIPPING # The user is swallowing - message.text = "/swallow" - message.entities[0].length = len("/swallow") + object.__setattr__(message, "text", "/swallow") + object.__setattr__(message.entities[0], "length", len("/swallow")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.SWALLOWING # The user is holding the cup again - message.text = "/hold" - message.entities[0].length = len("/hold") + object.__setattr__(message, "text", "/hold") + object.__setattr__(message.entities[0], "length", len("/hold")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING # The user wants to replenish the coffee supply - message.text = "/replenish" - message.entities[0].length = len("/replenish") + object.__setattr__(message, "text", "/replenish") + object.__setattr__(message.entities[0], "length", len("/replenish")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.REPLENISHING # check that we're in the right state now by checking that the update is accepted - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) assert handler.check_update(Update(0, message=message)) # The user wants to drink their coffee again) @@ -1891,31 +1842,31 @@ async def test_nested_conversation_handler(self, app, bot, user1, user2): assert self.current_state[user1.id] == self.DRINKING # The user is now ready to start coding - message.text = "/startCoding" - message.entities[0].length = len("/startCoding") + object.__setattr__(message, "text", "/startCoding") + object.__setattr__(message.entities[0], "length", len("/startCoding")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.CODING # The user decides it's time to drink again - message.text = "/drinkMore" - message.entities[0].length = len("/drinkMore") + object.__setattr__(message, "text", "/drinkMore") + object.__setattr__(message.entities[0], "length", len("/drinkMore")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING # The user is holding their cup - message.text = "/hold" - message.entities[0].length = len("/hold") + object.__setattr__(message, "text", "/hold") + object.__setattr__(message.entities[0], "length", len("/hold")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING # The user wants to end with the drinking and go back to coding - message.text = "/end" - message.entities[0].length = len("/end") + object.__setattr__(message, "text", "/end") + object.__setattr__(message.entities[0], "length", len("/end")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.END # check that we're in the right state now by checking that the update is accepted - message.text = "/drinkMore" - message.entities[0].length = len("/drinkMore") + object.__setattr__(message, "text", "/drinkMore") + object.__setattr__(message.entities[0], "length", len("/drinkMore")) assert handler.check_update(Update(0, message=message)) # The user wants to drink once more @@ -1923,13 +1874,13 @@ async def test_nested_conversation_handler(self, app, bot, user1, user2): assert self.current_state[user1.id] == self.DRINKING # The user wants to stop altogether - message.text = "/stop" - message.entities[0].length = len("/stop") + object.__setattr__(message, "text", "/stop") + object.__setattr__(message.entities[0], "length", len("/stop")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.STOPPING # check that the conversation has ended by checking that the start cmd is accepted - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert handler.check_update(Update(0, message=message)) async def test_nested_conversation_application_handler_stop(self, app, bot, user1, user2): @@ -1964,63 +1915,61 @@ def test_callback(u, c): ], ) message.set_bot(bot) - message._unfreeze() - message.entities[0]._unfreeze() async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY assert not self.test_flag # The user is thirsty and wants to brew coffee. - message.text = "/brew" - message.entities[0].length = len("/brew") + object.__setattr__(message, "text", "/brew") + object.__setattr__(message.entities[0], "length", len("/brew")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING assert not self.test_flag # Lets pour some coffee. - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING assert not self.test_flag # The user is holding the cup - message.text = "/hold" - message.entities[0].length = len("/hold") + object.__setattr__(message, "text", "/hold") + object.__setattr__(message.entities[0], "length", len("/hold")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING assert not self.test_flag # The user is sipping coffee - message.text = "/sip" - message.entities[0].length = len("/sip") + object.__setattr__(message, "text", "/sip") + object.__setattr__(message.entities[0], "length", len("/sip")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.SIPPING assert not self.test_flag # The user is swallowing - message.text = "/swallow" - message.entities[0].length = len("/swallow") + object.__setattr__(message, "text", "/swallow") + object.__setattr__(message.entities[0], "length", len("/swallow")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.SWALLOWING assert not self.test_flag # The user is holding the cup again - message.text = "/hold" - message.entities[0].length = len("/hold") + object.__setattr__(message, "text", "/hold") + object.__setattr__(message.entities[0], "length", len("/hold")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING assert not self.test_flag # The user wants to replenish the coffee supply - message.text = "/replenish" - message.entities[0].length = len("/replenish") + object.__setattr__(message, "text", "/replenish") + object.__setattr__(message.entities[0], "length", len("/replenish")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.REPLENISHING # check that we're in the right state now by checking that the update is accepted - message.text = "/pourCoffee" - message.entities[0].length = len("/pourCoffee") + object.__setattr__(message, "text", "/pourCoffee") + object.__setattr__(message.entities[0], "length", len("/pourCoffee")) assert handler.check_update(Update(0, message=message)) assert not self.test_flag @@ -2030,52 +1979,52 @@ def test_callback(u, c): assert not self.test_flag # The user is now ready to start coding - message.text = "/startCoding" - message.entities[0].length = len("/startCoding") + object.__setattr__(message, "text", "/startCoding") + object.__setattr__(message.entities[0], "length", len("/startCoding")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.CODING assert not self.test_flag # The user decides it's time to drink again - message.text = "/drinkMore" - message.entities[0].length = len("/drinkMore") + object.__setattr__(message, "text", "/drinkMore") + object.__setattr__(message.entities[0], "length", len("/drinkMore")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING assert not self.test_flag # The user is holding their cup - message.text = "/hold" - message.entities[0].length = len("/hold") + object.__setattr__(message, "text", "/hold") + object.__setattr__(message.entities[0], "length", len("/hold")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.HOLDING assert not self.test_flag # The user wants to end with the drinking and go back to coding - message.text = "/end" - message.entities[0].length = len("/end") + object.__setattr__(message, "text", "/end") + object.__setattr__(message.entities[0], "length", len("/end")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.END # check that we're in the right state now by checking that the update is accepted - message.text = "/drinkMore" - message.entities[0].length = len("/drinkMore") + object.__setattr__(message, "text", "/drinkMore") + object.__setattr__(message.entities[0], "length", len("/drinkMore")) assert handler.check_update(Update(0, message=message)) assert not self.test_flag # The user wants to drink once more - message.text = "/drinkMore" - message.entities[0].length = len("/drinkMore") + object.__setattr__(message, "text", "/drinkMore") + object.__setattr__(message.entities[0], "length", len("/drinkMore")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING assert not self.test_flag # The user wants to stop altogether - message.text = "/stop" - message.entities[0].length = len("/stop") + object.__setattr__(message, "text", "/stop") + object.__setattr__(message.entities[0], "length", len("/stop")) await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.STOPPING # check that the conv has ended by checking that the start cmd is accepted - message.text = "/start" - message.entities[0].length = len("/start") + object.__setattr__(message, "text", "/start") + object.__setattr__(message.entities[0], "length", len("/start")) assert handler.check_update(Update(0, message=message)) assert not self.test_flag @@ -2297,15 +2246,14 @@ async def blocking(_, __): text="/start", from_user=user1, ) - message._unfreeze() async with app: await app.process_update(Update(0, message=message)) assert not self.test_flag - message.text = "1" + object.__setattr__(message, "text", "1") await app.process_update(Update(0, message=message)) assert self.test_flag == 1 - message.text = "2" + object.__setattr__(message, "text", "2") await app.process_update(Update(0, message=message)) assert self.test_flag == 2 event.set() diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index 8b101b73f4c..50fdb7a7dc9 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -42,6 +42,7 @@ ) from telegram.ext import filters from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen @pytest.fixture @@ -58,15 +59,17 @@ def update(): forward_origin=MessageOriginUser(dtm.datetime.utcnow(), User(0, "Testuser", False)), ), ) - update._unfreeze() - update.message._unfreeze() - update.message.chat._unfreeze() - update.message.from_user._unfreeze() - update.message.via_bot._unfreeze() - update.message.sender_chat._unfreeze() - update.message.forward_origin._unfreeze() - update.message.forward_origin.sender_user._unfreeze() - return update + with ( + unfrozen(update), + unfrozen(update.message), + unfrozen(update.message.chat), + unfrozen(update.message.from_user), + unfrozen(update.message.via_bot), + unfrozen(update.message.sender_chat), + unfrozen(update.message.forward_origin), + unfrozen(update.message.forward_origin.sender_user), + ): + yield update @pytest.fixture(params=MessageEntity.ALL_TYPES) @@ -650,114 +653,121 @@ def test_filters_document_type(self, update): update.message.document = Document( "file_id", "unique_id", mime_type="application/vnd.android.package-archive" ) - update.message.document._unfreeze() assert filters.Document.APK.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.DOC.check_update(update) assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = "application/msword" + object.__setattr__(update.message.document, "mime_type", "application/msword") assert filters.Document.DOC.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.DOCX.check_update(update) assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = ( - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + object.__setattr__( + update.message.document, + "mime_type", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ) assert filters.Document.DOCX.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.EXE.check_update(update) assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = "application/octet-stream" + object.__setattr__(update.message.document, "mime_type", "application/octet-stream") # Python 3.15 changes the "exe" mime type to application/vnd.microsoft.portable-executable if int(platform.python_version_tuple()[1]) <= 14: assert filters.Document.EXE.check_update(update) else: assert not filters.Document.EXE.check_update(update) - update.message.document.mime_type = "application/vnd.microsoft.portable-executable" + object.__setattr__( + update.message.document, + "mime_type", + "application/vnd.microsoft.portable-executable", + ) assert filters.Document.EXE.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.DOCX.check_update(update) assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = "image/gif" + object.__setattr__(update.message.document, "mime_type", "image/gif") assert filters.Document.GIF.check_update(update) assert filters.Document.IMAGE.check_update(update) assert not filters.Document.JPG.check_update(update) assert not filters.Document.TEXT.check_update(update) - update.message.document.mime_type = "image/jpeg" + object.__setattr__(update.message.document, "mime_type", "image/jpeg") assert filters.Document.JPG.check_update(update) assert filters.Document.IMAGE.check_update(update) assert not filters.Document.MP3.check_update(update) assert not filters.Document.VIDEO.check_update(update) - update.message.document.mime_type = "audio/mpeg" + object.__setattr__(update.message.document, "mime_type", "audio/mpeg") assert filters.Document.MP3.check_update(update) assert filters.Document.AUDIO.check_update(update) assert not filters.Document.PDF.check_update(update) assert not filters.Document.IMAGE.check_update(update) - update.message.document.mime_type = "application/pdf" + object.__setattr__(update.message.document, "mime_type", "application/pdf") assert filters.Document.PDF.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.PY.check_update(update) assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = "text/x-python" + object.__setattr__(update.message.document, "mime_type", "text/x-python") assert filters.Document.PY.check_update(update) assert filters.Document.TEXT.check_update(update) assert not filters.Document.SVG.check_update(update) assert not filters.Document.APPLICATION.check_update(update) - update.message.document.mime_type = "image/svg+xml" + object.__setattr__(update.message.document, "mime_type", "image/svg+xml") assert filters.Document.SVG.check_update(update) assert filters.Document.IMAGE.check_update(update) assert not filters.Document.TXT.check_update(update) assert not filters.Document.VIDEO.check_update(update) - update.message.document.mime_type = "text/plain" + object.__setattr__(update.message.document, "mime_type", "text/plain") assert filters.Document.TXT.check_update(update) assert filters.Document.TEXT.check_update(update) assert not filters.Document.TARGZ.check_update(update) assert not filters.Document.APPLICATION.check_update(update) - update.message.document.mime_type = "application/x-compressed-tar" + object.__setattr__(update.message.document, "mime_type", "application/x-compressed-tar") assert filters.Document.TARGZ.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.WAV.check_update(update) assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = ( - "audio/x-wav" if int(platform.python_version_tuple()[1]) < 14 else "audio/vnd.wave" + object.__setattr__( + update.message.document, + "mime_type", + ("audio/x-wav" if int(platform.python_version_tuple()[1]) < 14 else "audio/vnd.wave"), ) assert filters.Document.WAV.check_update(update) assert filters.Document.AUDIO.check_update(update) assert not filters.Document.XML.check_update(update) assert not filters.Document.IMAGE.check_update(update) - update.message.document.mime_type = "text/xml" + object.__setattr__(update.message.document, "mime_type", "text/xml") assert filters.Document.XML.check_update(update) assert filters.Document.TEXT.check_update(update) assert not filters.Document.ZIP.check_update(update) assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = "application/zip" + object.__setattr__(update.message.document, "mime_type", "application/zip") assert filters.Document.ZIP.check_update(update) assert filters.Document.APPLICATION.check_update(update) assert not filters.Document.APK.check_update(update) assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = "image/x-rgb" + object.__setattr__(update.message.document, "mime_type", "image/x-rgb") assert not filters.Document.Category("application/").check_update(update) assert not filters.Document.MimeType("application/x-sh").check_update(update) - update.message.document.mime_type = "application/x-sh" + object.__setattr__(update.message.document, "mime_type", "application/x-sh") assert filters.Document.Category("application/").check_update(update) assert filters.Document.MimeType("application/x-sh").check_update(update) - update.message.document.mime_type = None + object.__setattr__(update.message.document, "mime_type", None) assert not filters.Document.Category("application/").check_update(update) assert not filters.Document.MimeType("application/x-sh").check_update(update) @@ -768,21 +778,20 @@ def test_filters_file_extension_basic(self, update): file_name="file.jpg", mime_type="image/jpeg", ) - update.message.document._unfreeze() assert filters.Document.FileExtension("jpg").check_update(update) assert not filters.Document.FileExtension("jpeg").check_update(update) assert not filters.Document.FileExtension("file.jpg").check_update(update) - update.message.document.file_name = "file.tar.gz" + object.__setattr__(update.message.document, "file_name", "file.tar.gz") assert filters.Document.FileExtension("tar.gz").check_update(update) assert filters.Document.FileExtension("gz").check_update(update) assert not filters.Document.FileExtension("tgz").check_update(update) assert not filters.Document.FileExtension("jpg").check_update(update) - update.message.document.file_name = None + object.__setattr__(update.message.document, "file_name", None) assert not filters.Document.FileExtension("jpg").check_update(update) - update.message.document = None + object.__setattr__(update.message, "document", None) assert not filters.Document.FileExtension("jpg").check_update(update) def test_filters_file_extension_minds_dots(self, update): @@ -792,27 +801,26 @@ def test_filters_file_extension_minds_dots(self, update): file_name="file.jpg", mime_type="image/jpeg", ) - update.message.document._unfreeze() assert not filters.Document.FileExtension(".jpg").check_update(update) assert not filters.Document.FileExtension("e.jpg").check_update(update) assert not filters.Document.FileExtension("file.jpg").check_update(update) assert not filters.Document.FileExtension("").check_update(update) - update.message.document.file_name = "file..jpg" + object.__setattr__(update.message.document, "file_name", "file..jpg") assert filters.Document.FileExtension("jpg").check_update(update) assert filters.Document.FileExtension(".jpg").check_update(update) assert not filters.Document.FileExtension("..jpg").check_update(update) - update.message.document.file_name = "file.docx" + object.__setattr__(update.message.document, "file_name", "file.docx") assert filters.Document.FileExtension("docx").check_update(update) assert not filters.Document.FileExtension("doc").check_update(update) assert not filters.Document.FileExtension("ocx").check_update(update) - update.message.document.file_name = "file" + object.__setattr__(update.message.document, "file_name", "file") assert not filters.Document.FileExtension("").check_update(update) assert not filters.Document.FileExtension("file").check_update(update) - update.message.document.file_name = "file." + object.__setattr__(update.message.document, "file_name", "file.") assert filters.Document.FileExtension("").check_update(update) def test_filters_file_extension_none_arg(self, update): @@ -822,17 +830,16 @@ def test_filters_file_extension_none_arg(self, update): file_name="file.jpg", mime_type="image/jpeg", ) - update.message.document._unfreeze() assert not filters.Document.FileExtension(None).check_update(update) - update.message.document.file_name = "file" + object.__setattr__(update.message.document, "file_name", "file") assert filters.Document.FileExtension(None).check_update(update) assert not filters.Document.FileExtension("None").check_update(update) - update.message.document.file_name = "file." + object.__setattr__(update.message.document, "file_name", "file.") assert not filters.Document.FileExtension(None).check_update(update) - update.message.document = None + object.__setattr__(update.message, "document", None) assert not filters.Document.FileExtension(None).check_update(update) def test_filters_file_extension_case_sensitivity(self, update): @@ -842,15 +849,14 @@ def test_filters_file_extension_case_sensitivity(self, update): file_name="file.jpg", mime_type="image/jpeg", ) - update.message.document._unfreeze() assert filters.Document.FileExtension("JPG").check_update(update) assert filters.Document.FileExtension("jpG").check_update(update) - update.message.document.file_name = "file.JPG" + object.__setattr__(update.message.document, "file_name", "file.JPG") assert filters.Document.FileExtension("jpg").check_update(update) assert not filters.Document.FileExtension("jpg", case_sensitive=True).check_update(update) - update.message.document.file_name = "file.Dockerfile" + object.__setattr__(update.message.document, "file_name", "file.Dockerfile") assert filters.Document.FileExtension("Dockerfile", case_sensitive=True).check_update( update ) @@ -892,23 +898,24 @@ def test_filters_photo(self, update): def test_filters_sticker(self, update): assert not filters.Sticker.ALL.check_update(update) update.message.sticker = Sticker("1", "uniq", 1, 2, False, False, Sticker.REGULAR) - update.message.sticker._unfreeze() assert filters.Sticker.ALL.check_update(update) assert filters.Sticker.STATIC.check_update(update) assert not filters.Sticker.VIDEO.check_update(update) assert not filters.Sticker.PREMIUM.check_update(update) - update.message.sticker.is_animated = True + object.__setattr__(update.message.sticker, "is_animated", True) assert filters.Sticker.ANIMATED.check_update(update) assert not filters.Sticker.VIDEO.check_update(update) assert not filters.Sticker.STATIC.check_update(update) assert not filters.Sticker.PREMIUM.check_update(update) - update.message.sticker.is_animated = False - update.message.sticker.is_video = True + object.__setattr__(update.message.sticker, "is_animated", False) + object.__setattr__(update.message.sticker, "is_video", True) assert not filters.Sticker.ANIMATED.check_update(update) assert not filters.Sticker.STATIC.check_update(update) assert filters.Sticker.VIDEO.check_update(update) assert not filters.Sticker.PREMIUM.check_update(update) - update.message.sticker.premium_animation = File("string", "uniqueString") + object.__setattr__( + update.message.sticker, "premium_animation", File("string", "uniqueString") + ) assert not filters.Sticker.ANIMATED.check_update(update) # premium stickers can be animated, video, or probably also static, # it doesn't really matter for the test diff --git a/tests/ext/test_inlinequeryhandler.py b/tests/ext/test_inlinequeryhandler.py index a1dd87fdc35..7edc69d2a16 100644 --- a/tests/ext/test_inlinequeryhandler.py +++ b/tests/ext/test_inlinequeryhandler.py @@ -35,6 +35,7 @@ ) from telegram.ext import CallbackContext, InlineQueryHandler, JobQueue from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -80,9 +81,8 @@ def inline_query(bot): location=Location(latitude=-23.691288, longitude=-46.788279), ), ) - update._unfreeze() - update.inline_query._unfreeze() - return update + with unfrozen(update), unfrozen(update.inline_query): + yield update class TestInlineQueryHandler: @@ -147,10 +147,10 @@ async def test_context_pattern(self, app, inline_query): update = Update( update_id=0, inline_query=InlineQuery(id="id", from_user=None, query="", offset="") ) - update.inline_query._unfreeze() - assert not handler.check_update(update) - update.inline_query.query = "not_a_match" - assert not handler.check_update(update) + with unfrozen(update.inline_query): + assert not handler.check_update(update) + update.inline_query.query = "not_a_match" + assert not handler.check_update(update) @pytest.mark.parametrize( ("query", "expected_result"), diff --git a/tests/ext/test_managedbotupdatedhandler.py b/tests/ext/test_managedbotupdatedhandler.py index 9fe5781d1d2..52efcacf265 100644 --- a/tests/ext/test_managedbotupdatedhandler.py +++ b/tests/ext/test_managedbotupdatedhandler.py @@ -36,6 +36,7 @@ from telegram._utils.datetime import UTC from telegram.ext import CallbackContext, JobQueue, ManagedBotUpdatedHandler from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -159,9 +160,9 @@ def test_with_username(self, managed_bot_updated_update): handler = ManagedBotUpdatedHandler(self.callback, username=["@user_b"]) assert not handler.check_update(managed_bot_updated_update) - managed_bot_updated_update.managed_bot.user._unfreeze() - managed_bot_updated_update.managed_bot.user.username = None - assert not handler.check_update(managed_bot_updated_update) + with unfrozen(managed_bot_updated_update.managed_bot.user): + managed_bot_updated_update.managed_bot.user.username = None + assert not handler.check_update(managed_bot_updated_update) def test_other_update_types(self, false_update): handler = ManagedBotUpdatedHandler(self.callback) diff --git a/tests/ext/test_messagehandler.py b/tests/ext/test_messagehandler.py index 8df5de7d1ef..e480195ca0e 100644 --- a/tests/ext/test_messagehandler.py +++ b/tests/ext/test_messagehandler.py @@ -36,6 +36,7 @@ from telegram.ext import CallbackContext, JobQueue, MessageHandler, filters from telegram.ext.filters import MessageFilter from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -66,10 +67,9 @@ def false_update(request): @pytest.fixture(scope="class") def message(bot): message = Message(1, None, Chat(1, ""), from_user=User(1, "", False)) - message._unfreeze() - message.chat._unfreeze() message.set_bot(bot) - return message + with unfrozen(message), unfrozen(message.chat): + yield message class TestMessageHandler: diff --git a/tests/ext/test_messagereactionhandler.py b/tests/ext/test_messagereactionhandler.py index 7de7f135ea6..50164b0a384 100644 --- a/tests/ext/test_messagereactionhandler.py +++ b/tests/ext/test_messagereactionhandler.py @@ -39,6 +39,7 @@ from telegram._utils.datetime import UTC from telegram.ext import CallbackContext, JobQueue, MessageReactionHandler from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -89,10 +90,8 @@ def message_reaction_updated(time, bot): actor_chat=Chat(2, Chat.SUPERGROUP), ) mr.set_bot(bot) - mr._unfreeze() - mr.chat._unfreeze() - mr.user._unfreeze() - return mr + with unfrozen(mr), unfrozen(mr.chat), unfrozen(mr.user): + yield mr @pytest.fixture(scope="class") @@ -107,9 +106,8 @@ def message_reaction_count_updated(time, bot): ], ) mr.set_bot(bot) - mr._unfreeze() - mr.chat._unfreeze() - return mr + with unfrozen(mr), unfrozen(mr.chat): + yield mr @pytest.fixture diff --git a/tests/ext/test_paidmediapurchasedhandler.py b/tests/ext/test_paidmediapurchasedhandler.py index 059c8e264ca..7eb04cd0a09 100644 --- a/tests/ext/test_paidmediapurchasedhandler.py +++ b/tests/ext/test_paidmediapurchasedhandler.py @@ -36,6 +36,7 @@ from telegram._utils.datetime import UTC from telegram.ext import CallbackContext, JobQueue, PaidMediaPurchasedHandler from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -151,9 +152,9 @@ def test_with_username(self, purchased_paid_media_update): handler = PaidMediaPurchasedHandler(self.callback, username=["@user_b"]) assert not handler.check_update(purchased_paid_media_update) - purchased_paid_media_update.purchased_paid_media.from_user._unfreeze() - purchased_paid_media_update.purchased_paid_media.from_user.username = None - assert not handler.check_update(purchased_paid_media_update) + with unfrozen(purchased_paid_media_update.purchased_paid_media.from_user): + purchased_paid_media_update.purchased_paid_media.from_user.username = None + assert not handler.check_update(purchased_paid_media_update) def test_other_update_types(self, false_update): handler = PaidMediaPurchasedHandler(self.callback) diff --git a/tests/ext/test_picklepersistence.py b/tests/ext/test_picklepersistence.py index 5c1128ad326..c2cb1131860 100644 --- a/tests/ext/test_picklepersistence.py +++ b/tests/ext/test_picklepersistence.py @@ -16,16 +16,19 @@ # # 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 dataclasses import datetime as dtm import gzip import os import pickle import sys from pathlib import Path +from typing import Any import pytest from telegram import Chat, Message, TelegramObject, Update, User +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram.ext import ContextTypes, PersistenceInput, PicklePersistence from telegram.warnings import PTBUserWarning from tests.auxil.files import SOURCE_ROOT_PATH @@ -238,20 +241,15 @@ class TestPicklePersistence: """Just tests the PicklePersistence interface. Integration of persistence into Applictation is tested in TestBasePersistence!""" - class DictSub(TelegramObject): # Used for testing our custom (Un)Pickler. - def __init__(self, private, normal, b): - super().__init__() - self._private = private - self.normal = normal - self._bot = b + @dataclasses.dataclass(frozen=True, slots=False) + class DictSub(TelegramObject): + _private: Any + normal: Any + @tg_dataclass() class SlotsSub(TelegramObject): - __slots__ = ("_private", "new_var") - - def __init__(self, new_var, private): - super().__init__() - self.new_var = new_var - self._private = private + new_var: Any = tg_field() + _private: Any = tg_field(alias="private") class NormalClass: def __init__(self, my_var): @@ -928,7 +926,9 @@ async def test_custom_pickler_unpickler_with_custom_objects( ): bot = cdc_bot - dict_s = self.DictSub("private", "normal", bot) + dict_s = self.DictSub("private", "normal") + dict_s.set_bot(bot) + slot_s = self.SlotsSub("new_var", "private_var") regular = self.NormalClass(12) diff --git a/tests/ext/test_precheckoutqueryhandler.py b/tests/ext/test_precheckoutqueryhandler.py index ab558c499be..ba5143b998d 100644 --- a/tests/ext/test_precheckoutqueryhandler.py +++ b/tests/ext/test_precheckoutqueryhandler.py @@ -35,6 +35,7 @@ ) from telegram.ext import CallbackContext, JobQueue, PreCheckoutQueryHandler from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text") @@ -76,9 +77,8 @@ def pre_checkout_query(): "id", User(1, "test user", False), "EUR", 223, "invoice_payload" ), ) - update._unfreeze() - update.pre_checkout_query._unfreeze() - return update + with unfrozen(update), unfrozen(update.pre_checkout_query): + yield update class TestPreCheckoutQueryHandler: diff --git a/tests/test_bot.py b/tests/test_bot.py index 58b57d3d469..fa630590e32 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -90,9 +90,11 @@ WebAppInfo, ) from telegram._payment.stars.staramount import StarAmount +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import UTC, from_timestamp, localize, to_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.strings import to_camel_case +from telegram._utils.types import ODVInput from telegram.constants import ( ChatAction, InlineQueryLimit, @@ -113,6 +115,7 @@ from tests.auxil.networking import OfflineRequest, expect_bad_request from tests.auxil.pytest_classes import PytestBot, PytestExtBot, make_bot from tests.auxil.slots import mro_slots +from tests.conftest import unfrozen from .auxil.build_messages import make_message from .auxil.dummy_objects import get_dummy_object @@ -207,6 +210,7 @@ def bot_methods(ext_bot=True, include_camel_case=False, include_do_api_request=F ) +@tg_dataclass() class InputMessageContentLPO(InputMessageContent): """ This is here to cover the case of InputMediaContent classes in testing answer_ilq that have @@ -214,19 +218,8 @@ class InputMessageContentLPO(InputMessageContent): than sorry … """ - __slots__ = ("entities", "link_preview_options", "message_text", "parse_mode") - - def __init__( - self, - message_text: str, - link_preview_options=DEFAULT_NONE, - *, - api_kwargs=None, - ): - super().__init__(api_kwargs=api_kwargs) - self._unfreeze() - self.message_text = message_text - self.link_preview_options = link_preview_options + message_text: str = tg_field() + link_preview_options: ODVInput[LinkPreviewOptions] = tg_field(default=DEFAULT_NONE) class TestBotWithoutRequest: @@ -1994,9 +1987,10 @@ async def test_arbitrary_callback_data_pinned_message_reply_to_message( get_dummy_object(Chat), reply_markup=offline_bot.callback_data_cache.process_keyboard(reply_markup), ) - message._unfreeze() # We do to_dict -> de_json to make sure those aren't the same objects - message.pinned_message = Message.de_json(message.to_dict(), offline_bot) + object.__setattr__( + message, "pinned_message", Message.de_json(message.to_dict(), offline_bot) + ) async def post(*args, **kwargs): update = Update( @@ -3438,10 +3432,10 @@ async def test_send_close_date_default_tz(self, tz_bot, super_group_id): close_date=close_date, read_timeout=60, ) - msg.poll._unfreeze() - # Sometimes there can be a few seconds delay, so don't let the test fail due to that- - msg.poll.close_date = msg.poll.close_date.astimezone(aware_close_date.tzinfo) - assert abs(msg.poll.close_date - aware_close_date) <= dtm.timedelta(seconds=5) + with unfrozen(msg.poll): + # Sometimes there can be a few seconds delay, so don't let the test fail due to that- + msg.poll.close_date = msg.poll.close_date.astimezone(aware_close_date.tzinfo) + assert abs(msg.poll.close_date - aware_close_date) <= dtm.timedelta(seconds=5) await asyncio.sleep(5.1) diff --git a/tests/test_callbackquery.py b/tests/test_callbackquery.py index 734e72d1970..8a0095b9c81 100644 --- a/tests/test_callbackquery.py +++ b/tests/test_callbackquery.py @@ -37,6 +37,7 @@ check_shortcut_call, check_shortcut_signature, ) +from tests.conftest import unfrozen @pytest.fixture(params=["message", "inline", "inaccessible_message"]) @@ -49,18 +50,18 @@ def callback_query(bot, request): game_short_name=CallbackQueryTestBase.game_short_name, ) cbq.set_bot(bot) - cbq._unfreeze() - if request.param == "message": - cbq.message = CallbackQueryTestBase.message - cbq.message.set_bot(bot) - elif request.param == "inline": - cbq.inline_message_id = CallbackQueryTestBase.inline_message_id - elif request.param == "inaccessible_message": - cbq.message = InaccessibleMessage( - chat=CallbackQueryTestBase.message.chat, - message_id=CallbackQueryTestBase.message.message_id, - ) - return cbq + with unfrozen(cbq): + if request.param == "message": + cbq.message = CallbackQueryTestBase.message + cbq.message.set_bot(bot) + elif request.param == "inline": + cbq.inline_message_id = CallbackQueryTestBase.inline_message_id + elif request.param == "inaccessible_message": + cbq.message = InaccessibleMessage( + chat=CallbackQueryTestBase.message.chat, + message_id=CallbackQueryTestBase.message.message_id, + ) + yield cbq class CallbackQueryTestBase: diff --git a/tests/test_chat.py b/tests/test_chat.py index 24a99b2404e..07cc837dbb3 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -36,6 +36,7 @@ check_shortcut_call, check_shortcut_signature, ) +from tests.conftest import unfrozen @pytest.fixture(scope="module") @@ -51,8 +52,8 @@ def chat(bot): is_direct_messages=ChatTestBase.is_direct_messages, ) chat.set_bot(bot) - chat._unfreeze() - return chat + with unfrozen(chat): + yield chat class ChatTestBase: diff --git a/tests/test_chatfullinfo.py b/tests/test_chatfullinfo.py index 69e5e71f437..484b6ae5cc7 100644 --- a/tests/test_chatfullinfo.py +++ b/tests/test_chatfullinfo.py @@ -41,6 +41,7 @@ from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import ReactionEmoji from telegram.warnings import PTBDeprecationWarning +from tests.conftest import unfrozen @pytest.fixture(scope="module") @@ -97,8 +98,8 @@ def chat_full_info(bot): first_profile_audio=ChatFullInfoTestBase.first_profile_audio, ) chat.set_bot(bot) - chat._unfreeze() - return chat + with unfrozen(chat): + yield chat # Shortcut methods are tested in test_chat.py. diff --git a/tests/test_choseninlineresult.py b/tests/test_choseninlineresult.py index dc5b2405cbd..7c04ea8c774 100644 --- a/tests/test_choseninlineresult.py +++ b/tests/test_choseninlineresult.py @@ -20,13 +20,14 @@ import pytest from telegram import ChosenInlineResult, Location, User, Voice +from tests.conftest import unfrozen @pytest.fixture(scope="module") def user(): user = User(1, "First name", False) - user._unfreeze() - return user + with unfrozen(user): + yield user @pytest.fixture(scope="module") diff --git a/tests/test_directmessagestopic.py b/tests/test_directmessagestopic.py index 609c5424141..0bbfeed3af8 100644 --- a/tests/test_directmessagestopic.py +++ b/tests/test_directmessagestopic.py @@ -21,6 +21,7 @@ import pytest from telegram import DirectMessagesTopic, User +from tests.conftest import unfrozen @pytest.fixture(scope="module") @@ -30,8 +31,8 @@ def direct_messages_topic(offline_bot): user=DirectMessagesTopicTestBase.user, ) dmt.set_bot(offline_bot) - dmt._unfreeze() - return dmt + with unfrozen(dmt): + yield dmt class DirectMessagesTopicTestBase: diff --git a/tests/test_message.py b/tests/test_message.py index 7f111729de6..260c310e21a 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -123,6 +123,7 @@ from tests.auxil.build_messages import make_message from tests.auxil.dummy_objects import get_dummy_object_json_dict from tests.auxil.pytest_classes import PytestExtBot, PytestMessage +from tests.conftest import unfrozen @pytest.fixture @@ -136,10 +137,8 @@ def message(bot): guest_query_id="706654132", ) message.set_bot(bot) - message._unfreeze() - message.chat._unfreeze() - message.from_user._unfreeze() - return message + with unfrozen(message), unfrozen(message.chat), unfrozen(message.from_user): + yield message @pytest.fixture( @@ -3122,7 +3121,8 @@ async def make_assertion(*_, **kwargs): @pytest.mark.parametrize("business_connection_id", [None, "123456789"]) async def test_delete(self, monkeypatch, message, business_connection_id): message = deepcopy(message) - message.business_connection_id = business_connection_id + with unfrozen(message): + message.business_connection_id = business_connection_id async def make_assertion(*_, **kwargs): url: str = kwargs.get("url") diff --git a/tests/test_poll.py b/tests/test_poll.py index 8c5641d8e00..439c5659871 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -45,6 +45,7 @@ from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import PollType from telegram.warnings import PTBDeprecationWarning +from tests.conftest import unfrozen @pytest.fixture(scope="module") @@ -55,8 +56,8 @@ def input_poll_option(): text_entities=InputPollOptionTestBase.text_entities, media=InputPollOptionTestBase.media, ) - out._unfreeze() - return out + with unfrozen(out): + yield out class InputPollOptionTestBase: @@ -230,8 +231,8 @@ def poll_option(): persistent_id=PollOptionTestBase.persistent_id, media=PollOptionTestBase.media, ) - out._unfreeze() - return out + with unfrozen(out): + yield out class PollOptionTestBase: @@ -432,8 +433,8 @@ def poll(): media=PollTestBase.media, explanation_media=PollTestBase.explanation_media, ) - poll._unfreeze() - return poll + with unfrozen(poll): + yield poll class PollTestBase: @@ -790,8 +791,8 @@ def poll_option_added(): option_text=PollOptionAddedTestBase.option_text, option_text_entities=PollOptionAddedTestBase.option_text_entities, ) - p._unfreeze() - return p + with unfrozen(p): + yield p class PollOptionAddedTestBase: @@ -902,8 +903,8 @@ def poll_option_deleted(): option_text=PollOptionDeletedTestBase.option_text, option_text_entities=PollOptionDeletedTestBase.option_text_entities, ) - p._unfreeze() - return p + with unfrozen(p): + yield p class PollOptionDeletedTestBase: diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index 4dafec0bf73..36b589516c2 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -20,7 +20,6 @@ import datetime as dtm import inspect import pickle -import re from collections.abc import Sequence from copy import deepcopy from pathlib import Path @@ -46,6 +45,7 @@ from telegram.ext import PicklePersistence from telegram.warnings import PTBUserWarning from tests.auxil.files import data_file +from tests.conftest import unfrozen def all_subclasses(cls): @@ -340,11 +340,10 @@ class SubClass(TelegramObject): def test_api_kwargs_read_only(self): tg_object = TelegramObject(api_kwargs={"foo": "bar"}) - tg_object._freeze() assert isinstance(tg_object.api_kwargs, MappingProxyType) with pytest.raises(TypeError): tg_object.api_kwargs["foo"] = "baz" - with pytest.raises(AttributeError, match="can't be set"): + with pytest.raises(dataclasses.FrozenInstanceError): tg_object.api_kwargs = {"foo": "baz"} # tags: deprecated next.version @@ -408,8 +407,8 @@ def test_to_dict_api_kwargs(self): def test_to_dict_missing_attribute(self): message = Message(1, dtm.datetime.now(), Chat(1, "private"), from_user=User(1, "", False)) - message._unfreeze() - del message.chat + with unfrozen(message): + del message.chat message_dict = message.to_dict() assert "chat" not in message_dict @@ -584,7 +583,7 @@ async def test_pickle_backwards_compatibility(self): # Ensure that loading objects that were pickled before attributes were made immutable # are still mutable - chat.id = 7 + object.__setattr__(chat, "id", 7) assert chat.id == 7 def test_pickle_handle_properties(self): @@ -734,12 +733,8 @@ class TGO(TelegramObject): @pytest.mark.parametrize("cls", TO_SUBCLASSES, ids=[cls.__name__ for cls in TO_SUBCLASSES]) def test_subclasses_are_frozen(self, cls): - if cls is TelegramObject or cls.__name__.startswith("_"): - # Protected classes don't need to be frozen and neither does the base class - return - # instantiating each subclass would be tedious as some attributes require special init - # args. So we inspect the code instead. + # args. So we check the dataclass type `cls` instead. source_file = inspect.getsourcefile(cls.__init__) parents = Path(source_file).parents @@ -749,51 +744,5 @@ def test_subclasses_are_frozen(self, cls): # If the class is defined in a test file, we don't want to test it. return - if source_file.endswith("telegramobject.py"): - pytest.fail( - f"{cls.__name__} does not have its own `__init__` " - "and can therefore not be frozen correctly" - ) - - source_lines, _ = inspect.getsourcelines(cls.__init__) - - # We use regex matching since a simple "if self._freeze() in source_lines[-1]" would also - # allo commented lines. - last_line_freezes = re.match(r"\s*self\.\_freeze\(\)", source_lines[-1]) - uses_with_unfrozen = re.search( - r"\n\s*with self\.\_unfrozen\(\)\:", inspect.getsource(cls.__init__) - ) - - assert last_line_freezes or uses_with_unfrozen, f"{cls.__name__} is not frozen correctly" - - def test_freeze_unfreeze(self): - class TestSub(TelegramObject): - def __init__(self): - super().__init__() - self._protected = True - self.public = True - self._freeze() - - foo = TestSub() - foo._protected = False - assert foo._protected is False - - with pytest.raises( - AttributeError, match="Attribute `public` of class `TestSub` can't be set!" - ): - foo.public = False - - with pytest.raises( - AttributeError, match="Attribute `public` of class `TestSub` can't be deleted!" - ): - del foo.public - - foo._unfreeze() - foo._protected = True - assert foo._protected is True - foo.public = False - assert foo.public is False - del foo.public - del foo._protected - assert not hasattr(foo, "public") - assert not hasattr(foo, "_protected") + assert dataclasses.is_dataclass(cls) + assert cls.__dataclass_params__.frozen diff --git a/tests/test_update.py b/tests/test_update.py index dbde30a3205..a1f44acd645 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -350,14 +350,11 @@ def test_effective_sender_non_anonymous(self, update): update = deepcopy(update) # Simulate 'Remain anonymous' being turned off if message := (update.message or update.edited_message or update.guest_message): - message._unfreeze() - message.sender_chat = None + object.__setattr__(message, "sender_chat", None) elif reaction := (update.message_reaction): - reaction._unfreeze() - reaction.actor_chat = None + object.__setattr__(reaction, "actor_chat", None) elif answer := (update.poll_answer): - answer._unfreeze() - answer.voter_chat = None + object.__setattr__(answer, "voter_chat", None) # Test that it's sometimes None per docstring sender = update.effective_sender @@ -383,14 +380,11 @@ def test_effective_sender_anonymous(self, update): update = deepcopy(update) # Simulate 'Remain anonymous' being turned on if message := (update.message or update.edited_message or update.guest_message): - message._unfreeze() - message.from_user = None + object.__setattr__(message, "from_user", None) elif reaction := (update.message_reaction): - reaction._unfreeze() - reaction.user = None + object.__setattr__(reaction, "user", None) elif answer := (update.poll_answer): - answer._unfreeze() - answer.user = None + object.__setattr__(answer, "user", None) # Test that it's sometimes None per docstring sender = update.effective_sender diff --git a/tests/test_user.py b/tests/test_user.py index f67bfed2125..41ae9c25570 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -25,6 +25,7 @@ check_shortcut_call, check_shortcut_signature, ) +from tests.conftest import unfrozen @pytest.fixture(scope="module") @@ -72,8 +73,8 @@ def user(bot): supports_guest_queries=UserTestBase.supports_guest_queries, ) user.set_bot(bot) - user._unfreeze() - return user + with unfrozen(user): + yield user class UserTestBase: From cf6485bc75d9f74305d6b25712d1a8cc054bec56 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:12:26 +0300 Subject: [PATCH 24/31] Migrate `_inputmedia` classes --- src/telegram/_files/inputmedia.py | 851 +++++++++++++++--------------- 1 file changed, 425 insertions(+), 426 deletions(-) diff --git a/src/telegram/_files/inputmedia.py b/src/telegram/_files/inputmedia.py index f33301ee5e6..6993b9a06bd 100644 --- a/src/telegram/_files/inputmedia.py +++ b/src/telegram/_files/inputmedia.py @@ -19,9 +19,11 @@ """Base classes for Telegram InputMedia, InputPaidMedia, InputPollMedia and InputPollOptionMedia Objects.""" +import dataclasses import datetime as dtm from collections.abc import Sequence -from typing import TYPE_CHECKING, Final, TypeAlias +from dataclasses import InitVar +from typing import ClassVar, TypeAlias, cast from telegram import constants from telegram._files.animation import Animation @@ -35,18 +37,48 @@ from telegram._telegramobject import TelegramObject from telegram._utils import enum from telegram._utils.argumentparsing import parse_sequence_arg, to_timedelta +from telegram._utils.dataclass import tg_dataclass, tg_field from telegram._utils.datetime import get_timedelta_value from telegram._utils.defaultvalue import DEFAULT_NONE from telegram._utils.files import parse_file_input -from telegram._utils.types import JSONDict, ODVInput, TimePeriod +from telegram._utils.types import FileInput, JSONDict, ODVInput from telegram._utils.warnings import warn from telegram.constants import BaseInputMediaType from telegram.warnings import PTBDeprecationWarning -if TYPE_CHECKING: - from telegram._utils.types import FileInput +def _parse_animation_input(value: "FileInput | Animation") -> str | InputFile: + return parse_file_input(value, Animation, attach=True, local_mode=True) + +def _parse_audio_input(value: "FileInput | Audio") -> str | InputFile: + return parse_file_input(value, Audio, attach=True, local_mode=True) + + +def _parse_document_input(value: "FileInput | Document") -> str | InputFile: + return parse_file_input(value, Document, attach=True, local_mode=True) + + +def _parse_photo_input(value: "FileInput | PhotoSize") -> str | InputFile: + return parse_file_input(value, PhotoSize, attach=True, local_mode=True) + + +def _parse_sticker_input(value: "FileInput | Sticker") -> str | InputFile: + return parse_file_input(value, Sticker, attach=True, local_mode=True) + + +def _parse_video_input(value: "FileInput | Video") -> str | InputFile: + return parse_file_input(value, Video, attach=True, local_mode=True) + + +def _parse_optional_file_input(value: FileInput | None) -> str | InputFile | None: + if value is None: + return None + + return parse_file_input(value, attach=True, local_mode=True) + + +@tg_dataclass() class _BaseInputMedia(TelegramObject): """ Base class for objects representing the various input media types. @@ -58,18 +90,14 @@ class _BaseInputMedia(TelegramObject): type (:obj:`str`): Type of media that the instance represents. """ - __slots__ = ("type",) + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.BaseInputMediaType, value, value) - def __init__( - self, - media_type: str, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.BaseInputMediaType, media_type, media_type) + type: str = tg_field(converter=_type_converter, alias="media_type") +@dataclasses.dataclass(frozen=True, slots=False, repr=False, eq=False, match_args=False) class InputMedia(_BaseInputMedia): """ This object represents the content of a media message to be sent. It should be one of: @@ -87,6 +115,11 @@ class InputMedia(_BaseInputMedia): .. seealso:: :wiki:`Working with Files and Media ` + .. deprecated:: NEXT.VERSION + The arguments and attributes :attr:`caption`, :attr:`caption_entities` + and :attr:`parse_mode` are deprecated for direct instances of :class:`InputMedia` and will + be removed. They remain available on the concrete media classes. + Args: media_type (:obj:`str`): Type of media that the instance represents. media (:obj:`str` | :class:`~telegram.InputFile`): File to send. @@ -117,6 +150,11 @@ class InputMedia(_BaseInputMedia): """ + # tags: deprecated NEXT.VERSION + # Use automatic slots and __init__, and remove optional fields + # We currently define explicit slots instead of dataclass fields + # to allow subclasses to control parameter ordering + __slots__ = ("caption", "caption_entities", "media", "parse_mode") def __init__( @@ -128,25 +166,20 @@ def __init__( parse_mode: ODVInput[str] = DEFAULT_NONE, *, api_kwargs: JSONDict | None = None, - ): + ) -> None: super().__init__(media_type=media_type, api_kwargs=api_kwargs) - with self._unfrozen(): - self.media: str | InputFile = media - self.caption: str | None = caption - self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities) - self.parse_mode: ODVInput[str] = parse_mode - @staticmethod - def _parse_thumbnail_input(thumbnail: "FileInput | None") -> str | InputFile | None: - # We use local_mode=True because we don't have access to the actual setting and want - # things to work in local mode. - return ( - parse_file_input(thumbnail, attach=True, local_mode=True) - if thumbnail is not None - else thumbnail - ) + self.media: str | InputFile + object.__setattr__(self, "media", media) + self.caption: str | None + object.__setattr__(self, "caption", caption) + self.caption_entities: tuple[MessageEntity, ...] + object.__setattr__(self, "caption_entities", parse_sequence_arg(caption_entities)) + self.parse_mode: ODVInput[str] + object.__setattr__(self, "parse_mode", parse_mode) +@tg_dataclass() class InputPaidMedia(TelegramObject): """ Base class for Telegram InputPaidMedia Objects. Currently, it can be one of: @@ -169,32 +202,25 @@ class InputPaidMedia(TelegramObject): media (:obj:`str` | :class:`telegram.InputFile`): Media to send. """ - PHOTO: Final[str] = constants.InputPaidMediaType.PHOTO + PHOTO: ClassVar[str] = constants.InputPaidMediaType.PHOTO """:const:`telegram.constants.InputPaidMediaType.PHOTO`""" - VIDEO: Final[str] = constants.InputPaidMediaType.VIDEO + VIDEO: ClassVar[str] = constants.InputPaidMediaType.VIDEO """:const:`telegram.constants.InputPaidMediaType.VIDEO`""" - LIVE_PHOTO: Final[str] = constants.InputPaidMediaType.LIVE_PHOTO + LIVE_PHOTO: ClassVar[str] = constants.InputPaidMediaType.LIVE_PHOTO """:const:`telegram.constants.InputPaidMediaType.LIVE_PHOTO` .. versionadded:: 22.8 """ - __slots__ = ("media", "type") - - def __init__( - self, - type: str, # pylint: disable=redefined-builtin - media: str | InputFile, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(api_kwargs=api_kwargs) - self.type: str = enum.get_member(constants.InputPaidMediaType, type, type) - self.media: str | InputFile = media + @staticmethod + def _type_converter(value: str) -> str: + return enum.get_member(constants.InputPaidMediaType, value, value) - self._freeze() + type: str = tg_field(converter=_type_converter) + media: str | InputFile = tg_field() +@tg_dataclass() class InputPaidMediaPhoto(InputPaidMedia): """The paid media to send is a photo. @@ -213,19 +239,13 @@ class InputPaidMediaPhoto(InputPaidMedia): media (:obj:`str` | :class:`telegram.InputFile`): Photo to send. """ - __slots__ = () - - def __init__( - self, - media: "FileInput | PhotoSize", - *, - api_kwargs: JSONDict | None = None, - ): - media = parse_file_input(media, PhotoSize, attach=True, local_mode=True) - super().__init__(type=InputPaidMedia.PHOTO, media=media, api_kwargs=api_kwargs) - self._freeze() + # Attribute only (init=False) + type: str = tg_field(init=False, default=InputPaidMedia.PHOTO) + # Required + media: str | InputFile = tg_field(converter=_parse_photo_input) +@tg_dataclass() class InputPaidMediaVideo(InputPaidMedia): """ The paid media to send is a video. @@ -286,56 +306,44 @@ class InputPaidMediaVideo(InputPaidMedia): suitable for streaming. """ - __slots__ = ( - "_duration", - "cover", - "height", - "start_timestamp", - "supports_streaming", - "thumbnail", - "width", + # Attribute only (init=False) + type: str = tg_field(init=False, default=InputPaidMedia.VIDEO) + media: str | InputFile = tg_field(converter=_parse_video_input) + thumbnail: str | InputFile | None = tg_field( + default=None, converter=_parse_optional_file_input + ) + width: int | None = tg_field(default=None) + height: int | None = tg_field(default=None) + _duration: dtm.timedelta | None = tg_field( + default=None, alias="duration", converter=to_timedelta ) + supports_streaming: bool | None = tg_field(default=None) + cover: str | InputFile | None = tg_field(default=None, converter=_parse_optional_file_input) + start_timestamp: int | None = tg_field(default=None) + + def __post_init__(self) -> None: + media = cast("FileInput | Video", self.media) - def __init__( - self, - media: "FileInput | Video", - thumbnail: "FileInput | None" = None, - width: int | None = None, - height: int | None = None, - duration: TimePeriod | None = None, - supports_streaming: bool | None = None, - cover: "FileInput | None" = None, - start_timestamp: int | None = None, - *, - api_kwargs: JSONDict | None = None, - ): if isinstance(media, Video): - width = width if width is not None else media.width - height = height if height is not None else media.height - duration = duration if duration is not None else media._duration - media = media.file_id - else: - # We use local_mode=True because we don't have access to the actual setting and want - # things to work in local mode. - media = parse_file_input(media, attach=True, local_mode=True) - - super().__init__(type=InputPaidMedia.VIDEO, media=media, api_kwargs=api_kwargs) - with self._unfrozen(): - self.thumbnail: str | InputFile | None = InputMedia._parse_thumbnail_input(thumbnail) - self.width: int | None = width - self.height: int | None = height - self._duration: dtm.timedelta | None = to_timedelta(duration) - self.supports_streaming: bool | None = supports_streaming - self.cover: InputFile | str | None = ( - parse_file_input(cover, attach=True, local_mode=True) if cover else None - ) - self.start_timestamp: int | None = start_timestamp + if self.width is None: + object.__setattr__(self, "width", media.width) + if self.height is None: + object.__setattr__(self, "height", media.height) + if self._duration is None: + object.__setattr__( + self, + "_duration", + media._duration, # pylint: disable=protected-access + ) + + InputPaidMedia.__post_init__(self) @property def duration(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._duration, attribute="duration") +@tg_dataclass() class InputPaidMediaLivePhoto(InputPaidMedia): """ The paid media to send is a live photo. @@ -365,22 +373,14 @@ class InputPaidMediaLivePhoto(InputPaidMedia): |fileinputnopath| """ - __slots__ = ("photo",) - - def __init__( - self, - media: "FileInput | Video", - photo: "FileInput | PhotoSize", - *, - api_kwargs: JSONDict | None = None, - ): - media = parse_file_input(media, tg_type=Video, attach=True, local_mode=True) - photo = parse_file_input(photo, tg_type=PhotoSize, attach=True, local_mode=True) - super().__init__(type=InputPaidMedia.LIVE_PHOTO, media=media, api_kwargs=api_kwargs) - with self._unfrozen(): - self.photo: str | InputFile = photo + # Attribute only (init=False) + type: str = tg_field(init=False, default=InputPaidMedia.LIVE_PHOTO) + # Required + media: str | InputFile = tg_field(converter=_parse_video_input) + photo: str | InputFile = tg_field(converter=_parse_photo_input) +@tg_dataclass() class InputMediaAnimation(InputMedia): """Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. @@ -476,34 +476,38 @@ class InputMediaAnimation(InputMedia): .. versionadded:: 21.3 """ - __slots__ = ( - "_duration", - "has_spoiler", - "height", - "show_caption_above_media", - "thumbnail", - "width", + # Attribute only (init=False) + type: str = tg_field(init=False, default=BaseInputMediaType.ANIMATION) + # Required + media: str | InputFile = tg_field(converter=_parse_animation_input) + # Optional + caption: str | None = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + width: int | None = tg_field(default=None) + height: int | None = tg_field(default=None) + _duration: dtm.timedelta | None = tg_field( + default=None, alias="duration", converter=to_timedelta + ) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + # tag: deprecated 22.8 + filename_depr: InitVar[str | None] = tg_field(default=None) + # - + has_spoiler: bool | None = tg_field(default=None) + thumbnail: str | InputFile | None = tg_field( + default=None, converter=_parse_optional_file_input ) + show_caption_above_media: bool | None = tg_field(default=None) + # Keyword only + filename: InitVar[str | None] = tg_field(default=None, kw_only=True) - def __init__( + def __post_init__( self, - media: "FileInput | Animation", - caption: str | None = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - width: int | None = None, - height: int | None = None, - duration: TimePeriod | None = None, - caption_entities: Sequence[MessageEntity] | None = None, - # tag: deprecated 22.8 - filename_depr: str | None = None, - # - - has_spoiler: bool | None = None, - thumbnail: "FileInput | None" = None, - show_caption_above_media: bool | None = None, - *, - filename: str | None = None, - api_kwargs: JSONDict | None = None, - ): + filename_depr: str | None, + filename: str | None, + /, + ) -> None: if filename_depr is not None and filename is not None: raise ValueError("`filename_depr` and `filename` are mutually exclusive.") if filename_depr is not None: @@ -513,43 +517,43 @@ def __init__( "Positional passing of `filename` or keyword usage of `filename_depr`" " is deprecated. `filename` will become a keyword-only argument.", ), - stacklevel=2, + stacklevel=4, ) + media = cast("FileInput | Animation", self.media) + if isinstance(media, Animation): - width = media.width if width is None else width - height = media.height if height is None else height - duration = duration if duration is not None else media._duration - media = media.file_id + if self.width is None: + object.__setattr__(self, "width", media.width) + if self.height is None: + object.__setattr__(self, "height", media.height) + if self._duration is None: + object.__setattr__( + self, + "_duration", + media._duration, # pylint: disable=protected-access + ) else: - # We use local_mode=True because we don't have access to the actual setting and want - # things to work in local mode. effective_filename = filename_depr or filename - media = parse_file_input( - media, filename=effective_filename, attach=True, local_mode=True - ) - - super().__init__( - BaseInputMediaType.ANIMATION, - media, - caption, - caption_entities, - parse_mode, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - self.thumbnail: str | InputFile | None = self._parse_thumbnail_input(thumbnail) - self.width: int | None = width - self.height: int | None = height - self._duration: dtm.timedelta | None = to_timedelta(duration) - self.has_spoiler: bool | None = has_spoiler - self.show_caption_above_media: bool | None = show_caption_above_media + if effective_filename is not None: + # We have to convert here because it requires both `media` and `filename`. + # It will harmlessly run again in the parent __post_init__ + object.__setattr__( + self, + "media", + parse_file_input( + media, filename=effective_filename, attach=True, local_mode=True + ), + ) + + InputMedia.__post_init__(self) @property def duration(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._duration, attribute="duration") +@tg_dataclass() class InputMediaPhoto(InputMedia): """Represents a photo to be sent. @@ -615,26 +619,30 @@ class InputMediaPhoto(InputMedia): .. versionadded:: 21.3 """ - __slots__ = ( - "has_spoiler", - "show_caption_above_media", + # Attribute only + type: str = tg_field(init=False, default=BaseInputMediaType.PHOTO) + # Required + media: str | InputFile = tg_field(converter=_parse_photo_input) + # Optional + caption: str | None = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg ) - - def __init__( + # tag: deprecated 22.8 + filename_depr: InitVar[str | None] = tg_field(default=None) + # - + has_spoiler: bool | None = tg_field(default=None) + show_caption_above_media: bool | None = tg_field(default=None) + # Keyword only + filename: InitVar[str | None] = tg_field(default=None, kw_only=True) + + def __post_init__( self, - media: "FileInput | PhotoSize", - caption: str | None = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - # tag: deprecated 22.8 - filename_depr: str | None = None, - # - - has_spoiler: bool | None = None, - show_caption_above_media: bool | None = None, - *, - filename: str | None = None, - api_kwargs: JSONDict | None = None, - ): + filename_depr: str | None, + filename: str | None, + /, + ) -> None: if filename_depr is not None and filename is not None: raise ValueError("`filename_depr` and `filename` are mutually exclusive.") if filename_depr is not None: @@ -644,29 +652,30 @@ def __init__( "Positional passing of `filename` or keyword usage of `filename_depr`" " is deprecated. `filename` will become a keyword-only argument.", ), - stacklevel=2, + stacklevel=3, ) - # We use local_mode=True because we don't have access to the actual setting and want - # things to work in local mode. effective_filename = filename_depr or filename - media = parse_file_input( - media, PhotoSize, filename=effective_filename, attach=True, local_mode=True - ) - super().__init__( - BaseInputMediaType.PHOTO, - media, - caption, - caption_entities, - parse_mode, - api_kwargs=api_kwargs, - ) - - with self._unfrozen(): - self.has_spoiler: bool | None = has_spoiler - self.show_caption_above_media: bool | None = show_caption_above_media + if effective_filename is not None: + # We have to convert here because it requires both `media` and `filename`. + # It will harmlessly run again in the parent __post_init__ + media = cast("FileInput | PhotoSize", self.media) + object.__setattr__( + self, + "media", + parse_file_input( + media, + tg_type=PhotoSize, + filename=effective_filename, + attach=True, + local_mode=True, + ), + ) + + InputMedia.__post_init__(self) +@tg_dataclass() class InputMediaVideo(InputMedia): """Represents a video to be sent. @@ -781,40 +790,41 @@ class InputMediaVideo(InputMedia): .. versionchanged:: 21.11 """ - __slots__ = ( - "_duration", - "cover", - "has_spoiler", - "height", - "show_caption_above_media", - "start_timestamp", - "supports_streaming", - "thumbnail", - "width", + # Attribute only + type: str = tg_field(init=False, default=BaseInputMediaType.VIDEO) + # Required + media: "str | InputFile" = tg_field(converter=_parse_video_input) + # Optional + caption: str | None = tg_field(default=None) + width: int | None = tg_field(default=None) + height: int | None = tg_field(default=None) + _duration: dtm.timedelta | None = tg_field( + default=None, alias="duration", converter=to_timedelta + ) + supports_streaming: bool | None = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + # tag: deprecated 22.8 + filename_depr: InitVar[str | None] = tg_field(default=None) + # - + has_spoiler: bool | None = tg_field(default=None) + thumbnail: "str | InputFile | None" = tg_field( + default=None, converter=_parse_optional_file_input ) + show_caption_above_media: bool | None = tg_field(default=None) + cover: "str | InputFile | None" = tg_field(default=None, converter=_parse_optional_file_input) + start_timestamp: int | None = tg_field(default=None) + # Keyword only + filename: InitVar[str | None] = tg_field(default=None, kw_only=True) - def __init__( + def __post_init__( self, - media: "FileInput | Video", - caption: str | None = None, - width: int | None = None, - height: int | None = None, - duration: TimePeriod | None = None, - supports_streaming: bool | None = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - # tag: deprecated 22.8 - filename_depr: str | None = None, - # - - has_spoiler: bool | None = None, - thumbnail: "FileInput | None" = None, - show_caption_above_media: bool | None = None, - cover: "FileInput | None" = None, - start_timestamp: int | None = None, - *, - filename: str | None = None, - api_kwargs: JSONDict | None = None, - ): + filename_depr: str | None, + filename: str | None, + /, + ) -> None: if filename_depr is not None and filename is not None: raise ValueError("`filename_depr` and `filename` are mutually exclusive.") if filename_depr is not None: @@ -824,48 +834,43 @@ def __init__( "Positional passing of `filename` or keyword usage of `filename_depr`" " is deprecated. `filename` will become a keyword-only argument.", ), - stacklevel=2, + stacklevel=4, ) + media = cast("FileInput | Video", self.media) + if isinstance(media, Video): - width = width if width is not None else media.width - height = height if height is not None else media.height - duration = duration if duration is not None else media._duration - media = media.file_id + if self.width is None: + object.__setattr__(self, "width", media.width) + if self.height is None: + object.__setattr__(self, "height", media.height) + if self._duration is None: + object.__setattr__( + self, + "_duration", + media._duration, # pylint: disable=protected-access + ) else: - # We use local_mode=True because we don't have access to the actual setting and want - # things to work in local mode. effective_filename = filename_depr or filename - media = parse_file_input( - media, filename=effective_filename, attach=True, local_mode=True - ) - - super().__init__( - BaseInputMediaType.VIDEO, - media, - caption, - caption_entities, - parse_mode, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - self.width: int | None = width - self.height: int | None = height - self._duration: dtm.timedelta | None = to_timedelta(duration) - self.thumbnail: str | InputFile | None = self._parse_thumbnail_input(thumbnail) - self.supports_streaming: bool | None = supports_streaming - self.has_spoiler: bool | None = has_spoiler - self.show_caption_above_media: bool | None = show_caption_above_media - self.cover: InputFile | str | None = ( - parse_file_input(cover, attach=True, local_mode=True) if cover else None - ) - self.start_timestamp: int | None = start_timestamp + if effective_filename is not None: + # We have to convert here because it requires both `media` and `filename`. + # It will harmlessly run again in the parent __post_init__ + object.__setattr__( + self, + "media", + parse_file_input( + media, filename=effective_filename, attach=True, local_mode=True + ), + ) + + InputMedia.__post_init__(self) @property def duration(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._duration, attribute="duration") +@tg_dataclass() class InputMediaLocation(_BaseInputMedia): """Represents a location to be sent. @@ -885,23 +890,16 @@ class InputMediaLocation(_BaseInputMedia): measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`. """ - __slots__ = ("horizontal_accuracy", "latitude", "longitude") - - def __init__( - self, - latitude: float, - longitude: float, - horizontal_accuracy: float | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(media_type=BaseInputMediaType.LOCATION, api_kwargs=api_kwargs) - with self._unfrozen(): - self.latitude: float = latitude - self.longitude: float = longitude - self.horizontal_accuracy: float | None = horizontal_accuracy + # Attribute only (init=False) + type: str = tg_field(init=False, default=BaseInputMediaType.LOCATION) + # Required + latitude: float = tg_field() + longitude: float = tg_field() + # Optional + horizontal_accuracy: float | None = tg_field(default=None) +@tg_dataclass() class InputMediaVenue(_BaseInputMedia): """Represents a venue to be sent. @@ -935,42 +933,21 @@ class InputMediaVenue(_BaseInputMedia): `supported types `__) """ - __slots__ = ( - "address", - "foursquare_id", - "foursquare_type", - "google_place_id", - "google_place_type", - "latitude", - "longitude", - "title", - ) - - def __init__( - self, - latitude: float, - longitude: float, - title: str, - address: str, - foursquare_id: str | None = None, - foursquare_type: str | None = None, - google_place_id: str | None = None, - google_place_type: str | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - super().__init__(media_type=BaseInputMediaType.VENUE, api_kwargs=api_kwargs) - with self._unfrozen(): - self.latitude: float = latitude - self.longitude: float = longitude - self.title: str = title - self.address: str = address - self.foursquare_id: str | None = foursquare_id - self.foursquare_type: str | None = foursquare_type - self.google_place_id: str | None = google_place_id - self.google_place_type: str | None = google_place_type - - + # Attribute only (init=False) + type: str = tg_field(init=False, default=BaseInputMediaType.VENUE) + # Required + latitude: float = tg_field() + longitude: float = tg_field() + title: str = tg_field() + address: str = tg_field() + # Optional + foursquare_id: str | None = tg_field(default=None) + foursquare_type: str | None = tg_field(default=None) + google_place_id: str | None = tg_field(default=None) + google_place_type: str | None = tg_field(default=None) + + +@tg_dataclass() class InputMediaSticker(_BaseInputMedia): """Represents a sticker file to be sent. @@ -998,24 +975,39 @@ class InputMediaSticker(_BaseInputMedia): stickers. """ - __slots__ = ("emoji", "media") + # Attribute only (init=False) + type: str = tg_field(init=False, default=BaseInputMediaType.STICKER) + # Required + media: "str | InputFile" = tg_field(converter=_parse_sticker_input) + # Optional + emoji: str | None = tg_field(default=None) + # Keyword only + filename: InitVar[str | None] = tg_field(default=None, kw_only=True) - def __init__( + def __post_init__( self, - media: "FileInput | Sticker", - emoji: str | None = None, - *, - filename: str | None = None, - api_kwargs: JSONDict | None = None, - ): - media = parse_file_input(media, Sticker, filename=filename, attach=True, local_mode=True) + filename: str | None, + /, + ) -> None: + if filename is not None: + # We have to convert here because it requires both `media` and `filename`. + # It will harmlessly run again in the parent __post_init__ + object.__setattr__( + self, + "media", + parse_file_input( + self.media, + tg_type=Sticker, + filename=filename, + attach=True, + local_mode=True, + ), + ) - super().__init__(media_type=BaseInputMediaType.STICKER, api_kwargs=api_kwargs) - with self._unfrozen(): - self.media: str | InputFile = media - self.emoji: str | None = emoji + _BaseInputMedia.__post_init__(self) +@tg_dataclass() class InputMediaAudio(InputMedia): """Represents an audio file to be treated as music to be sent. @@ -1100,25 +1092,36 @@ class InputMediaAudio(InputMedia): """ - __slots__ = ("_duration", "performer", "thumbnail", "title") + # Attribute only (init=False) + type: str = tg_field(init=False, default=BaseInputMediaType.AUDIO) + # Required + media: "str | InputFile" = tg_field(converter=_parse_audio_input) + # Optional + caption: str | None = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + _duration: dtm.timedelta | None = tg_field( + default=None, alias="duration", converter=to_timedelta + ) + performer: str | None = tg_field(default=None) + title: str | None = tg_field(default=None) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + # tag: deprecated 22.8 + filename_depr: InitVar[str | None] = tg_field(default=None) + # - + thumbnail: "str | InputFile | None" = tg_field( + default=None, converter=_parse_optional_file_input + ) + # Keyword only + filename: InitVar[str | None] = tg_field(default=None, kw_only=True) - def __init__( + def __post_init__( self, - media: "FileInput | Audio", - caption: str | None = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - duration: TimePeriod | None = None, - performer: str | None = None, - title: str | None = None, - caption_entities: Sequence[MessageEntity] | None = None, - # tag: deprecated 22.8 - filename_depr: str | None = None, - # - - thumbnail: "FileInput | None" = None, - *, - filename: str | None = None, - api_kwargs: JSONDict | None = None, - ): + filename_depr: str | None, + filename: str | None, + /, + ) -> None: if filename_depr is not None and filename is not None: raise ValueError("`filename_depr` and `filename` are mutually exclusive.") if filename_depr is not None: @@ -1128,41 +1131,43 @@ def __init__( "Positional passing of `filename` or keyword usage of `filename_depr`" " is deprecated. `filename` will become a keyword-only argument.", ), - stacklevel=2, + stacklevel=4, ) + media = cast("FileInput | Audio", self.media) + if isinstance(media, Audio): - duration = duration if duration is not None else media._duration - performer = media.performer if performer is None else performer - title = media.title if title is None else title - media = media.file_id + if self._duration is None: + object.__setattr__( + self, + "_duration", + media._duration, # pylint: disable=protected-access + ) + if self.performer is None: + object.__setattr__(self, "performer", media.performer) + if self.title is None: + object.__setattr__(self, "title", media.title) else: - # We use local_mode=True because we don't have access to the actual setting and want - # things to work in local mode. effective_filename = filename_depr or filename - media = parse_file_input( - media, filename=effective_filename, attach=True, local_mode=True - ) - - super().__init__( - BaseInputMediaType.AUDIO, - media, - caption, - caption_entities, - parse_mode, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - self.thumbnail: str | InputFile | None = self._parse_thumbnail_input(thumbnail) - self._duration: dtm.timedelta | None = to_timedelta(duration) - self.title: str | None = title - self.performer: str | None = performer + if effective_filename is not None: + # We have to convert here because it requires both `media` and `filename`. + # It will harmlessly run again in the parent __post_init__ + object.__setattr__( + self, + "media", + parse_file_input( + media, filename=effective_filename, attach=True, local_mode=True + ), + ) + + InputMedia.__post_init__(self) @property def duration(self) -> int | dtm.timedelta | None: return get_timedelta_value(self._duration, attribute="duration") +@tg_dataclass() class InputMediaDocument(InputMedia): """Represents a general file to be sent. @@ -1231,23 +1236,32 @@ class InputMediaDocument(InputMedia): .. versionadded:: 20.2 """ - __slots__ = ("disable_content_type_detection", "thumbnail") + # Attribute only (init=False) + type: str = tg_field(init=False, default=BaseInputMediaType.DOCUMENT) + # Required + media: "str | InputFile" = tg_field(converter=_parse_document_input) + # Optional + caption: str | None = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + disable_content_type_detection: bool | None = tg_field(default=None) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + # tag: deprecated 22.8 + filename_depr: InitVar[str | None] = tg_field(default=None) + # - + thumbnail: "str | InputFile | None" = tg_field( + default=None, converter=_parse_optional_file_input + ) + # Keyword only + filename: InitVar[str | None] = tg_field(default=None, kw_only=True) - def __init__( + def __post_init__( self, - media: "FileInput | Document", - caption: str | None = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - disable_content_type_detection: bool | None = None, - caption_entities: Sequence[MessageEntity] | None = None, - # tag: deprecated 22.8 - filename_depr: str | None = None, - # - - thumbnail: "FileInput | None" = None, - *, - filename: str | None = None, - api_kwargs: JSONDict | None = None, - ): + filename_depr: str | None, + filename: str | None, + /, + ) -> None: if filename_depr is not None and filename is not None: raise ValueError("`filename_depr` and `filename` are mutually exclusive.") if filename_depr is not None: @@ -1257,29 +1271,30 @@ def __init__( "Positional passing of `filename` or keyword usage of `filename_depr`" " is deprecated. `filename` will become a keyword-only argument.", ), - stacklevel=2, + stacklevel=3, ) - # We use local_mode=True because we don't have access to the actual setting and want - # things to work in local mode. effective_filename = filename_depr or filename - media = parse_file_input( - media, Document, filename=effective_filename, attach=True, local_mode=True - ) - - super().__init__( - BaseInputMediaType.DOCUMENT, - media, - caption, - caption_entities, - parse_mode, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - self.thumbnail: str | InputFile | None = self._parse_thumbnail_input(thumbnail) - self.disable_content_type_detection: bool | None = disable_content_type_detection + if effective_filename is not None: + # We have to convert here because it requires both `media` and `filename`. + # It will harmlessly run again in the parent __post_init__ + media = cast("FileInput | Document", self.media) + object.__setattr__( + self, + "media", + parse_file_input( + media, + tg_type=Document, + filename=effective_filename, + attach=True, + local_mode=True, + ), + ) + InputMedia.__post_init__(self) + +@tg_dataclass() class InputMediaLivePhoto(InputMedia): """Represents a live photo to be sent. @@ -1321,35 +1336,19 @@ class InputMediaLivePhoto(InputMedia): spoiler animation. """ - __slots__ = ("has_spoiler", "photo", "show_caption_above_media") - - def __init__( - self, - media: "FileInput | Video", - photo: "FileInput | PhotoSize", - caption: str | None = None, - parse_mode: ODVInput[str] = DEFAULT_NONE, - caption_entities: Sequence[MessageEntity] | None = None, - show_caption_above_media: bool | None = None, - has_spoiler: bool | None = None, - *, - api_kwargs: JSONDict | None = None, - ): - media = parse_file_input(media, tg_type=Video, attach=True, local_mode=True) - photo = parse_file_input(photo, tg_type=PhotoSize, attach=True, local_mode=True) - - super().__init__( - BaseInputMediaType.LIVE_PHOTO, - media, - caption, - caption_entities, - parse_mode, - api_kwargs=api_kwargs, - ) - with self._unfrozen(): - self.photo: str | InputFile = photo - self.show_caption_above_media: bool | None = show_caption_above_media - self.has_spoiler: bool | None = has_spoiler + # Attribute only (init=False) + type: str = tg_field(init=False, default=BaseInputMediaType.LIVE_PHOTO) + # Required + media: "str | InputFile" = tg_field(converter=_parse_video_input) + photo: "str | InputFile" = tg_field(converter=_parse_photo_input) + # Optional + caption: str | None = tg_field(default=None) + parse_mode: ODVInput[str] = tg_field(default=DEFAULT_NONE) + caption_entities: tuple[MessageEntity, ...] = tg_field( + default=None, converter=parse_sequence_arg + ) + show_caption_above_media: bool | None = tg_field(default=None) + has_spoiler: bool | None = tg_field(default=None) InputPollMedia: TypeAlias = ( From 908a07b6b89c1d45767e272383c5507361025b52 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Mon, 21 Sep 2026 01:29:26 +0300 Subject: [PATCH 25/31] Simplify `passportelementerrors` migration --- .../_passport/passportelementerrors.py | 185 +++++++----------- 1 file changed, 68 insertions(+), 117 deletions(-) diff --git a/src/telegram/_passport/passportelementerrors.py b/src/telegram/_passport/passportelementerrors.py index aa7e1f431e8..1b873a75f12 100644 --- a/src/telegram/_passport/passportelementerrors.py +++ b/src/telegram/_passport/passportelementerrors.py @@ -19,7 +19,7 @@ # pylint: disable=redefined-builtin """This module contains the classes that represent Telegram PassportElementError.""" -from collections.abc import Sequence +import dataclasses from telegram._telegramobject import TelegramObject from telegram._utils.argumentparsing import parse_sequence_arg @@ -28,7 +28,13 @@ @tg_dataclass() -class PassportElementError(TelegramObject): +class _BasePassportElementError(TelegramObject): + source: str = tg_field(compare=True) + type: str = tg_field(compare=True) + + +@dataclasses.dataclass(frozen=True, slots=False, repr=False, eq=False, match_args=False) +class PassportElementError(_BasePassportElementError): """Baseclass for the PassportElementError* classes. This object represents an error in the Telegram Passport element which was submitted that @@ -49,22 +55,24 @@ class PassportElementError(TelegramObject): """ - # Required - source: str = tg_field(compare=True) - type: str = tg_field(compare=True) # tags: deprecated NEXT.VERSION - # Include in initalizer for next relase - message: str = tg_field(init=False) + # Use automatic slots and __init__ + # We currently define explicit slots instead of dataclass fields + # to allow subclasses to control `message` parameter ordering + + __slots__ = ("message",) - # tags: deprecated NEXT.VERSION - # Remove manual __init__ def __init__( - self, source: str, type: str, message: str, *, api_kwargs: JSONDict | None = None + self, + source: str, + type: str, + message: str, + *, + api_kwargs: JSONDict | None = None, ) -> None: - object.__setattr__(self, "source", str(source)) - object.__setattr__(self, "type", str(type)) + super().__init__(source, type, api_kwargs=api_kwargs) + object.__setattr__(self, "message", str(message)) - TelegramObject.__init__(self, api_kwargs=api_kwargs) @tg_dataclass() @@ -77,6 +85,10 @@ class PassportElementErrorDataField(PassportElementError): considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`field_name`, :attr:`data_hash` and :attr:`message` are equal. + .. deprecated:: NEXT.VERSION + Passing :paramref:`message` positionally is deprecated. Its position will change in + a future release. + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the error, one of ``"personal_details"``, ``"passport"``, ``"driver_license"``, ``"identity_card"``, @@ -102,22 +114,7 @@ class PassportElementErrorDataField(PassportElementError): data_hash: str = tg_field(compare=True) # tags: deprecated NEXT.VERSION # Remove for next relase - message: str = tg_field(compare=True, init=False) - - # tags: deprecated NEXT.VERSION - # Remove manual __init__, present here to preserve ordering - def __init__( - self, - type: str, - field_name: str, - data_hash: str, - message: str, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - PassportElementError.__init__(self, "data", type, message, api_kwargs=api_kwargs) - object.__setattr__(self, "field_name", field_name) - object.__setattr__(self, "data_hash", data_hash) + message: str = tg_field(compare=True) @tg_dataclass() @@ -130,6 +127,10 @@ class PassportElementErrorFile(PassportElementError): considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. + .. deprecated:: NEXT.VERSION + Passing :paramref:`message` positionally is deprecated. Its position will change in + a future release. + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, @@ -152,15 +153,7 @@ class PassportElementErrorFile(PassportElementError): file_hash: str = tg_field(compare=True) # tags: deprecated NEXT.VERSION # Remove for next relase - message: str = tg_field(compare=True, init=False) - - # tags: deprecated NEXT.VERSION - # Remove manual __init__, present here to preserve ordering - def __init__( - self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ) -> None: - PassportElementError.__init__(self, "file", type, message, api_kwargs=api_kwargs) - object.__setattr__(self, "file_hash", file_hash) + message: str = tg_field(compare=True) @tg_dataclass() @@ -173,6 +166,10 @@ class PassportElementErrorFiles(PassportElementError): considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hashes`, and :attr:`message` are equal. + .. deprecated:: NEXT.VERSION + Passing :paramref:`message` positionally is deprecated. Its position will change in + a future release. + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"utility_bill"``, ``"bank_statement"``, ``"rental_agreement"``, @@ -198,23 +195,10 @@ class PassportElementErrorFiles(PassportElementError): # Attribute only (init=False) source: str = tg_field(compare=True, init=False, default="files") # Required - file_hashes: tuple[str, ...] = tg_field(compare=True) + file_hashes: tuple[str, ...] = tg_field(compare=True, converter=parse_sequence_arg) # tags: deprecated NEXT.VERSION # Remove for next relase - message: str = tg_field(compare=True, init=False) - - # tags: deprecated NEXT.VERSION - # Remove manual __init__, present here to preserve ordering - def __init__( - self, - type: str, - file_hashes: Sequence[str], - message: str, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - PassportElementError.__init__(self, "files", type, message, api_kwargs=api_kwargs) - object.__setattr__(self, "file_hashes", parse_sequence_arg(file_hashes)) + message: str = tg_field(compare=True) @tg_dataclass() @@ -227,6 +211,10 @@ class PassportElementErrorFrontSide(PassportElementError): considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. + .. deprecated:: NEXT.VERSION + Passing :paramref:`message` positionally is deprecated. Its position will change in + a future release. + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. @@ -249,15 +237,7 @@ class PassportElementErrorFrontSide(PassportElementError): file_hash: str = tg_field(compare=True) # tags: deprecated NEXT.VERSION # Remove for next relase - message: str = tg_field(compare=True, init=False) - - # tags: deprecated NEXT.VERSION - # Remove manual __init__, present here to preserve ordering - def __init__( - self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ) -> None: - PassportElementError.__init__(self, "front_side", type, message, api_kwargs=api_kwargs) - object.__setattr__(self, "file_hash", file_hash) + message: str = tg_field(compare=True) @tg_dataclass() @@ -270,6 +250,10 @@ class PassportElementErrorReverseSide(PassportElementError): considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. + .. deprecated:: NEXT.VERSION + Passing :paramref:`message` positionally is deprecated. Its position will change in + a future release. + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"driver_license"``, ``"identity_card"``. @@ -292,15 +276,7 @@ class PassportElementErrorReverseSide(PassportElementError): file_hash: str = tg_field(compare=True) # tags: deprecated NEXT.VERSION # Remove for next relase - message: str = tg_field(compare=True, init=False) - - # tags: deprecated NEXT.VERSION - # Remove manual __init__, present here to preserve ordering - def __init__( - self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ) -> None: - PassportElementError.__init__(self, "reverse_side", type, message, api_kwargs=api_kwargs) - object.__setattr__(self, "file_hash", file_hash) + message: str = tg_field(compare=True) @tg_dataclass() @@ -313,6 +289,10 @@ class PassportElementErrorSelfie(PassportElementError): considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. + .. deprecated:: NEXT.VERSION + Passing :paramref:`message` positionally is deprecated. Its position will change in + a future release. + Args: type (:obj:`str`): The section of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, ``"internal_passport"``. @@ -333,15 +313,7 @@ class PassportElementErrorSelfie(PassportElementError): file_hash: str = tg_field(compare=True) # tags: deprecated NEXT.VERSION # Remove for next relase - message: str = tg_field(compare=True, init=False) - - # tags: deprecated NEXT.VERSION - # Remove manual __init__, present here to preserve ordering - def __init__( - self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ) -> None: - PassportElementError.__init__(self, "selfie", type, message, api_kwargs=api_kwargs) - object.__setattr__(self, "file_hash", file_hash) + message: str = tg_field(compare=True) @tg_dataclass() @@ -354,6 +326,10 @@ class PassportElementErrorTranslationFile(PassportElementError): considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hash`, and :attr:`message` are equal. + .. deprecated:: NEXT.VERSION + Passing :paramref:`message` positionally is deprecated. Its position will change in + a future release. + Args: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, @@ -378,17 +354,7 @@ class PassportElementErrorTranslationFile(PassportElementError): file_hash: str = tg_field(compare=True) # tags: deprecated NEXT.VERSION # Remove for next relase - message: str = tg_field(compare=True, init=False) - - # tags: deprecated NEXT.VERSION - # Remove manual __init__, present here to preserve ordering - def __init__( - self, type: str, file_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ) -> None: - PassportElementError.__init__( - self, "translation_file", type, message, api_kwargs=api_kwargs - ) - object.__setattr__(self, "file_hash", file_hash) + message: str = tg_field(compare=True) @tg_dataclass() @@ -401,6 +367,10 @@ class PassportElementErrorTranslationFiles(PassportElementError): considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`file_hashes`, and :attr:`message` are equal. + .. deprecated:: NEXT.VERSION + Passing :paramref:`message` positionally is deprecated. Its position will change in + a future release. + Args: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue, one of ``"passport"``, ``"driver_license"``, ``"identity_card"``, @@ -428,25 +398,10 @@ class PassportElementErrorTranslationFiles(PassportElementError): # Attribute only (init=False) source: str = tg_field(compare=True, init=False, default="translation_files") # Required - file_hashes: tuple[str, ...] = tg_field(compare=True) + file_hashes: tuple[str, ...] = tg_field(compare=True, converter=parse_sequence_arg) # tags: deprecated NEXT.VERSION # Remove for next relase - message: str = tg_field(compare=True, init=False) - - # tags: deprecated NEXT.VERSION - # Remove manual __init__, present here to preserve ordering - def __init__( - self, - type: str, - file_hashes: Sequence[str], - message: str, - *, - api_kwargs: JSONDict | None = None, - ) -> None: - PassportElementError.__init__( - self, "translation_files", type, message, api_kwargs=api_kwargs - ) - object.__setattr__(self, "file_hashes", parse_sequence_arg(file_hashes)) + message: str = tg_field(compare=True) @tg_dataclass() @@ -459,6 +414,10 @@ class PassportElementErrorUnspecified(PassportElementError): considered equal, if their :attr:`~telegram.PassportElementError.source`, :attr:`type`, :attr:`element_hash`, and :attr:`message` are equal. + .. deprecated:: NEXT.VERSION + Passing :paramref:`message` positionally is deprecated. Its position will change in + a future release. + Args: type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue. element_hash (:obj:`str`): Base64-encoded element hash. @@ -477,12 +436,4 @@ class PassportElementErrorUnspecified(PassportElementError): element_hash: str = tg_field(compare=True) # tags: deprecated NEXT.VERSION # Remove for next relase - message: str = tg_field(compare=True, init=False) - - # tags: deprecated NEXT.VERSION - # Remove manual __init__, present here to preserve ordering - def __init__( - self, type: str, element_hash: str, message: str, *, api_kwargs: JSONDict | None = None - ) -> None: - PassportElementError.__init__(self, "unspecified", type, message, api_kwargs=api_kwargs) - object.__setattr__(self, "element_hash", element_hash) + message: str = tg_field(compare=True) From bf850c98ba7ac946bc19ac4d5a7584c596ff5aa0 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 22 Sep 2026 02:04:34 +0300 Subject: [PATCH 26/31] Restore TO hashing behaviour and add tests for it I temporarily broke old TO.__hash__ behaviour that used identity hashing when two objects had no _id_attrs (i.e empty tuple). Not sure why it was that way since empty id attrs mean their equall in terms of __eq__ (yes we did issue a warning but still), so they should have same hash. Anyways that should be restored now because otherwis it would be a breaking change. --- src/telegram/_utils/dataclass.py | 7 +++-- tests/test_telegramobject.py | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/telegram/_utils/dataclass.py b/src/telegram/_utils/dataclass.py index 9d8ac5778de..22790889a89 100644 --- a/src/telegram/_utils/dataclass.py +++ b/src/telegram/_utils/dataclass.py @@ -224,8 +224,11 @@ def decorate(cls: type[_T]) -> type[_T]: ) def __hash__(self: object) -> int: - values = tuple(getattr(self, name) for name in compare_fields) - return hash((self.__class__, values)) + if compare_fields: + values = tuple(getattr(self, name) for name in compare_fields) + return hash((self.__class__, values)) + + return object.__hash__(self) transformed_cls.__hash__ = __hash__ # type: ignore[method-assign] diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index 36b589516c2..72a0b68dd26 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -475,6 +475,58 @@ def __init__(self): assert b == a assert len(recwarn) == 0 + def test_hash_without_comparison_fields_uses_identity(self, recwarn): + @tg_dataclass() + class TGO(TelegramObject): + arg: str = tg_field(compare=False) + + a = TGO("test") + b = TGO("test") + assert a == b + + ab_set = {a, b} + assert len(ab_set) == 2 + + ab_dict = { + a: "a", + b: "b", + } + assert ab_dict[a] == "a" + assert ab_dict[b] == "b" + + def test_hash_with_comparison_fields_uses_field_values(self, recwarn): + @tg_dataclass() + class TGO(TelegramObject): + arg: str = tg_field(compare=True) + + a = TGO("test") + b = TGO("test") + assert a == b + + ab_set = {a, b} + assert len(ab_set) == 1 + + ab_dict = { + a: 1, + b: 2, + } + assert ab_dict[a] == 2 + assert ab_dict[b] == 2 + assert len(ab_dict) == 1 + + c = TGO("not test") + + ac_set = {a, c} + assert len(ac_set) == 2 + + ac_dict = { + a: 1, + c: 3, + } + assert ac_dict[a] == 1 + assert ac_dict[c] == 3 + assert len(ac_dict) == 2 + def test_bot_instance_none(self): tg_object = TelegramObject() with pytest.raises(RuntimeError): From 557c9b93082d933f4ac646997124f0a5bdf4fdbb Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 22 Sep 2026 02:25:57 +0300 Subject: [PATCH 27/31] Restore equality for direct TO instances --- src/telegram/_telegramobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py index 97119483290..46f284b3e4a 100644 --- a/src/telegram/_telegramobject.py +++ b/src/telegram/_telegramobject.py @@ -47,7 +47,7 @@ _DATETIME_FIELD = object() # Sentinel that marks datetime fields in the de_json plan. -@tg_dataclass(eq=False) +@tg_dataclass() class TelegramObject: """Base class for most Telegram objects. From 2aa34211a771dba4525500053be2bea7bd074419 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 22 Sep 2026 02:23:11 +0300 Subject: [PATCH 28/31] Fix minor bugs in some field definitions --- src/telegram/_botaccesssettings.py | 4 +++- src/telegram/_botcommandscope.py | 8 ++++---- src/telegram/_chatboost.py | 8 ++++---- src/telegram/_forcereply.py | 2 +- src/telegram/_menubutton.py | 2 +- src/telegram/_passport/passportdata.py | 6 ++---- src/telegram/_payment/stars/revenuewithdrawalstate.py | 4 ++-- src/telegram/_payment/stars/transactionpartner.py | 6 +++--- src/telegram/_poll.py | 6 +++--- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/telegram/_botaccesssettings.py b/src/telegram/_botaccesssettings.py index 6071c2eae40..df2c8826ab5 100644 --- a/src/telegram/_botaccesssettings.py +++ b/src/telegram/_botaccesssettings.py @@ -48,4 +48,6 @@ class BotAccessSettings(TelegramObject): """ is_access_restricted: bool = tg_field(compare=True) - added_users: tuple[User, ...] = tg_field(compare=True, converter=parse_sequence_arg) + added_users: tuple[User, ...] = tg_field( + compare=True, default=None, converter=parse_sequence_arg + ) diff --git a/src/telegram/_botcommandscope.py b/src/telegram/_botcommandscope.py index d0c9a81d4e7..867cf5504cd 100644 --- a/src/telegram/_botcommandscope.py +++ b/src/telegram/_botcommandscope.py @@ -110,7 +110,7 @@ class BotCommandScopeDefault(BotCommandScope): """ # Attribute only (init=False) - type: str = tg_field(init=False, default=BotCommandScope.DEFAULT) + type: str = tg_field(compare=True, init=False, default=BotCommandScope.DEFAULT) @tg_dataclass() @@ -124,7 +124,7 @@ class BotCommandScopeAllPrivateChats(BotCommandScope): """ # Attribute only (init=False) - type: str = tg_field(init=False, default=BotCommandScope.ALL_PRIVATE_CHATS) + type: str = tg_field(compare=True, init=False, default=BotCommandScope.ALL_PRIVATE_CHATS) @tg_dataclass() @@ -136,7 +136,7 @@ class BotCommandScopeAllGroupChats(BotCommandScope): type (:obj:`str`): Scope type :tg-const:`telegram.BotCommandScope.ALL_GROUP_CHATS`. """ - type: str = tg_field(init=False, default=BotCommandScope.ALL_GROUP_CHATS) + type: str = tg_field(compare=True, init=False, default=BotCommandScope.ALL_GROUP_CHATS) @tg_dataclass() @@ -149,7 +149,7 @@ class BotCommandScopeAllChatAdministrators(BotCommandScope): """ # Attribute only (init=False) - type: str = tg_field(init=False, default=BotCommandScope.ALL_CHAT_ADMINISTRATORS) + type: str = tg_field(compare=True, init=False, default=BotCommandScope.ALL_CHAT_ADMINISTRATORS) @tg_dataclass() diff --git a/src/telegram/_chatboost.py b/src/telegram/_chatboost.py index 5f4b03f1567..bdac1984042 100644 --- a/src/telegram/_chatboost.py +++ b/src/telegram/_chatboost.py @@ -97,7 +97,7 @@ class ChatBoostSource(TelegramObject): def _source_converter(value: str) -> str: return enum.get_member(constants.ChatBoostSources, value, value) - source: str = tg_field(compare=True, default=None, converter=_source_converter) + source: str = tg_field(compare=True, converter=_source_converter) @tg_dataclass() @@ -118,7 +118,7 @@ class ChatBoostSourcePremium(ChatBoostSource): """ # Attribute only (init=False) - source: str = tg_field(init=False, default=ChatBoostSource.PREMIUM) + source: str = tg_field(compare=True, init=False, default=ChatBoostSource.PREMIUM) user: User = tg_field() @@ -142,7 +142,7 @@ class ChatBoostSourceGiftCode(ChatBoostSource): """ # Attribute only (init=False) - source: str = tg_field(init=False, default=ChatBoostSource.GIFT_CODE) + source: str = tg_field(compare=True, init=False, default=ChatBoostSource.GIFT_CODE) user: User = tg_field() @@ -184,7 +184,7 @@ class ChatBoostSourceGiveaway(ChatBoostSource): """ # Attribute only (init=False) - source: str = tg_field(init=False, default=ChatBoostSource.GIVEAWAY) + source: str = tg_field(compare=True, init=False, default=ChatBoostSource.GIVEAWAY) giveaway_message_id: int = tg_field() user: User | None = tg_field(default=None) diff --git a/src/telegram/_forcereply.py b/src/telegram/_forcereply.py index 14956dabbb0..b5c63981bd0 100644 --- a/src/telegram/_forcereply.py +++ b/src/telegram/_forcereply.py @@ -80,7 +80,7 @@ class ForceReply(TelegramObject): # Attribute only (init=False) force_reply: bool = tg_field(init=False, default=True) - selective: bool | None = tg_field(compare=True) + selective: bool | None = tg_field(compare=True, default=None) input_field_placeholder: str | None = tg_field(default=None) MIN_INPUT_FIELD_PLACEHOLDER: ClassVar[int] = constants.ReplyLimit.MIN_INPUT_FIELD_PLACEHOLDER diff --git a/src/telegram/_menubutton.py b/src/telegram/_menubutton.py index 7032af8430b..fcb6fc2196d 100644 --- a/src/telegram/_menubutton.py +++ b/src/telegram/_menubutton.py @@ -87,7 +87,7 @@ class MenuButtonCommands(MenuButton): """ # Attribute only (init=False) - type: str = tg_field(init=False, default=MenuButton.COMMANDS) + type: str = tg_field(compare=True, init=False, default=MenuButton.COMMANDS) @tg_dataclass() diff --git a/src/telegram/_passport/passportdata.py b/src/telegram/_passport/passportdata.py index 0ed374d7f6d..6974e0419c5 100644 --- a/src/telegram/_passport/passportdata.py +++ b/src/telegram/_passport/passportdata.py @@ -63,10 +63,8 @@ class PassportData(TelegramObject): """ - data: tuple[EncryptedPassportElement, ...] = tg_field( - compare=True, converter=parse_sequence_arg - ) - credentials: EncryptedCredentials = tg_field(compare=True) + data: tuple[EncryptedPassportElement, ...] = tg_field(converter=parse_sequence_arg) + credentials: EncryptedCredentials = tg_field() # Attribute only (init=False) _decrypted_data: tuple[EncryptedPassportElement, ...] | None = tg_field( diff --git a/src/telegram/_payment/stars/revenuewithdrawalstate.py b/src/telegram/_payment/stars/revenuewithdrawalstate.py index 15000f1ccb1..36547491eda 100644 --- a/src/telegram/_payment/stars/revenuewithdrawalstate.py +++ b/src/telegram/_payment/stars/revenuewithdrawalstate.py @@ -83,7 +83,7 @@ class RevenueWithdrawalStatePending(RevenueWithdrawalState): """ # Attribute only (init=False) - type: str = tg_field(init=False, default=RevenueWithdrawalState.PENDING) + type: str = tg_field(compare=True, init=False, default=RevenueWithdrawalState.PENDING) @tg_dataclass() @@ -125,4 +125,4 @@ class RevenueWithdrawalStateFailed(RevenueWithdrawalState): """ # Attribute only (init=False) - type: str = tg_field(init=False, default=RevenueWithdrawalState.FAILED) + type: str = tg_field(compare=True, init=False, default=RevenueWithdrawalState.FAILED) diff --git a/src/telegram/_payment/stars/transactionpartner.py b/src/telegram/_payment/stars/transactionpartner.py index f3e7b2b9ff4..b274e781c4c 100644 --- a/src/telegram/_payment/stars/transactionpartner.py +++ b/src/telegram/_payment/stars/transactionpartner.py @@ -191,7 +191,7 @@ class TransactionPartnerFragment(TransactionPartner): """ # Attribute only (init=False) - type: str = tg_field(init=False, default=TransactionPartner.FRAGMENT) + type: str = tg_field(compare=True, init=False, default=TransactionPartner.FRAGMENT) # Optional withdrawal_state: "RevenueWithdrawalState | None" = tg_field(default=None) @@ -343,7 +343,7 @@ class TransactionPartnerOther(TransactionPartner): """ # Attribute only (init=False) - type: str = tg_field(init=False, default=TransactionPartner.OTHER) + type: str = tg_field(compare=True, init=False, default=TransactionPartner.OTHER) @tg_dataclass() @@ -358,7 +358,7 @@ class TransactionPartnerTelegramAds(TransactionPartner): """ # Attribute only (init=False) - type: str = tg_field(init=False, default=TransactionPartner.TELEGRAM_ADS) + type: str = tg_field(compare=True, init=False, default=TransactionPartner.TELEGRAM_ADS) @tg_dataclass() diff --git a/src/telegram/_poll.py b/src/telegram/_poll.py index e03a0c6cd0c..b85b244b60c 100644 --- a/src/telegram/_poll.py +++ b/src/telegram/_poll.py @@ -19,7 +19,7 @@ """This module contains an object that represents a Telegram Poll.""" import datetime as dtm -from typing import TYPE_CHECKING, ClassVar, Final +from typing import TYPE_CHECKING, ClassVar from telegram import constants from telegram._chat import Chat @@ -289,12 +289,12 @@ def parse_entities(self, types: list[str] | None = None) -> dict[MessageEntity, """ return parse_message_entities(self.text, self.text_entities, types) - MIN_LENGTH: Final[int] = constants.PollLimit.MIN_OPTION_LENGTH + MIN_LENGTH: ClassVar[int] = constants.PollLimit.MIN_OPTION_LENGTH """:const:`telegram.constants.PollLimit.MIN_OPTION_LENGTH` .. versionadded:: 20.0 """ - MAX_LENGTH: Final[int] = constants.PollLimit.MAX_OPTION_LENGTH + MAX_LENGTH: ClassVar[int] = constants.PollLimit.MAX_OPTION_LENGTH """:const:`telegram.constants.PollLimit.MAX_OPTION_LENGTH` .. versionadded:: 20.0 From 01bb8c55292c751c6cbd5c7be5f7a665a23aaa65 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:39:54 +0300 Subject: [PATCH 29/31] Document all breaking changes in changelog --- .../5334.Yat2ZJVdKq3PJCX7kWFLfD.toml | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml b/changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml index e2954017da6..4446f10c3fd 100644 --- a/changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml +++ b/changes/unreleased/5334.Yat2ZJVdKq3PJCX7kWFLfD.toml @@ -1,5 +1,32 @@ -breaking = "Dataclasses" -internal = "Dataclasses" +breaking = """Implement Telegram types as dataclasses + +PTB has long `stability policy `__. With this release we migrated Telegram types to dataclasses. That, however, had to come with the following breaking changes: + +- User defined classes that subclass from :class:`~telegram.TelegramObject` will no longer work unchanged. +- Direct instances of :class:`~telegram.TelegramObject` are frozen by default. +- Loading a pickled :class:`~telegram.TelegramObject` will return a frozen instance by default. +- Comparing a parent :class:`telegram.TelegramObject` with a child whose comparison attributes equal the parent's, or where both have no comparison attributes, will now evaluate to :obj:`False`. + For example: + + .. code:: python + + parent = BotCommandScope(type=BotCommandScope.DEFAULT) + child = BotCommandScopeDefault() + + parent == child # False in NEXT.VERSION and later + parent == child # True in v22.8 + + This affects the following parent classes and some or all of their subclasses: + * :class:`~telegram.BackgroundFill`, :class:`~telegram.BackgroundType`, :class:`~telegram.BotCommandScope`, :class:`~telegram.ChatBoostSource`, :class:`~telegram.ChatMember`, :class:`~telegram.InlineQueryResult`, :class:`~telegram.InputMedia`, :class:`~telegram.InputPaidMedia`, :class:`~telegram.InputProfilePhoto`, :class:`~telegram.InputStoryContent`, :class:`~telegram.MaybeInaccessibleMessage`, :class:`~telegram.MenuButton`, :class:`~telegram.MessageOrigin`, :class:`~telegram.ReactionType`, :class:`~telegram.RevenueWithdrawalState`, and :class:`~telegram.TransactionPartner`. + +""" +other = """Implement Telegram types as dataclasses + +- Comparisons between Telegram objects that have an empty set of attributes that define equality, will no longer emit :class:`telegram.warnings.PTBUserWarning`. +- Removed redundant coercion to :obj:`int` for :attr:`telegram.ChatInviteLink.pending_join_request_count`, :attr:`telegram.Location.proximity_alert_radius`, :attr:`telegram.InlineQueryResultLocation.proximity_alert_radius`, and :attr:`telegram.InputLocationMessageContent.proximity_alert_radius`. Values are now stored as passed. +- Removed redundant coercion to :obj:`str` for ``file_id``, ``file_unique_id`` attributes of Telegram media types, :attr:`telegram.Contact.phone_number`, :attr:`telegram.InlineQueryResult.id`, :attr:`telegram.PassportElementError.source` , and :attr:`telegram.PassportElementError.type`. Values are now stored as passed. + +""" [[pull_requests]] uid = "5334" author_uids = ["aelkheir"] From 2389caeaeeaecb2905687f659728afdf62ed1025 Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:08:47 +0300 Subject: [PATCH 30/31] Fix some `test_poll` failures Same breaking change caused by a bug in old `__eq__()`. tl;dr previously __eq__ allowed parent == child # True --- tests/test_poll.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/test_poll.py b/tests/test_poll.py index 439c5659871..6f63607b6a2 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -28,7 +28,6 @@ InputPollOption, LivePhoto, Location, - MaybeInaccessibleMessage, MessageEntity, PhotoSize, Poll, @@ -42,6 +41,7 @@ Venue, Video, ) +from telegram._message import Message from telegram._utils.datetime import UTC, to_timestamp from telegram.constants import PollType from telegram.warnings import PTBDeprecationWarning @@ -796,7 +796,7 @@ def poll_option_added(): class PollOptionAddedTestBase: - poll_message = MaybeInaccessibleMessage( + poll_message = Message( message_id=1, date=dtm.datetime.now(dtm.timezone.utc), chat=Chat(1, "test_chat"), @@ -870,9 +870,7 @@ def test_equality(self): option_text_entities=self.option_text_entities, ) c = PollOptionAdded( - poll_message=MaybeInaccessibleMessage( - 2, dtm.datetime.now(dtm.timezone.utc), Chat(1, "test_chat") - ), + poll_message=Message(2, dtm.datetime.now(dtm.timezone.utc), Chat(1, "test_chat")), option_persistent_id=self.option_persistent_id, option_text=self.option_text, option_text_entities=self.option_text_entities, @@ -908,7 +906,7 @@ def poll_option_deleted(): class PollOptionDeletedTestBase: - poll_message = MaybeInaccessibleMessage( + poll_message = Message( message_id=1, date=dtm.datetime.now(dtm.timezone.utc), chat=Chat(1, "test_chat"), @@ -986,9 +984,7 @@ def test_equality(self): option_text_entities=self.option_text_entities, ) c = PollOptionDeleted( - poll_message=MaybeInaccessibleMessage( - 2, dtm.datetime.now(dtm.timezone.utc), Chat(1, "test_chat") - ), + poll_message=Message(2, dtm.datetime.now(dtm.timezone.utc), Chat(1, "test_chat")), option_persistent_id=self.option_persistent_id, option_text=self.option_text, option_text_entities=self.option_text_entities, From ed5bc39daea1a2b2aa8178b76560c1062210e5ee Mon Sep 17 00:00:00 2001 From: aelkheir <90580077+aelkheir@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:27:47 +0300 Subject: [PATCH 31/31] Unify TelegramObject comparison tests after removing warnings --- tests/test_telegramobject.py | 37 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/tests/test_telegramobject.py b/tests/test_telegramobject.py index 72a0b68dd26..6a535caf034 100644 --- a/tests/test_telegramobject.py +++ b/tests/test_telegramobject.py @@ -43,7 +43,6 @@ from telegram._utils.defaultvalue import DEFAULT_FALSE, DEFAULT_NONE, DefaultValue from telegram._utils.types import ODVInput from telegram.ext import PicklePersistence -from telegram.warnings import PTBUserWarning from tests.auxil.files import data_file from tests.conftest import unfrozen @@ -449,31 +448,25 @@ class SubClass(TelegramObject): assert "default_none" not in to_dict assert to_dict["default_false"] is False - def test_meaningless_comparison(self, recwarn): - expected_warning = "Objects of type TGO can not be meaningfully tested for equivalence." - - class TGO(TelegramObject): + def test_comparison(self): + @tg_dataclass() + class EmptyTGO(TelegramObject): pass - a = TGO() - b = TGO() - assert a == b - assert len(recwarn) == 1 - assert str(recwarn[0].message) == expected_warning - assert recwarn[0].category is PTBUserWarning - assert recwarn[0].filename == __file__, "wrong stacklevel" + assert EmptyTGO() == EmptyTGO() - def test_meaningful_comparison(self, recwarn): - class TGO(TelegramObject): - def __init__(self): - self._id_attrs = (1,) + @tg_dataclass() + class ExcludedFieldTGO(TelegramObject): + attribute: str = tg_field(compare=False) - a = TGO() - b = TGO() - assert a == b - assert len(recwarn) == 0 - assert b == a - assert len(recwarn) == 0 + assert ExcludedFieldTGO("attr") == ExcludedFieldTGO("not attr") + + @tg_dataclass() + class ComparedFieldTGO(TelegramObject): + attribute: str = tg_field(compare=True) + + assert ComparedFieldTGO("foo") == ComparedFieldTGO("foo") + assert ComparedFieldTGO("foo") != ComparedFieldTGO("not foo") def test_hash_without_comparison_fields_uses_identity(self, recwarn): @tg_dataclass()