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 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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"]