Skip to content

fix: исправлена работа перечислений#80

Merged
love-apples merged 3 commits intolove-apples:mainfrom
Olegt0rr:bugfix/fix-enums
Apr 5, 2026
Merged

fix: исправлена работа перечислений#80
love-apples merged 3 commits intolove-apples:mainfrom
Olegt0rr:bugfix/fix-enums

Conversation

@Olegt0rr
Copy link
Copy Markdown
Collaborator

@Olegt0rr Olegt0rr commented Apr 5, 2026

Описание

Переход всех строковых Enum на StrEnum + auto() + @unique, добавление backport-совместимости для Python 3.10.


Проблема

Пользователь @jimbojohnez сообщил в #77, что после обновления до 0.9.18 элементы разметки с type="link" в MessageBody.markup десериализуются как MarkupElement вместо MarkupLink, из-за чего теряется поле url:

# Ожидалось:
MarkupLink(type=<TextStyle.LINK: 'link'>, from_=119, length=26, url='https://...')

# Получалось:
MarkupElement(type=<TextStyle.LINK: 'link'>, from_=119, length=26)
#                                                                  ^ нет url!

Первопричина

Два связанных бага:

1. Python 3.10 — ImportError

text_style.py импортировал StrEnum из стандартной библиотеки, которая появилась только в Python 3.11. На Python 3.10 (минимальная поддерживаемая версия проекта) весь модуль падал с:

ImportError: cannot import name 'StrEnum' from 'enum'

2. plain Enum — сломанный union-дискриминатор

До 0.9.18 TextStyle наследовался от Enum (без str). При таком варианте Pydantic v2 не может использовать Literal[TextStyle.LINK] как дискриминатор в union — все элементы деградируют до базового MarkupElement:

# До 0.9.18 — СЛОМАНО с plain Enum:
class TextStyle(Enum):          # ← Pydantic не может сравнить "link" (str)
    LINK = "link"               #   с TextStyle.LINK (Enum member) в Literal

class MarkupLink(MarkupElement):
    type: Literal[TextStyle.LINK] = TextStyle.LINK  # дискриминатор не работает

StrEnum решает это: члены StrEnum являются строками, поэтому Pydantic корректно сравнивает "link" с Literal[TextStyle.LINK].


Почему StrEnum, а не Literal[TextStyle.LINK.value]

Клиент предложил точечный фикс #79

type: Literal[TextStyle.LINK.value] = TextStyle.LINK

Это работает, но мы выбрали системное решение через StrEnum по нескольким причинам:

  1. Семантическая корректность. Все наши enum-ы представляют строки из JSON API ("link", "message_created", "dialog", …). StrEnum явно выражает этот контракт: члены являются строками. С plain Enum это неявная конвенция, которую легко нарушить.

  2. Pydantic-совместимость из коробки. StrEnum устраняет целый класс проблем с Literal-дискриминацией. Тот же паттерн Literal[EnumMember] используется ещё в 8 типах вложений (AttachmentType) и 15 типах обновлений (UpdateType) — все они были потенциально сломаны с plain Enum.

  3. Удобство сериализации. StrEnum сериализуется в JSON как обычная строка без .value — не нужны кастомные json_encoders или model_serializer.

  4. Удобство сравнения. TextStyle.LINK == "link"True для StrEnum, False для plain Enum. Код, работающий с API-ответами, становится проще.

Почему auto(), а не явные строки

# Было:
STRONG = "strong"
USER_MENTION = "user_mention"

# Стало:
STRONG = auto()
USER_MENTION = auto()
  1. Единый источник истины. Значение enum-члена определяется его именем (NAME"name"). Невозможно допустить опечатку вида STRONG = "strng" или рассинхронизацию имени и значения.

  2. @unique защищает от дубликатов. С auto() каждое имя гарантированно уникально. С явными строками можно случайно скопировать значение и получить алиас вместо нового члена.

  3. Конвенция 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:

import sys

if sys.version_info >= (3, 11):
    from enum import StrEnum
else:
    from enum import Enum

    class StrEnum(str, Enum):
        @staticmethod
        def _generate_next_value_(name, start, count, last_values):
            return name.lower()

На Python 3.10 backport ведёт себя идентично stdlib StrEnum: auto() генерирует name.lower(), члены являются строками.

2. Все строковые Enum → StrEnum + auto() + @unique

Для enum-ов, где name.lower() совпадает с API-значением:

# Было:
from enum import Enum

class ChatType(str, Enum):
    DIALOG = "dialog"
    CHAT = "chat"
    CHANNEL = "channel"

# Стало:
from enum import auto, unique
from ._compat import StrEnum

@unique
class ChatType(StrEnum):
    DIALOG = auto()
    CHAT = auto()
    CHANNEL = auto()

Для enum-ов с нестандартными значениями (ApiPath, HTTPMethod, AddChatMembersErrorCode) — переведены на StrEnum с явными строками, где auto() дал бы неверное значение (/me, POST, add.participant.privacy).


Изменённые файлы

Файл Изменение
maxapi/enums/_compat.py ✨ Новый: backport StrEnum для Python 3.10
maxapi/enums/text_style.py StrEnum (stdlib) → compat + все значения auto() + @unique
maxapi/enums/attachment.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/button_type.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/chat_permission.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/chat_status.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/chat_type.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/intent.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/message_link_type.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/parse_mode.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/sender_action.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/update.py (str, Enum)StrEnum + auto() + @unique
maxapi/enums/upload_type.py (str, Enum)StrEnum + auto() + @unique
maxapi/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.py ✨ Новый: 5 регрессионных тестов
tests/test_strenum_compat.py ✨ Новый: 5 тестов на backport _compat.py

Тесты

tests/test_markup_discrimination.py — регрессия для сценария из репорта:

  • test_link_markup_deserialized_as_markup_linktype="link"MarkupLink
  • test_user_mention_markup_deserializedtype="user_mention"MarkupUserMention
  • test_strong_markup_remains_markup_elementtype="strong"MarkupElement
  • test_markup_link_url_preserved_from_apiurl из API сохраняется в MarkupLink.url
  • test_markup_link_without_url_is_still_markup_linktype="link" без urlMarkupLink(url=None)

tests/test_strenum_compat.py — тесты backport-модуля:

  • test_auto_generates_lowercase_nameauto()name.lower()
  • test_members_are_str_instancesisinstance(member, str)
  • test_value_equals_str.value — строка
  • test_unique_decorator_rejects_duplicates@unique бросает ValueError
  • test_explicit_value_preserved — явные строки не перезаписываются
410 passed, 4 skipped in 0.65s

Совместимость

  • ✅ Python 3.10 — через backport в _compat.py
  • ✅ Python 3.11–3.14 — stdlib StrEnum
  • ✅ Pydantic v2.x — без изменений в зависимостях
  • ✅ Все значения enum-ов остались теми же (API не ломается)

Closes

Closes #77

Copilot AI review requested due to automatic review settings April 5, 2026 05:46
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread maxapi/enums/_compat.py
Comment thread tests/test_strenum_compat.py
@Olegt0rr Olegt0rr mentioned this pull request Apr 5, 2026
@Olegt0rr Olegt0rr requested a review from Copilot April 5, 2026 06:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread maxapi/enums/_compat.py
@love-apples love-apples merged commit 025493a into love-apples:main Apr 5, 2026
16 of 17 checks passed
@Olegt0rr Olegt0rr deleted the bugfix/fix-enums branch April 5, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MarkupElement вместо MarkupLink в MessageBody.markup

3 participants