This commit is contained in:
2025-11-21 19:41:35 +03:00
parent 8edd59c1b8
commit 2e76f68d4a
37 changed files with 395 additions and 482 deletions
@@ -0,0 +1,32 @@
import sys
from unittest.mock import patch
import pytest
from pytest import CaptureFixture
from argenta import App, Orchestrator, Router, Command, Response
@pytest.fixture(autouse=True)
def patched_argv():
with patch.object(sys, 'argv', ['program.py']):
yield
def test_input_incorrect_command(capsys: CaptureFixture[str]):
router = Router()
orchestrator = Orchestrator()
@router.command(Command('test'))
def test(response: Response) -> None:
print('test command')
app = App(override_system_messages=True, print_func=print)
app.include_router(router)
app.set_unknown_command_handler(
lambda command: print(f'Unknown command: {command.trigger}')
)
with patch("builtins.input", side_effect=["help", "q"]):
orchestrator.start_polling(app)
output = capsys.readouterr().out
assert "\nUnknown command: help\n" in output
@@ -1,25 +1,21 @@
import io
import unittest
from contextlib import redirect_stdout
from argenta import App, Router, Command, Response
from argenta.command import InputCommand
class TestAppIntegration(unittest.TestCase):
def setUp(self) -> None:
self.app = App(override_system_messages=True, repeat_command_groups=False)
self.router = Router(title="App")
def test_simple_app() -> None:
app = App(override_system_messages=True, repeat_command_groups_printing=False)
router = Router(title="App")
@self.router.command(Command("HELP", description="Show help"))
def help_cmd(response: Response):
print("Available commands: HELP")
@router.command(Command("HELP", description="Show help"))
def help_cmd(response: Response):
print("Available commands: HELP")
_ = help_cmd # appease linter: function is registered via decorator
app.include_router(router)
self.app.include_router(self.router)
def test_help_command(self):
with redirect_stdout(io.StringIO()) as stdout:
self.router.finds_appropriate_handler(InputCommand.parse("HELP"))
self.assertIn("Available commands:", stdout.getvalue())
with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse("HELP"))
assert "Available commands:" in stdout.getvalue()
@@ -1,59 +1,42 @@
import io
import unittest
from contextlib import redirect_stdout
from unittest.mock import Mock
from dishka import Provider, provide, make_container, Scope # pyright: ignore[reportUnknownVariableType]
from argenta.command import InputCommand
from dishka import Provider, make_container, Scope
from argenta import Router, Command
from argenta.di.integration import inject, setup_dishka, FromDishka
from argenta.response import Response
from argenta.response.status import ResponseStatus
from argenta import Router, Response
from argenta.di.integration import setup_dishka, FromDishka
class Service:
def hello(self) -> str:
return "world"
def get_service() -> Service:
return Service()
router = Router(title="DI")
@router.command(Command("HELLO"))
@inject # Auto-inject dependencies from the Response container
@router.command("HELLO")
def hello(response: Response, service: FromDishka[Service]) -> None:
print(f"hello {service.hello()}")
class TestDIHandler(unittest.TestCase):
def test_hello_uses_service(self):
# Prepare DI container with a stub
fake = Mock(spec=Service)
fake.hello.return_value = "mocked"
class TestProvider(Provider):
scope = Scope.APP
@provide(scope=Scope.APP)
def service(self) -> Service:
return fake
container = make_container(TestProvider())
# Bind container to Response via integration
setup_dishka(app=_FakeApp(), container=container, auto_inject=False) # type: ignore[arg-type]
# Create Response bound to the container
r = Response(ResponseStatus.ALL_FLAGS_VALID)
r._dishka_container = container # direct assignment is acceptable in tests # pyright: ignore[reportPrivateUsage]
# Call handler directly
with redirect_stdout(io.StringIO()) as stdout:
hello(r)
self.assertIn("hello mocked", stdout.getvalue())
class _FakeApp:
# Minimal stub for setup_dishka; app object is not used in unit tests
registered_routers = []
registered_routers = [router]
def test_hello_uses_service():
provider = Provider(scope=Scope.APP)
provider.provide(get_service)
container = make_container(provider)
setup_dishka(app=_FakeApp(), container=container, auto_inject=True)
# Call handler
with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse('HELLO'))
assert "hello world" in stdout.getvalue()
@@ -1,5 +1,4 @@
import io
import unittest
from contextlib import redirect_stdout
from argenta import Router, Command, Response
@@ -8,15 +7,13 @@ from argenta.command import InputCommand
router = Router(title="Demo")
@router.command(Command("PING", description="Ping command"))
def ping(response: Response):
print("PONG")
class TestSimpleHandler(unittest.TestCase):
def test_ping_prints_pong(self):
# Имитация запуска хендлера через роутер
with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse("PING"))
self.assertIn("PONG", stdout.getvalue())
def test_ping_prints_pong():
# Call handler
with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse("PING"))
assert "PONG" in stdout.getvalue()
+1 -5
View File
@@ -12,7 +12,7 @@ default:
@{{sphinxbuild}} -M help "{{sourcedir}}" "{{builddir}}" {{sphinxopts}}
# Build all language versions
build-all:
build:
{{sphinxbuild}} -b html -D language=ru {{sourcedir}} {{builddir}}/html/ru
{{sphinxbuild}} -b html -D language =en {{sourcedir}} {{builddir}}/html/en
@@ -28,7 +28,3 @@ live-en:
update-langs:
{{sphinxbuild}} -b gettext . _build/gettext
sphinx-intl update -p _build/gettext -l en
# Generic build target (html, latex, etc.)
build target:
{{sphinxbuild}} -M {{target}} "{{sourcedir}}" "{{builddir}}" {{sphinxopts}}
@@ -75,7 +75,7 @@ msgstr ""
#: ../../root/api/app/index.rst:47
msgid ""
"``repeat_command_groups``: Если ``True``, список доступных команд "
"``repeat_command_groups_printing``: Если ``True``, список доступных команд "
"выводится перед каждым вводом."
msgstr ""
@@ -296,7 +296,7 @@ msgstr ""
#~ msgstr ""
#~ msgid ""
#~ "``repeat_command_groups``: Если **True** (по "
#~ "``repeat_command_groups_printing``: Если **True** (по "
#~ "умолчанию), описание доступных команд будет"
#~ " выводиться перед каждым вводом."
#~ msgstr ""
+9 -9
View File
@@ -30,7 +30,7 @@ App
system_router_title: str | None = "System points:",
ignore_command_register: bool = True,
dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE,
repeat_command_groups: bool = True,
repeat_command_groups_printing: bool = True,
override_system_messages: bool = False,
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
print_func: Printer = DEFAULT_PRINT_FUNC) -> None
@@ -40,14 +40,14 @@ App
* ``prompt``: Приглашение к вводу, отображаемое перед каждой командой.
* ``initial_message``: Сообщение, выводимое при запуске приложения.
* ``farewell_message``: Сообщение, выводимое при выходе из приложения.
* ``exit_command``: Команда, используемая для выхода из приложения.
* ``exit_command``: Команда, которая маркируется как триггер для выхода из приложения.
* ``system_router_title``: Заголовок для системного роутера (содержит команду выхода).
* ``ignore_command_register``: Если ``True``, регистр команд игнорируется при поиске обработчика.
* ``dividing_line``: Стиль разделительной линии (``StaticDividingLine`` или ``DynamicDividingLine``).
* ``repeat_command_groups``: Если ``True``, список доступных команд выводится перед каждым вводом.
* ``ignore_command_register``: Если ``True``, регистр вводимых команд игнорируется при поиске обработчика.
* ``dividing_line``: Тип разделительной линии (``StaticDividingLine`` или ``DynamicDividingLine``).
* ``repeat_command_groups_printing``: Если ``True``, список доступных команд выводится перед каждым вводом.
* ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается.
* ``autocompleter``: Объект, отвечающий за автодополнение команд.
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.print``).
* ``autocompleter``: Экземпляр класса :ref:`AutoCompleter <root_api_app_autocompleter>`, отвечающий за автодополнение команд.
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.Console().print``).
-----
@@ -68,7 +68,7 @@ App
- .. py:method:: add_message_on_startup(self, message: str) -> None
Добавляет текстовое сообщение, которое выводится при запуске приложения после `initial_message`.
Добавляет текстовое сообщение, которое выводится при запуске приложения после ``initial_message``.
:param message: Строка с сообщением.
@@ -89,7 +89,7 @@ App
.. py:method:: set_description_message_pattern(self, handler: Callable[[str, str], str]) -> None
Устанавливает шаблон для форматирования строки описания команды.
Устанавливает шаблон для форматирования описания команды.
Обработчик принимает триггер команды (``str``) и её описание (``str``), а возвращает отформатированную строку.
+12 -14
View File
@@ -27,14 +27,12 @@
.. code-block:: python
from argenta import (
App, Orchestrator, Router, Command, Response
)
from argenta import App, Orchestrator, Router, Command, Response
* :ref:`App <root_api_app_index>`Основной класс приложения.
* :ref:`Orchestrator <root_api_orchestrator_index>` — Класс для управления жизненным циклом.
* :ref:`App <root_api_app_index>`Объект приложения, который отвечает за логику роутинга, настройки, валидации и т.д.
* :ref:`Orchestrator <root_api_orchestrator_index>` — Класс для конфигурирования и запуска всего приложения.
* :ref:`Router <root_api_router>` — Класс для группировки и регистрации команд.
* :ref:`Command <root_api_command_index>` — Класс для создания команд.
* :ref:`Command <root_api_command_index>` — Класс для создания команд при инициализации хэндлеров.
* :ref:`Response <root_api_response>` — Объект ответа, передаваемый в обработчики.
.. rubric:: Команды и флаги
@@ -55,9 +53,9 @@
* :ref:`Flags <root_api_command_flags>` — Коллекция для регистрации флагов.
* :ref:`InputFlag <root_api_command_input_flag>` — Класс для введённого пользователем флага.
* :ref:`InputFlags <root_api_command_input_flags>` — Коллекция введённых флагов.
* :ref:`PossibleValues <root_api_command_possible_values>` — Правила валидации значений флагов.
* :ref:`PossibleValues <root_api_command_possible_values>` — Правила валидации значений флага.
* :ref:`ValidationStatus <root_api_command_validation_status>` — Статусы валидации флагов.
* ``PredefinedFlags`` — Готовые наборы флагов (например, ``--help``).
* :ref:`PredefinedFlags <root_api_command_flag_predefined_flags>` — Коллекция предопределённых флагов.
.. rubric:: Настройка приложения
@@ -70,10 +68,10 @@
PredefinedMessages
)
* :ref:`AutoCompleter <root_api_app_autocompleter>` — Базовый класс для автодополнения.
* :ref:`StaticDividingLine <root_api_app_dividing_lines>` — Статическая разделительная линия.
* :ref:`DynamicDividingLine <root_api_app_dividing_lines>` — Динамическая разделительная линия.
* ``PredefinedMessages`` — Готовые системные сообщения.
* :ref:`AutoCompleter <root_api_app_autocompleter>` - Класс для настройки автодополнения.
* :ref:`StaticDividingLine <root_api_app_dividing_lines>` — Статическая разделительная линия для оформления вывода.
* :ref:`DynamicDividingLine <root_api_app_dividing_lines>` — Динамическая разделительная линия для оформления вывода.
* :ref:`PredefinedMessages <root_api_predefined_messages>` — Готовые сообщения для вывода при старте приложения.
.. rubric:: Внедрение зависимостей
@@ -84,8 +82,8 @@
inject
)
* :ref:`FromDishka <root_dependency_injection>` — Маркер для внедрения зависимостей.
* :ref:`inject <root_dependency_injection>` — Декоратор для асинхронного внедрения.
* :ref:`FromDishka <root_dependency_injection>` — Маркер аргумента функции как зависимости, которая должна быть инжектирована.
* :ref:`inject <root_dependency_injection>` — Декоратор для инжектирования зависимостей, указанных в сигнатуре.
.. toctree::
+1 -1
View File
@@ -6,7 +6,7 @@
Наше обязательство
------------------
В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, гендерной идентичности и самовыражения, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы, религии или сексуальной идентичности и ориентации.
В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы или религии.
-----
+16 -13
View File
@@ -55,7 +55,7 @@
Поищите ответ в существующих `Issues <https://github.com/koloideal/Argenta/issues>`_. Если вы нашли похожий вопрос, но всё ещё нуждаетесь в разъяснениях, можете написать в нём. Также рекомендуем поискать ответ в интернете.
Если ответа не нашлось, создайте новый `Issue <https://github.com/koloideal/Argenta/issues/new>`_ и предоставьте как можно больше контекста, включая версии проекта и платформы (CPython, pip и т.д.).
Если ответа не нашлось, создайте новый `Issue <https://github.com/koloideal/Argenta/issues/new>`_ и предоставьте как можно больше контекста, включая версии проекта и платформы.
Мы займемся вашей задачей как можно скорее.
@@ -87,10 +87,10 @@
* Также поищите в интернете (включая `Stack Overflow`), чтобы узнать, обсуждалась ли проблема за пределами `GitHub`.
* Соберите информацию об ошибке:
* Трассировка стека.
* ОС, платформа и версия (Windows, Linux, macOS, x86, ARM).
* Версия интерпретатора, компилятора, SDK, среды выполнения, менеджера пакетов и т.д.
* Входные данные и полученный результат.
* Можете ли вы надёжно воспроизвести проблему? Воспроизводится ли она на старых версиях?
* ОС, платформа и версия (Windows, Linux, macOS, x86, ARM).
* Версия интерпретатора, компилятора, SDK, среды выполнения, менеджера пакетов и т.д.
* Входные данные и полученный результат.
* Можете ли вы надёжно воспроизвести проблему? Воспроизводится ли она на старых версиях?
.. rubric:: Как мне отправить хороший отчет об ошибке?
@@ -187,7 +187,7 @@
python -m pytest tests
#. Сделайте коммит, следуя нашему руководству по стилю, и отправьте изменения в ваш форк.
#. Сделайте коммит, следуя :ref:`нашему руководству по стилю <styleguide>`, и отправьте изменения в ваш форк.
.. code-block:: bash
@@ -209,6 +209,10 @@
.. note::
Мы поддерживаем документацию на двух языках: русском и английском.
.. important::
Для инкапсуляции различных команд, необходимых для настройки и запуска проекта мы используем ``just``, он же фигурирует в различных примерах в документации, поэтому рекомендуем вам `установить его <https://github.com/casey/just#installation>`_
Для улучшения документации вы можете следовать процессу, похожему на внесение вклада в код:
@@ -220,33 +224,32 @@
cd docs
#. Внесите изменения в **русскую** версию документации (`docs/index.rst` и/или `docs/root/*`).
#. Чтобы собрать документацию локально и увидеть изменения, выполните:
#. Чтобы собрать документацию локально в режиме автоматического ребилда и увидеть изменения, выполните:
.. code-block:: bash
make live-ru
just live-ru
#. Откройте `127.0.0.1:8000` в браузере, чтобы просмотреть сгенерированную документацию.
#. После завершения работы над русской версией необходимо создать английский перевод:
.. code-block:: bash
make update-langs
just update-langs
#. После обновления шаблона обновите файлы перевода, расположенные в `docs/locales/en/LC_MESSAGES/`.
#. Когда изменения будут готовы, сделайте коммит и откройте `Pull Request`. Используйте префикс `docs:` в сообщении коммита.
-----
.. _Руководства по стилю:
.. _styleguide:
Руководства по стилю
--------------------
.. _Сообщения коммитов:
.. _commits_messages:
Сообщения коммитов
~~~~~~~~~~~~~~~~~~
**Сообщения коммитов**
Мы следуем спецификации `Conventional Commits <https://www.conventionalcommits.org/en/v1.0.0/>`_. Это делает историю проекта более читаемой и позволяет автоматически генерировать журнал изменений.
+19 -4
View File
@@ -1,18 +1,20 @@
Тестирование
============
В этом разделе описаны практики тестирования приложений на основе ``Argenta``. Примеры основаны на фактическом публичном API: ``App``, ``Router``, ``Command``, ``Orchestrator``, DI через ``dishka`` и интеграцию в ``argenta.di.integration``.
В этом разделе описаны практики тестирования приложений на основе ``Argenta``. Примеры основаны на фактическом публичном API.
Модульное тестирование хендлеров
--------------------------------
Обработчики в Argenta — обычные функции. Их удобно тестировать как чистые функции, не поднимая весь цикл приложения. Рекомендуются ``unittest`` или ``pytest``.
Пример с ``unittest`` для простого хендлера без DI:
Пример для простого хендлера без DI:
.. literalinclude:: ../code_snippets/testing/simple_handler_unittest.py
:language: python
:linenos:
-----
Тестирование с внедрением зависимостей (DI)
-------------------------------------------
@@ -22,6 +24,8 @@
.. literalinclude:: ../code_snippets/testing/di_handler_unittest.py
:language: python
:linenos:
-----
Интеграционное тестирование приложения
--------------------------------------
@@ -31,11 +35,22 @@
.. literalinclude:: ../code_snippets/testing/app_integration_unittest.py
:language: python
:linenos:
-----
E2E-тестирование цикла (опционально)
E2E-тестирование цикла
------------------------------------
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк во ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — вынесите конфигурацию в функцию ``main()`` и запускайте модуль в подпроцессе с подготовленным вводом/выводом.
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
.. danger ::
Обязательно передавайте строковый триггер команды выхода последним элементом в списке, который передаёте в контекстном менеджере при патче ``input`` как аргумент ``side_effects``, иначе тестируемое приложение будет ожидать ввода следующей команды и не сможет корректно завершиться.
.. literalinclude:: ../code_snippets/testing/app_e2e_test.py
:language: python
:linenos:
-----
Советы по тестированию
----------------------