fix: исправлена работа перечислений#80
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
PR исправляет проблему с десериализацией MessageBody.markup (элементы type="link"/"user_mention" должны попадать в MarkupLink/MarkupUserMention), переводя строковые enum’ы на StrEnum (с backport для Python 3.10), чтобы Literal[TextStyle.*] корректно работал как discriminator в Pydantic v2.
Changes:
- Добавлен backport
StrEnumдля Python < 3.11 (maxapi/enums/_compat.py). - Переведены строковые enum’ы на
StrEnum+auto()+@unique(где возможно). - Добавлены регрессионные тесты на дискриминацию markup-элементов и на совместимость
StrEnum.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
maxapi/enums/_compat.py |
Backport StrEnum для Python 3.10 / прокси на stdlib для 3.11+ |
maxapi/enums/text_style.py |
TextStyle переведён на StrEnum + auto() + @unique для корректной Literal-дискриминации |
maxapi/enums/upload_type.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/update.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/sender_action.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/parse_mode.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/message_link_type.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/intent.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/chat_type.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/chat_status.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/chat_permission.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/button_type.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/attachment.py |
Строковый enum переведён на StrEnum + auto() |
maxapi/enums/http_method.py |
Enum переведён на StrEnum (со строковыми значениями) |
maxapi/enums/api_path.py |
Enum переведён на StrEnum (со строковыми значениями) |
maxapi/enums/add_chat_members_error_code.py |
Enum переведён на StrEnum (со строковыми значениями) |
tests/test_markup_discrimination.py |
Регрессионные тесты на корректную дискриминацию MarkupLink/MarkupUserMention |
tests/test_strenum_compat.py |
Тесты базовой совместимости backport StrEnum (auto(), @unique, str-инстансы) |
docs/utils/vcf.md |
Удаление хвостовой пустой строки |
docs/filters/contact.md |
Удаление хвостовой пустой строки |
docs/filters/channel_post.md |
Удаление хвостовой пустой строки |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Описание
Переход всех строковых
EnumнаStrEnum+auto()+@unique, добавление backport-совместимости для Python 3.10.Проблема
Пользователь @jimbojohnez сообщил в #77, что после обновления до
0.9.18элементы разметки сtype="link"вMessageBody.markupдесериализуются какMarkupElementвместоMarkupLink, из-за чего теряется полеurl:Первопричина
Два связанных бага:
1. Python 3.10 —
ImportErrortext_style.pyимпортировалStrEnumиз стандартной библиотеки, которая появилась только в Python 3.11. На Python 3.10 (минимальная поддерживаемая версия проекта) весь модуль падал с:2. plain
Enum— сломанный union-дискриминаторДо
0.9.18TextStyleнаследовался отEnum(безstr). При таком варианте Pydantic v2 не может использоватьLiteral[TextStyle.LINK]как дискриминатор в union — все элементы деградируют до базовогоMarkupElement:StrEnumрешает это: членыStrEnumявляются строками, поэтому Pydantic корректно сравнивает"link"сLiteral[TextStyle.LINK].Почему
StrEnum, а неLiteral[TextStyle.LINK.value]Клиент предложил точечный фикс #79
Это работает, но мы выбрали системное решение через
StrEnumпо нескольким причинам:Семантическая корректность. Все наши enum-ы представляют строки из JSON API (
"link","message_created","dialog", …).StrEnumявно выражает этот контракт: члены являются строками. С plainEnumэто неявная конвенция, которую легко нарушить.Pydantic-совместимость из коробки.
StrEnumустраняет целый класс проблем сLiteral-дискриминацией. Тот же паттернLiteral[EnumMember]используется ещё в 8 типах вложений (AttachmentType) и 15 типах обновлений (UpdateType) — все они были потенциально сломаны с plainEnum.Удобство сериализации.
StrEnumсериализуется в JSON как обычная строка без.value— не нужны кастомныеjson_encodersилиmodel_serializer.Удобство сравнения.
TextStyle.LINK == "link"—TrueдляStrEnum,Falseдля plainEnum. Код, работающий с API-ответами, становится проще.Почему
auto(), а не явные строкиЕдиный источник истины. Значение enum-члена определяется его именем (
NAME→"name"). Невозможно допустить опечатку видаSTRONG = "strng"или рассинхронизацию имени и значения.@uniqueзащищает от дубликатов. Сauto()каждое имя гарантированно уникально. С явными строками можно случайно скопировать значение и получить алиас вместо нового члена.Конвенция MAX API. Все значения из API — snake_case имя enum-члена в нижнем регистре.
StrEnum.auto()генерирует именноname.lower(), что совпадает 1:1. Для трёх enum-ов, где это не так (ApiPath→"/me",HTTPMethod→"POST",AddChatMembersErrorCode→"add.participant.privacy"), значения оставлены явными.Решение
1. Backport
StrEnumдля Python 3.10Новый модуль
maxapi/enums/_compat.py:На Python 3.10 backport ведёт себя идентично stdlib
StrEnum:auto()генерируетname.lower(), члены являются строками.2. Все строковые Enum →
StrEnum+auto()+@uniqueДля enum-ов, где
name.lower()совпадает с API-значением:Для enum-ов с нестандартными значениями (
ApiPath,HTTPMethod,AddChatMembersErrorCode) — переведены наStrEnumс явными строками, гдеauto()дал бы неверное значение (/me,POST,add.participant.privacy).Изменённые файлы
maxapi/enums/_compat.pyStrEnumдля Python 3.10maxapi/enums/text_style.pyStrEnum(stdlib) → compat + все значенияauto()+@uniquemaxapi/enums/attachment.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/button_type.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/chat_permission.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/chat_status.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/chat_type.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/intent.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/message_link_type.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/parse_mode.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/sender_action.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/update.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/upload_type.py(str, Enum)→StrEnum+auto()+@uniquemaxapi/enums/api_path.py(str, Enum)→StrEnum+@unique, явные значения (/me, …)maxapi/enums/http_method.py(str, Enum)→StrEnum+@unique, явные значения (POST, …)maxapi/enums/add_chat_members_error_code.py(str, Enum)→StrEnum+@unique, явные значения (точки в строках)tests/test_markup_discrimination.pytests/test_strenum_compat.py_compat.pyТесты
tests/test_markup_discrimination.py— регрессия для сценария из репорта:test_link_markup_deserialized_as_markup_link—type="link"→MarkupLinktest_user_mention_markup_deserialized—type="user_mention"→MarkupUserMentiontest_strong_markup_remains_markup_element—type="strong"→MarkupElementtest_markup_link_url_preserved_from_api—urlиз API сохраняется вMarkupLink.urltest_markup_link_without_url_is_still_markup_link—type="link"безurl→MarkupLink(url=None)tests/test_strenum_compat.py— тесты backport-модуля:test_auto_generates_lowercase_name—auto()→name.lower()test_members_are_str_instances—isinstance(member, str)test_value_equals_str—.value— строкаtest_unique_decorator_rejects_duplicates—@uniqueбросаетValueErrortest_explicit_value_preserved— явные строки не перезаписываютсяСовместимость
_compat.pyStrEnumCloses
Closes #77