8000 Add String Representation for Selected Classes (#3826) · guillemap/python-telegram-bot@9c7298c · GitHub
[go: up one dir, main page]

Skip to content

Commit 9c7298c

Browse files
authored
Add String Representation for Selected Classes (python-telegram-bot#3826)
1 parent 39abf83 commit 9c7298c

14 files changed

+240
-2
lines changed

telegram/_bot.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
9393
from telegram._utils.files import is_local_file, parse_file_input
9494
from telegram._utils.logging import get_logger
95+
from telegram._utils.repr import build_repr_with_selected_attrs
9596
from telegram._utils.types import (
9697
CorrectOptionID,
9798
DVInput,
@@ -308,6 +309,17 @@ def __init__(
308309

309310
self._freeze()
310311

312+
def __repr__(self) -> str:
313+
"""Give a string representation of the bot in the form ``Bot[token=...]``.
314+
315+
As this class doesn't implement :meth:`object.__str__`, the default implementation
316+
will be used, which is equivalent to :meth:`__repr__`.
317+
318+
Returns:
319+
:obj:`str`
320+
"""
321+
return build_repr_with_selected_attrs(self, token=self.token)
322+
311323
@property
312324
def token(self) -> str:
313325
""":obj:`str`: Bot's unique authentication token.

telegram/_utils/repr.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env python
2+
#
3+
# A library that provides a Python interface to the Telegram Bot API
4+
# Copyright (C) 2015-2023
5+
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
6+
#
7+
# This program is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Lesser Public License as published by
9+
# the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Lesser Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser Public License
18+
# along with this program. If not, see [http://www.gnu.org/licenses/].
19+
"""This module contains auxiliary functionality for building strings for __repr__ method.
20+
21+
Warning:
22+
Contents of this module are intended to be used internally by the library and *not* by the
23+
user. Changes to this module are not considered breaking changes and may not be documented in
24+
the changelog.
25+
"""
26+
from typing import Any
27+
28+
29+
def build_repr_with_selected_attrs(obj: object, **kwargs: Any) -> str:
30+
"""Create ``__repr__`` string in the style ``Classname[arg1=1, arg2=2]``.
31+
32+
The square brackets emphasize the fact that an object cannot be instantiated
33+
from this string.
34+
35+
Attributes that are to be used in the representation, are passed as kwargs.
36+
"""
37+
return (
38+
f"{obj.__class__.__name__}"
39+
# square brackets emphasize that an object cannot be instantiated with these params
40+
f"[{', '.join(_stringify(name, value) for name, value in kwargs.items())}]"
41+
)
42+
43+
44+
def _stringify(key: str, val: Any) -> str:
45+
return f"{key}={val.__qualname__ if callable(val) else val}"

telegram/ext/_application.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from telegram._update import Update
5555
from telegram._utils.defaultvalue import DEFAULT_NONE, DEFAULT_TRUE, DefaultValue
5656
from telegram._utils.logging import get_logger
57+
from telegram._utils.repr import build_repr_with_selected_attrs
5758
from telegram._utils.types import SCT, DVType, ODVInput
5859
from telegram._utils.warnings import warn
5960
from telegram.error import TelegramError
@@ -80,7 +81,6 @@
8081
_STOP_SIGNAL = object()
8182
_DEFAULT_0 = DefaultValue(0)
8283

83-
8484
# Since python 3.12, the coroutine passed to create_task should not be an (async) generator. Remove
8585
# this check when we drop support for python 3.11.
8686
if sys.version_info >= (3, 12):
@@ -90,7 +90,6 @@
9090

9191
_ErrorCoroType = Optional[_CoroType[RT]]
9292

93-
9493
_LOGGER = get_logger(__name__)
9594

9695

@@ -345,6 +344,17 @@ def __init__(
345344
self.__update_persistence_lock = asyncio.Lock()
346345
self.__create_task_tasks: Set[asyncio.Task] = set() # Used for awaiting tasks upon exit
347346

347+
def __repr__(self) -> str:
348+
"""Give a string representation of the application in the form ``Application[bot=...]``.
349+
350+
As this class doesn't implement :meth:`object.__str__`, the default implementation
351+
will be used, which is equivalent to :meth:`__repr__`.
352+
353+
Returns:
354+
:obj:`str`
355+
"""
356+
return build_repr_with_selected_attrs(self, bot=self.bot)
357+
348358
def _check_initialized(self) -> None:
349359
if not self._initialized:
350360
raise RuntimeError(

telegram/ext/_basehandler.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, Union
2222

2323
from telegram._utils.defaultvalue import DEFAULT_TRUE
24+
from telegram._utils.repr import build_repr_with_selected_attrs
2425
from telegram._utils.types import DVType
2526
from telegram.ext._utils.types import CCT, HandlerCallback
2627

@@ -95,6 +96,17 @@ def __init__(
9596
self.callback: HandlerCallback[UT, CCT, RT] = callback
9697
self.block: DVType[bool] = block
9798

99+
def __repr__(self) -> str:
100+
"""Give a string representation of the handler in the form ``ClassName[callback=...]``.
101+
102+
As this class doesn't implement :meth:`object.__str__`, the default implementation
103+
will be used, which is equivalent to :meth:`__repr__`.
104+
105+
Returns:
106+
:obj:`str`
107+
"""
108+
return build_repr_with_selected_attrs(self, callback=self.callback.__qualname__)
109+
98110
@abstractmethod
99111
def check_update(self, update: object) -> Optional[Union[bool, object]]:
100112
"""

telegram/ext/_conversationhandler.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from telegram import Update
3939
from telegram._utils.defaultvalue import DEFAULT_TRUE, DefaultValue
4040
from telegram._utils.logging import get_logger
41+
from telegram._utils.repr import build_repr_with_selected_attrs
4142
from telegram._utils.types import DVType
4243
from telegram._utils.warnings import warn
4344
from telegram.ext._application import ApplicationHandlerStop
@@ -440,6 +441,30 @@ def __init__(
440441
stacklevel=2,
441442
)
442443

444+
def __repr__(self) -> str:
445+
"""Give a string representation of the ConversationHandler in the form
446+
``ConversationHandler[name=..., states={...}]``.
447+
448+
If there are more than 3 states, only the first 3 states are listed.
449+
450+
As this class doesn't implement :meth:`object.__str__`, the default implementation
451+
will be used, which is equivalent to :meth:`__repr__`.
452+
453+
Returns:
454+
:obj:`str`
455+
"""
456+
truncation_threshold = 3
457+
states = dict(list(self.states.items())[:truncation_threshold])
458+
states_string = str(states)
459+
if len(self.states) > truncation_threshold:
460+
states_string = states_string[:-1] + ", ...}"
461+
462+
return build_repr_with_selected_attrs(
463+
self,
464+
name=self.name,
465+
states=states_string,
466+
)
467+
443468
@property
444469
def entry_points(self) -> List[BaseHandler[Update, CCT]]:
445470
"""List[:class:`telegram.ext.BaseHandler`]: A list of :obj:`BaseHandler` objects that can

telegram/ext/_extbot.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
from telegram._utils.datetime import to_timestamp
8686
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
8787
from telegram._utils.logging import get_logger
88+
from telegram._utils.repr import build_repr_with_selected_attrs
8889
from telegram._utils.types import (
8990
CorrectOptionID,
9091
DVInput,
@@ -246,6 +247,17 @@ def __init__(
246247

247248
self._callback_data_cache = CallbackDataCache(bot=self, maxsize=maxsize)
248249

250+
def __repr__(self) -> str:
251+
"""Give a string representation of the bot in the form ``ExtBot[token=...]``.
252+
253+
As this class doesn't implement :meth:`object.__str__`, the default implementation
254+
will be used, which is equivalent to :meth:`__repr__`.
255+
256+
Returns:
257+
:obj:`str`
258+
"""
259+
return build_repr_with_selected_attrs(self, token=self.token)
260+
249261
@classmethod
250262
def _warn(
251263
cls, message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0

telegram/ext/_jobqueue.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
except ImportError:
3232
APS_AVAILABLE = False
3333

34+
from telegram._utils.repr import build_repr_with_selected_attrs
3435
from telegram._utils.types import JSONDict
3536
from telegram._utils.warnings import warn
3637
from telegram.ext._extbot import ExtBot
@@ -97,6 +98,17 @@ def __init__(self) -> None:
9798
timezone=pytz.utc, executors={"default": self._executor}
9899
)
99100

101+
def __repr__(self) -> str:
102+
"""Give a string representation of the JobQueue in the form ``JobQueue[application=...]``.
103+
104+
As this class doesn't implement :meth:`object.__str__`, the default implementation
105+
will be used, which is equivalent to :meth:`__repr__`.
106+
107+
Returns:
108+
:obj:`str`
109+
"""
110+
return build_repr_with_selected_attrs(self, application=self.application)
111+
100112
def _tz_now(self) -> datetime.datetime:
101113
return datetime.datetime.now(self.scheduler.timezone)
102114

@@ -766,6 +778,24 @@ def __init__(
766778

767779
self._job = cast("APSJob", None) # skipcq: PTC-W0052
768780

781+
def __repr__(self) -> str:
782+
"""Give a string representation of the job in the form
783+
``Job[id=..., name=..., callback=..., trigger=...]``.
784+
785+
As this class doesn't implement :meth:`object.__str__`, the default implementation
786+
will be used, which is equivalent to :meth:`__repr__`.
787+
788+
Returns:
789+
:obj:`str`
790+
"""
791+
return build_repr_with_selected_attrs(
792+
self,
793+
id=self.job.id,
794+
name=self.name,
795+
callback=self.callback.__name__,
796+
trigger=self.job.trigger,
797+
)
798+
769799
@property
770800
def job(self) -> "APSJob":
771801
""":class:`apscheduler.job.Job`: The APS Job this job is a wrapper for.

telegram/ext/_updater.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
from telegram._utils.defaultvalue import DEFAULT_NONE
3939
from telegram._utils.logging import get_logger
40+
from telegram._utils.repr import build_repr_with_selected_attrs
4041
from telegram._utils.types import ODVInput
4142
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
4243

@@ -124,6 +125,17 @@ def __init__(
124125
self.__polling_task: Optional[asyncio.Task] = None
125126
self.__polling_cleanup_cb: Optional[Callable[[], Coroutine[Any, Any, None]]] = None
126127

128+
def __repr__(self) -> str:
129+
"""Give a string representation of the updater in the form ``Updater[bot=...]``.
130+
131+
As this class doesn't implement :meth:`object.__str__`, the default implementation
132+
will be used, which is equivalent to :meth:`__repr__`.
133+
134+
Returns:
135+
:obj:`str`
136+
"""
137+
return build_repr_with_selected_attrs(self, bot=self.bot)
138+
127139
@property
128140
def running(self) -> bool:
129141
return self._running

tests/ext/test_application.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ async def post_stop(application: Application) -> None:
201201
assert isinstance(app.chat_data[1], dict)
202202
assert isinstance(app.user_data[1], dict)
203203

204+
async def test_repr(self, app):
205+
assert repr(app) == f"PytestApplication[bot={app.bot!r}]"
206+
204207
def test_job_queue(self, one_time_bot, app, recwarn):
< 10000 /code>
205208
expected_warning = (
206209
"No `JobQueue` set up. To use `JobQueue`, you must install PTB via "

tests/ext/test_basehandler.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,19 @@ def check_update(self, update: object):
3636
for attr in inst.__slots__:
3737
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
3838
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
39+
40+
def test_repr(self):
41+
async def some_func():
42+
return None
43+
44+
class SubclassHandler(BaseHandler):
45+
__slots__ = ()
46+
47+
def __init__(self):
48+
super().__init__(callback=some_func)
49+
50+
def check_update(self, update: object):
51+
pass
52+
53+
sh = SubclassHandler()
54+
assert repr(sh) == "SubclassHandler[callback=TestHandler.test_repr.<locals>.some_func]"

tests/ext/test_conversationhandler.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# along with this program. If not, see [http://www.gnu.org/licenses/].
1919
"""Persistence of conversations is tested in test_basepersistence.py"""
2020
import asyncio
21+
import functools
2122
import logging
2223
from pathlib import Path
2324
from warnings import filterwarnings
@@ -75,6 +76,7 @@ def user2():
7576

7677

7778
def raise_ahs(func):
79+
@functools.wraps(func) # for checking __repr__
7880
async def decorator(self, *args, **kwargs):
7981
result = await func(self, *args, **kwargs)
8082
if self.raise_app_handler_stop:
@@ -289,6 +291,41 @@ def test_init_persistent_no_name(self):
289291
self.entry_points, states=self.states, fallbacks=[], persistent=True
290292
)
291293

294+
def test_repr_no_truncation(self):
295+
# ConversationHandler's __repr__ is not inherited from BaseHandler.
296+
ch = ConversationHandler(
297+
name="test_handler",
298+
entry_points=[],
299+
states=self.drinking_states,
300+
fallbacks=[],
301+
)
302+
assert repr(ch) == (
303+
"ConversationHandler[name=test_handler, "
304+
"states={'a': [CommandHandler[callback=TestConversationHandler.sip]], "
305+
"'b': [CommandHandler[callback=TestConversationHandler.swallow]], "
306+
"'c': [CommandHandler[callback=TestConversationHandler.hold]]}]"
307+
)
308+
309+
def test_repr_with_truncation(self):
310+
from copy import copy
311+
312+
states = copy(self.drinking_states)
313+
# there are exactly 3 drinking states. adding one more to make sure it's truncated
314+
states["extra_to_be_truncated"] = [CommandHandler("foo", self.start)]
315+
316+
ch = ConversationHandler(
317+
name="test_handler",
318+
entry_points=[],
319+
states=states,
320+
fallbacks=[],
321+
)
322+
assert repr(ch) == (
323+
"ConversationHandler[name=test_handler, "
324+
"states={'a': [CommandHandler[callback=TestConversationHandler.sip]], "
325+
"'b': [CommandHandler[callback=TestConversationHandler.swallow]], "
326+
"'c': [CommandHandler[callback=TestConversationHandler.hold]], ...}]"
327+
)
328+
292329
async def test_check_update_returns_non(self, app, user1):
293330
"""checks some cases where updates should not be handled"""
294331
conv_handler = ConversationHandler([], {}, [], per_message=True, per_chat=True)

tests/ext/test_jobqueue.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ class TestJobQueue:
8585
" We recommend double checking if the passed value is correct."
8686
)
8787

88+
async def test_repr(self, app):
89+
jq = JobQueue()
90+
jq.set_application(app)
91+
assert repr(jq) == f"JobQueue[application={app!r}]"
92+
93+
when = dtm.datetime.utcnow() + dtm.timedelta(days=1)
94+
callback = self.job_run_once
95+
job = jq.run_once(callback, when, name="name2")
96+
assert repr(job) == (
97+
f"Job[id={job.job.id}, name={job.name}, callback=job_run_once, "
98+
f"trigger=date["
99+
f"{when.strftime('%Y-%m-%d %H:%M:%S UTC')}"
100+
f"]]"
101+
)
102+
88103
@pytest.fixture(autouse=True)
89104
def _reset(self):
90105
self.result = 0

0 commit comments

Comments
 (0)
0