GeezerVison is showing github.com — Add DeserializationTestBase and de_json meta-tests by 2ale2 · Pull Request #5401 · python-telegram-bot/python-telegram-bot
Conversation
Part of #4635 (first step, see "Scope" below)
What
Adds DeserializationTestBase (tests/auxil/deserialization.py), an abstract base class for the test classes of TelegramObject subclasses, plus meta-tests (tests/test_deserialization.py), as discussed in the issue (option 2 + meta-test).
A test class sets de_json_class, provides a de_json_dict fixture with all arguments set, and implements the abstract test_de_json (class-specific value checks). In return it gets these generic tests:
test_de_json_dict_is_complete: de_json_dict covers every __init__ argument with non-None values
test_de_json_missing_optional_args: only required args passed, optional attributes end up empty
test_de_json_api_kwargs: unknown fields land in api_kwargs and survive to_dict() (backward compatibility)
test_de_json_missing_required_args: each required arg dropped in turn, deserialization still works via __DE_JSON_COMPAT__ (forward compatibility, point 3 of the issue)
test_de_json_localization: datetime fields (taken from __DE_JSON_PLAN__) are localized according to Defaults.tzinfo
The meta-tests check that
- every
TelegramObject in telegram.__all__ (except Bot) has a test class based on the base class, except those in PENDING_MIGRATION (not migrated yet) and NOT_RECEIVED_FROM_API (see below),
- no
Test* class is abstract. pytest silently skips abstract classes, so a class that forgets test_de_json would otherwise run none of its tests,
PENDING_MIGRATION doesn't contain already-migrated classes.
Scope
To keep the diff reviewable, this PR migrates three classes as examples (Dice, ChatInviteLink, ChatMemberBanned). Their former test_de_json_required_args/test_de_json_all_args/test_de_json_localization tests are replaced by de_json_dict + test_de_json and the generic tests. The remaining classes are listed in PENDING_MIGRATION, which should only shrink in follow-up PRs. Happy to change this if you prefer a single big PR.
Design decisions
Happy to change any of these if you see it differently:
- Input-only classes are not required to use the base class. Classes that the Bot API only accepts and never sends (
Input*, InlineQueryResult*, BotCommandScope*, PassportElementError*, reply keyboards, LabeledPrice, …; checked against the type hints of all received classes and the return types of all Bot methods) are listed in NOT_RECEIVED_FROM_API. They are never deserialized from Telegram data, so the backward/forward compatibility checks don't apply to them. This matches the current test suite, where 66 of them have no de_json test at all. Their existing tests are left untouched, and they may still opt in to the base class.
- No skip for classes without datetime fields.
test_de_json_localization returns early instead of calling pytest.skip, to avoid over a thousand skipped tests in the report (one per class and timezone parametrization) once everything is migrated.
Check-list for PRs
- Created new or adapted existing unit tests
- Documented code changes according to the CSI standard
- Added myself alphabetically to
AUTHORS.rst (optional)
- Not applicable (test-only change):
versionadded/versionchanged directives, new classes in docs/__all__, Stability Policy
This is my first pull request ever (that's why im creating it as a draft), so any feedback on the code or on the contribution process is very welcome! 🙂
I wrote this with the help of Claude Code and reviewed and tested every single change myself, multiple times.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Thanks for the PR and effort! Reading the implementation reminded me of the P=NP problem lol.
#5334 could change de_json logic, however your centralized approach should make it easy to change stuff if needed. That PR can also make the automation aspect feasible, so we can reduce the repetition without increasing complexity or adding convoluted logic.
The remaining classes are listed in PENDING_MIGRATION, which should only shrink in follow-up PRs. Happy to change this if you prefer a single big PR.
I would like one big PR, PENDING_MIGRATION was a little confusing to read at first. But definitely like your approach of having a reviewable diff though, very appreciated.
Classes that the Bot API only accepts and never sends (Input*, InlineQueryResult*, BotCommandScope*, PassportElementError*, reply keyboards, LabeledPrice, …; checked against the type hints of all received classes and the return types of all Bot methods) are listed in NOT_RECEIVED_FROM_API. They are never deserialized from Telegram data, so the backward/forward compatibility checks don't apply to them. This matches the current test suite, where 66 of them have no de_json test at all. Their existing tests are left untouched, and they may still opt in to the base class.
Idea: Instead of having and maintaining this list, it could be fetched or generated from test_official.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Comment on lines
+65
to
+66
|
|
class TestChatInviteLinkWithoutRequest(ChatInviteLinkTestBase, DeserializationTestBase):
|
|
|
de_json_class = ChatInviteLink
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be possible to automatically add the TestBase class as an abstract class? I did something similar with adding the pytest.mark for request v/s no request classes. Can something along those lines be possible?
If it involves the use of metaclasses its probably not worth it.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into it. The no_req marker can be added in pytest_collection_modifyitems because markers are just metadata that can be attached after collection, but base classes are fixed when the class is created. To be honest I don't have ideas that may be worth to mention... maybe a pytest_pycollect_makeitem hook in conftest.py? It could replaces each test class with a subclass that also inherits from DeserializationTestBase, but I think it would have more side effects then useful outcomes...
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
2ale2
commented
Nov 11, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Here I am! Thanks for the review Harshil. I am honestly happy it could be something useful and not a complete mess. :)
One big PR: sure! I'll try to migrate all the remaining classes in this PR and drop PENDING_MIGRATION. I could maybe try to make a commit per group of related classes (chat members, payments, passport, …), so it would be easier to review.
Generating NOT_RECEIVED_FROM_API: that's definitely a good idea. I didn't look at that. My fault. I tried to find a way to dynamically compute the types that are never received according to the Bot API docs and I missed 9 input-only classes (LocationAddress, StoryArea, StoryAreaPosition and the six StoryAreaType*). Just that tells me that's a good idea.
If I understand correctly, test_official needs internet access and only runs in its own workflow, while the meta-test is part of the normal offline test suite. If I am correct, I see two ways to do it:
- Compute the set in the meta-test from PTB's own type hints. Start from
Update and from the return types of the Bot methods, and follow the type hints of each class. There's no list to maintain, it works offline, and test_official already checks that the type hints match the docs. I tried this and it gives the same result as the docs. The only special case is the Passport credentials: the bot decrypts them itself and then builds a Credentials object (in EncryptedCredentials.decrypted_data), so no type hint leads there. Adding Credentials as a second starting point next to Update is enough to cover it and the classes inside it.
- Keep the list and add a check to
test_official that compares it with the docs, so it can't silently get out of date. For this, the scraper would also need to read the return types of the methods, which are only in the description text.
I'd lean towards 1, but I'm open to other options I maybe not be noticing right now
#5334: I had a look at the current state of that branch. The __init__ signatures (with the aliases) and __DE_JSON_PLAN__/__DE_JSON_COMPAT__ are still there, and that's all the base class relies on. If _de_json becomes dataclass-first, I'd expect only a couple of helpers in tests/auxil/deserialization.py to change. The bigger practical point is that both PRs touch almost every test file (#5334 removes the test_slot_behaviour tests), so there will be a lot of merge conflicts, even if mostly trivial i guess. Do you have a preferred order? I can wait for #5334, or rebase if this one lands second. Since #5334 is huge, maybe I could avoid forcing aelkheir to merge those... idk, your opinion will be helpful fot sure.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This branch has not been deployed
No deployments
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|