Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
37dccdc
add `__repr__` to `Application`
lemontree210 Aug 4, 2023
ca0a991
add `__repr__` to `CallbackContext`
lemontree210 Aug 4, 2023
2263820
add `__repr__` to `Bot` and `ExtBot`
lemontree210 Aug 4, 2023
6e6b9b6
add `__repr__` to `Job`
lemontree210 Aug 4, 2023
30f075d
add `__repr__` for `JobQueue`
lemontree210 Aug 4, 2023
2b60955
Merge branch 'master' into repr-for-classes-3770
lemontree210 Aug 6, 2023
3051620
move `__repr__` in `Job` up in code with no change
lemontree210 Aug 6, 2023
0da0efc
add `.job.trigger` to `Job.__repr__()`
lemontree210 Aug 7, 2023
edd551c
add __repr__ to `Updater`
lemontree210 Aug 7, 2023
1f5bbd6
add __repr__ to `BaseHandler`
lemontree210 Aug 7, 2023
43d93e3
add __repr__ to `ConversationHandler`
lemontree210 Aug 7, 2023
867762d
use `__qualname__` for callback in `BaseHandler`
lemontree210 Aug 7, 2023
9c2a9c3
add auxiliary function and try building `Application.__repr__` with it
lemontree210 Aug 14, 2023
ef396ac
replace parentheses with square brackets
lemontree210 Aug 21, 2023
8c83c69
replace `__repr__` in `CCT`, `Bot`, `ExtBot`, `Job`, `JobQueue`
lemontree210 Aug 21, 2023
600022d
replace `__repr__` in `BaseHandler`
lemontree210 Aug 21, 2023
4d16fba
replace `__repr__` in `Updater`, remove queue size
lemontree210 Aug 21, 2023
ef50a0f
replace `__repr__` in `ConversationHandler`, show truncated list of s…
lemontree210 Aug 21, 2023
275009b
Merge remote-tracking branch 'origin/master' into repr-for-classes-3770
lemontree210 Aug 24, 2023
cddaba8
add tests for `Application`, `Bot`, `CallbackContext`
lemontree210 Aug 25, 2023
8b2a573
add test for `Job` and `JobQueue`
lemontree210 Aug 25, 2023
76c2ad7
add test for __repr__ in `TypeHandler`
lemontree210 Aug 26, 2023
def0508
add ellipsis to ConversationHandler.__repr__, add test
lemontree210 Aug 26, 2023
886ebf1
add test for `Updater`
lemontree210 Aug 26, 2023
0aa874b
move test for `__repr__` from test_typehandler.py to test_basehandler.py
lemontree210 Aug 28, 2023
6f75015
set explicit `when=` for test of `JobQueue.__repr__`
lemontree210 Aug 28, 2023
61714ea
remove `__repr__` from `CallbackContext`
lemontree210 Sep 5, 2023
38c2daa
Merge remote-tracking branch 'origin/master' into repr-for-classes-3770
lemontree210 Sep 5, 2023
101bb1b
refactor: move `_stringify()` outside of func
lemontree210 Sep 8, 2023
0a93e44
amend comment
lemontree210 Sep 8, 2023
40c7940
fix deepsource issue
lemontree210 Sep 8, 2023
07023df
add test for `ConversationHandler`
lemontree210 Sep 8, 2023
69e8652
use `app` fixture in test instead of instantiating new app
lemontree210 Sep 10, 2023
07bb191
remove unnecessary import
lemontree210 Sep 10, 2023
40e1c0f
introduce dict-like repr of states in ConversationHandler
lemontree210 Sep 10, 2023
ca2b6a3
Merge branch 'master' into repr-for-classes-3770
lemontree210 Sep 10, 2023
68936d6
Merge branch 'master' into repr-for-classes-3770
lemontree210 Sep 11, 2023
161f2b5
fix docstring of `build_repr...`
lemontree210 Sep 11, 2023
91ff99a
add docstring for `Application.__repr__()`
lemontree210 Sep 11, 2023
fb37210
add docstring for `Bot.__repr__()` and `ExtBot.__repr__()`
lemontree210 Sep 11, 2023
9f3ebee
add docstring for `Job.__repr__()` and `JobQueue.__repr__()`
lemontree210 Sep 11, 2023
add9159
add docstring for `Updater.__repr__()`
lemontree210 Sep 11, 2023
b450579
add docstring for `BaseHandler.__repr__()`
lemontree210 Sep 11, 2023
1b311d5
add docstring for `ConversationHandler.__repr__()`
lemontree210 Sep 11, 2023
2be38b1
remove warning since fixture is used in test
lemontree210 Sep 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions telegram/_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
from telegram._utils.files import is_local_file, parse_file_input
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import (
CorrectOptionID,
DVInput,
Expand Down Expand Up @@ -308,6 +309,17 @@ def __init__(

self._freeze()

def __repr__(self) -> str:
"""Give a string representation of the bot in the form ``Bot[token=...]``.

As this class doesn't implement :meth:`object.__str__`, the default implementation
will be used, which is equivalent to :meth:`__repr__`.

Returns:
:obj:`str`
"""
return build_repr_with_selected_attrs(self, token=self.token)

@property
def token(self) -> str:
""":obj:`str`: Bot's unique authentication token.
Expand Down
45 changes: 45 additions & 0 deletions telegram/_utils/repr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2023
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains auxiliary functionality for building strings for __repr__ method.

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 typing import Any


def build_repr_with_selected_attrs(obj: object, **kwargs: Any) -> str:
"""Create ``__repr__`` string in the style ``Classname[arg1=1, arg2=2]``.

The square brackets emphasize the fact that an object cannot be instantiated
from this string.

Attributes that are to be used in the representation, are passed as kwargs.
"""
return (
f"{obj.__class__.__name__}"
# square brackets emphasize that an object cannot be instantiated with these params
f"[{', '.join(_stringify(name, value) for name, value in kwargs.items())}]"
)


def _stringify(key: str, val: Any) -> str:
return f"{key}={val.__qualname__ if callable(val) else val}"
14 changes: 12 additions & 2 deletions telegram/ext/_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from telegram._update import Update
from telegram._utils.defaultvalue import DEFAULT_NONE, DEFAULT_TRUE, DefaultValue
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import SCT, DVType, ODVInput
from telegram._utils.warnings import warn
from telegram.error import TelegramError
Expand All @@ -80,7 +81,6 @@
_STOP_SIGNAL = object()
_DEFAULT_0 = DefaultValue(0)


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

_ErrorCoroType = Optional[_CoroType[RT]]


_LOGGER = get_logger(__name__)


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

def __repr__(self) -> str:
"""Give a string representation of the application in the form ``Application[bot=...]``.

As this class doesn't implement :meth:`object.__str__`, the default implementation
will be used, which is equivalent to :meth:`__repr__`.

Returns:
:obj:`str`
"""
return build_repr_with_selected_attrs(self, bot=self.bot)

def _check_initialized(self) -> None:
if not self._initialized:
raise RuntimeError(
Expand Down
12 changes: 12 additions & 0 deletions telegram/ext/_basehandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, Union

from telegram._utils.defaultvalue import DEFAULT_TRUE
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import DVType
from telegram.ext._utils.types import CCT, HandlerCallback

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

def __repr__(self) -> str:
"""Give a string representation of the handler in the form ``ClassName[callback=...]``.

As this class doesn't implement :meth:`object.__str__`, the default implementation
will be used, which is equivalent to :meth:`__repr__`.

Returns:
:obj:`str`
"""
return build_repr_with_selected_attrs(self, callback=self.callback.__qualname__)

@abstractmethod
def check_update(self, update: object) -> Optional[Union[bool, object]]:
"""
Expand Down
25 changes: 25 additions & 0 deletions telegram/ext/_conversationhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from telegram import Update
from telegram._utils.defaultvalue import DEFAULT_TRUE, DefaultValue
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import DVType
from telegram._utils.warnings import warn
from telegram.ext._application import ApplicationHandlerStop
Expand Down Expand Up @@ -440,6 +441,30 @@ def __init__(
stacklevel=2,
)

def __repr__(self) -> str:
"""Give a string representation of the ConversationHandler in the form
``ConversationHandler[name=..., states={...}]``.

If there are more than 3 states, only the first 3 states are listed.

As this class doesn't implement :meth:`object.__str__`, the default implementation
will be used, which is equivalent to :meth:`__repr__`.

Returns:
:obj:`str`
"""
truncation_threshold = 3
states = dict(list(self.states.items())[:truncation_threshold])
states_string = str(states)
if len(self.states) > truncation_threshold:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, codecov complains that the (implicit) else clause here is not covered …

Copy link
Member Author

@lemontree210 lemontree210 Sep 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

states_string = states_string[:-1] + ", ...}"

return build_repr_with_selected_attrs(
self,
name=self.name,
states=states_string,
)

@property
def entry_points(self) -> List[BaseHandler[Update, CCT]]:
"""List[:class:`telegram.ext.BaseHandler`]: A list of :obj:`BaseHandler` objects that can
Expand Down
12 changes: 12 additions & 0 deletions telegram/ext/_extbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
from telegram._utils.datetime import to_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import (
CorrectOptionID,
DVInput,
Expand Down Expand Up @@ -246,6 +247,17 @@ def __init__(

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

def __repr__(self) -> str:
"""Give a string representation of the bot in the form ``ExtBot[token=...]``.

As this class doesn't implement :meth:`object.__str__`, the default implementation
will be used, which is equivalent to :meth:`__repr__`.

Returns:
:obj:`str`
"""
return build_repr_with_selected_attrs(self, token=self.token)

@classmethod
def _warn(
cls, message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0
Expand Down
30 changes: 30 additions & 0 deletions telegram/ext/_jobqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
except ImportError:
APS_AVAILABLE = False

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

def __repr__(self) -> str:
"""Give a string representation of the JobQueue in the form ``JobQueue[application=...]``.

As this class doesn't implement :meth:`object.__str__`, the default implementation
will be used, which is equivalent to :meth:`__repr__`.

Returns:
:obj:`str`
"""
return build_repr_with_selected_attrs(self, application=self.application)

def _tz_now(self) -> datetime.datetime:
return datetime.datetime.now(self.scheduler.timezone)

Expand Down Expand Up @@ -766,6 +778,24 @@ def __init__(

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

def __repr__(self) -> str:
"""Give a string representation of the job in the form
``Job[id=..., name=..., callback=..., trigger=...]``.

As this class doesn't implement :meth:`object.__str__`, the default implementation
will be used, which is equivalent to :meth:`__repr__`.

Returns:
:obj:`str`
"""
return build_repr_with_selected_attrs(
self,
id=self.job.id,
name=self.name,
callback=self.callback.__name__,
trigger=self.job.trigger,
)

@property
def job(self) -> "APSJob":
""":class:`apscheduler.job.Job`: The APS Job this job is a wrapper for.
Expand Down
12 changes: 12 additions & 0 deletions telegram/ext/_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.logging import get_logger
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import ODVInput
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut

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

def __repr__(self) -> str:
"""Give a string representation of the updater in the form ``Updater[bot=...]``.

As this class doesn't implement :meth:`object.__str__`, the default implementation
will be used, which is equivalent to :meth:`__repr__`.

Returns:
:obj:`str`
"""
return build_repr_with_selected_attrs(self, bot=self.bot)

@property
def running(self) -> bool:
return self._running
Expand Down
3 changes: 3 additions & 0 deletions tests/ext/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ async def post_stop(application: Application) -> None:
assert isinstance(app.chat_data[1], dict)
assert isinstance(app.user_data[1], dict)

async def test_repr(self, app):
assert repr(app) == f"PytestApplication[bot={app.bot!r}]"

def test_job_queue(self, one_time_bot, app, recwarn):
expected_warning = (
"No `JobQueue` set up. To use `JobQueue`, you must install PTB via "
Expand Down
16 changes: 16 additions & 0 deletions tests/ext/test_basehandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,19 @@ def check_update(self, update: object):
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"

def test_repr(self):
async def some_func():
return None

class SubclassHandler(BaseHandler):
__slots__ = ()

def __init__(self):
super().__init__(callback=some_func)

def check_update(self, update: object):
pass

sh = SubclassHandler()
assert repr(sh) == "SubclassHandler[callback=TestHandler.test_repr.<locals>.some_func]"
37 changes: 37 additions & 0 deletions tests/ext/test_conversationhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Persistence of conversations is tested in test_basepersistence.py"""
import asyncio
import functools
import logging
from pathlib import Path
from warnings import filterwarnings
Expand Down Expand Up @@ -75,6 +76,7 @@ def user2():


def raise_ahs(func):
@functools.wraps(func) # for checking __repr__
async def decorator(self, *args, **kwargs):
result = await func(self, *args, **kwargs)
if self.raise_app_handler_stop:
Expand Down Expand Up @@ -289,6 +291,41 @@ def test_init_persistent_no_name(self):
self.entry_points, states=self.states, fallbacks=[], persistent=True
)

def test_repr_no_truncation(self):
# ConversationHandler's __repr__ is not inherited from BaseHandler.
ch = ConversationHandler(
name="test_handler",
entry_points=[],
states=self.drinking_states,
fallbacks=[],
)
assert repr(ch) == (
"ConversationHandler[name=test_handler, "
"states={'a': [CommandHandler[callback=TestConversationHandler.sip]], "
"'b': [CommandHandler[callback=TestConversationHandler.swallow]], "
"'c': [CommandHandler[callback=TestConversationHandler.hold]]}]"
)

def test_repr_with_truncation(self):
from copy import copy

states = copy(self.drinking_states)
# there are exactly 3 drinking states. adding one more to make sure it's truncated
states["extra_to_be_truncated"] = [CommandHandler("foo", self.start)]

ch = ConversationHandler(
name="test_handler",
entry_points=[],
states=states,
fallbacks=[],
)
assert repr(ch) == (
"ConversationHandler[name=test_handler, "
"states={'a': [CommandHandler[callback=TestConversationHandler.sip]], "
"'b': [CommandHandler[callback=TestConversationHandler.swallow]], "
"'c': [CommandHandler[callback=TestConversationHandler.hold]], ...}]"
)

async def test_check_update_returns_non(self, app, user1):
"""checks some cases where updates should not be handled"""
conv_handler = ConversationHandler([], {}, [], per_message=True, per_chat=True)
Expand Down
15 changes: 15 additions & 0 deletions tests/ext/test_jobqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ class TestJobQueue:
" We recommend double checking if the passed value is correct."
)

async def test_repr(self, app):
jq = JobQueue()
jq.set_application(app)
assert repr(jq) == f"JobQueue[application={app!r}]"

when = dtm.datetime.utcnow() + dtm.timedelta(days=1)
callback = self.job_run_once
job = jq.run_once(callback, when, name="name2")
assert repr(job) == (
f"Job[id={job.job.id}, name={job.name}, callback=job_run_once, "
f"trigger=date["
f"{when.strftime('%Y-%m-%d %H:%M:%S UTC')}"
f"]]"
)

@pytest.fixture(autouse=True)
def _reset(self):
self.result = 0
Expand Down
Loading