From 2e76f68d4a92ca8fa06b3ce0acdb2bc031645f1e Mon Sep 17 00:00:00 2001 From: kolo Date: Fri, 21 Nov 2025 19:41:35 +0300 Subject: [PATCH] docs --- docs/code_snippets/testing/app_e2e_test.py | 32 ++++ .../testing/app_integration_unittest.py | 26 ++-- .../testing/di_handler_unittest.py | 69 ++++----- .../testing/simple_handler_unittest.py | 13 +- docs/justfile | 6 +- .../en/LC_MESSAGES/root/api/app/index.po | 4 +- docs/root/api/app/index.rst | 18 +-- docs/root/api/index.rst | 26 ++-- docs/root/code_of_conduct.rst | 2 +- docs/root/contributing.rst | 29 ++-- docs/root/testing.rst | 23 ++- justfile | 32 ++++ pyproject.toml | 4 + src/argenta/__init__.py | 2 +- src/argenta/app/__init__.py | 6 +- src/argenta/app/autocompleter/entity.py | 27 +--- src/argenta/app/defaults.py | 1 + src/argenta/app/dividing_line/__init__.py | 6 +- src/argenta/app/models.py | 144 +++++++----------- src/argenta/app/protocols.py | 3 +- src/argenta/command/exceptions.py | 16 +- src/argenta/command/flag/defaults.py | 30 ++-- src/argenta/command/flag/models.py | 34 +++-- src/argenta/command/models.py | 43 +++--- src/argenta/di/integration.py | 8 +- src/argenta/di/providers.py | 10 +- src/argenta/metrics/__init__.py | 3 +- src/argenta/metrics/main.py | 4 +- .../orchestrator/argparser/__init__.py | 6 +- .../argparser/arguments/__init__.py | 9 +- .../argparser/arguments/models.py | 46 +++--- src/argenta/orchestrator/argparser/entity.py | 65 ++++---- src/argenta/orchestrator/entity.py | 13 +- src/argenta/response/status.py | 8 +- src/argenta/router/command_handler/entity.py | 4 +- src/argenta/router/entity.py | 34 ++--- translator.py | 71 --------- 37 files changed, 395 insertions(+), 482 deletions(-) create mode 100644 docs/code_snippets/testing/app_e2e_test.py create mode 100644 justfile delete mode 100644 translator.py diff --git a/docs/code_snippets/testing/app_e2e_test.py b/docs/code_snippets/testing/app_e2e_test.py new file mode 100644 index 0000000..6d0d4a9 --- /dev/null +++ b/docs/code_snippets/testing/app_e2e_test.py @@ -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 diff --git a/docs/code_snippets/testing/app_integration_unittest.py b/docs/code_snippets/testing/app_integration_unittest.py index 4f1ef30..f3a0caf 100644 --- a/docs/code_snippets/testing/app_integration_unittest.py +++ b/docs/code_snippets/testing/app_integration_unittest.py @@ -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() diff --git a/docs/code_snippets/testing/di_handler_unittest.py b/docs/code_snippets/testing/di_handler_unittest.py index 63ff89f..62b897c 100644 --- a/docs/code_snippets/testing/di_handler_unittest.py +++ b/docs/code_snippets/testing/di_handler_unittest.py @@ -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() diff --git a/docs/code_snippets/testing/simple_handler_unittest.py b/docs/code_snippets/testing/simple_handler_unittest.py index 3d0c835..176b5e6 100644 --- a/docs/code_snippets/testing/simple_handler_unittest.py +++ b/docs/code_snippets/testing/simple_handler_unittest.py @@ -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() diff --git a/docs/justfile b/docs/justfile index afe81be..7f8b733 100644 --- a/docs/justfile +++ b/docs/justfile @@ -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}} diff --git a/docs/locales/en/LC_MESSAGES/root/api/app/index.po b/docs/locales/en/LC_MESSAGES/root/api/app/index.po index 33cfbb9..cfa12e3 100644 --- a/docs/locales/en/LC_MESSAGES/root/api/app/index.po +++ b/docs/locales/en/LC_MESSAGES/root/api/app/index.po @@ -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 "" diff --git a/docs/root/api/app/index.rst b/docs/root/api/app/index.rst index 30924ee..33e6c65 100644 --- a/docs/root/api/app/index.rst +++ b/docs/root/api/app/index.rst @@ -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 `, отвечающий за автодополнение команд. + * ``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``), а возвращает отформатированную строку. diff --git a/docs/root/api/index.rst b/docs/root/api/index.rst index b53bc95..b360276 100644 --- a/docs/root/api/index.rst +++ b/docs/root/api/index.rst @@ -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 ` — Основной класс приложения. -* :ref:`Orchestrator ` — Класс для управления жизненным циклом. +* :ref:`App ` — Объект приложения, который отвечает за логику роутинга, настройки, валидации и т.д. +* :ref:`Orchestrator ` — Класс для конфигурирования и запуска всего приложения. * :ref:`Router ` — Класс для группировки и регистрации команд. -* :ref:`Command ` — Класс для создания команд. +* :ref:`Command ` — Класс для создания команд при инициализации хэндлеров. * :ref:`Response ` — Объект ответа, передаваемый в обработчики. .. rubric:: Команды и флаги @@ -55,9 +53,9 @@ * :ref:`Flags ` — Коллекция для регистрации флагов. * :ref:`InputFlag ` — Класс для введённого пользователем флага. * :ref:`InputFlags ` — Коллекция введённых флагов. -* :ref:`PossibleValues ` — Правила валидации значений флагов. +* :ref:`PossibleValues ` — Правила валидации значений флага. * :ref:`ValidationStatus ` — Статусы валидации флагов. -* ``PredefinedFlags`` — Готовые наборы флагов (например, ``--help``). +* :ref:`PredefinedFlags ` — Коллекция предопределённых флагов. .. rubric:: Настройка приложения @@ -70,10 +68,10 @@ PredefinedMessages ) -* :ref:`AutoCompleter ` — Базовый класс для автодополнения. -* :ref:`StaticDividingLine ` — Статическая разделительная линия. -* :ref:`DynamicDividingLine ` — Динамическая разделительная линия. -* ``PredefinedMessages`` — Готовые системные сообщения. +* :ref:`AutoCompleter ` - Класс для настройки автодополнения. +* :ref:`StaticDividingLine ` — Статическая разделительная линия для оформления вывода. +* :ref:`DynamicDividingLine ` — Динамическая разделительная линия для оформления вывода. +* :ref:`PredefinedMessages ` — Готовые сообщения для вывода при старте приложения. .. rubric:: Внедрение зависимостей @@ -84,8 +82,8 @@ inject ) -* :ref:`FromDishka ` — Маркер для внедрения зависимостей. -* :ref:`inject ` — Декоратор для асинхронного внедрения. +* :ref:`FromDishka ` — Маркер аргумента функции как зависимости, которая должна быть инжектирована. +* :ref:`inject ` — Декоратор для инжектирования зависимостей, указанных в сигнатуре. .. toctree:: diff --git a/docs/root/code_of_conduct.rst b/docs/root/code_of_conduct.rst index 87cb74d..5ccd41d 100644 --- a/docs/root/code_of_conduct.rst +++ b/docs/root/code_of_conduct.rst @@ -6,7 +6,7 @@ Наше обязательство ------------------ -В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, гендерной идентичности и самовыражения, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы, религии или сексуальной идентичности и ориентации. +В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы или религии. ----- diff --git a/docs/root/contributing.rst b/docs/root/contributing.rst index 6d0447d..54f6cb0 100644 --- a/docs/root/contributing.rst +++ b/docs/root/contributing.rst @@ -55,7 +55,7 @@ Поищите ответ в существующих `Issues `_. Если вы нашли похожий вопрос, но всё ещё нуждаетесь в разъяснениях, можете написать в нём. Также рекомендуем поискать ответ в интернете. -Если ответа не нашлось, создайте новый `Issue `_ и предоставьте как можно больше контекста, включая версии проекта и платформы (CPython, pip и т.д.). +Если ответа не нашлось, создайте новый `Issue `_ и предоставьте как можно больше контекста, включая версии проекта и платформы. Мы займемся вашей задачей как можно скорее. @@ -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:`нашему руководству по стилю `, и отправьте изменения в ваш форк. .. code-block:: bash @@ -209,6 +209,10 @@ .. note:: Мы поддерживаем документацию на двух языках: русском и английском. + +.. important:: + + Для инкапсуляции различных команд, необходимых для настройки и запуска проекта мы используем ``just``, он же фигурирует в различных примерах в документации, поэтому рекомендуем вам `установить его `_ Для улучшения документации вы можете следовать процессу, похожему на внесение вклада в код: @@ -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 `_. Это делает историю проекта более читаемой и позволяет автоматически генерировать журнал изменений. diff --git a/docs/root/testing.rst b/docs/root/testing.rst index cbe81c5..652ddc0 100644 --- a/docs/root/testing.rst +++ b/docs/root/testing.rst @@ -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: + +----- Советы по тестированию ---------------------- diff --git a/justfile b/justfile new file mode 100644 index 0000000..1649082 --- /dev/null +++ b/justfile @@ -0,0 +1,32 @@ +set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] +set shell := ["bash", "-c"] + +# Вывести список всех рецептов +default: + @just --list + +# Запустить тесты через pytest +tests: + python -m pytest tests + +# Запустить тесты с отчетом о покрытии +tests-cov: + python -m pytest --cov=argenta tests + +# Отформатировать код (Ruff + isort) +format: + python -m ruff format ./src + python -m isort ./src + +# Проверить типы через mypy (strict) +mypy: + python -m mypy -p argenta --strict + +# Проверить стиль через wemake-python-styleguide +wps: + python -m flake8 --format=wemake ./src + +# Запустить линтер Ruff +ruff: + python -m ruff check ./src + diff --git a/pyproject.toml b/pyproject.toml index 90c4553..3e3642a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ exclude = [ ".__pycache__", "tests" ] +line-length=110 [tool.pyright] typeCheckingMode = "strict" @@ -52,6 +53,9 @@ typeCheckingMode = "strict" [tool.mypy] disable_error_code = "import-untyped" +[tool.isort] +line_length=90 + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/src/argenta/__init__.py b/src/argenta/__init__.py index e631d0b..6def41f 100644 --- a/src/argenta/__init__.py +++ b/src/argenta/__init__.py @@ -1,6 +1,6 @@ from argenta.app.models import App as App from argenta.command.models import Command as Command +from argenta.data_bridge.entity import DataBridge as DataBridge from argenta.orchestrator.entity import Orchestrator as Orchestrator from argenta.response.entity import Response as Response from argenta.router.entity import Router as Router -from argenta.data_bridge.entity import DataBridge as DataBridge diff --git a/src/argenta/app/__init__.py b/src/argenta/app/__init__.py index c5d4e46..0cd6ba0 100644 --- a/src/argenta/app/__init__.py +++ b/src/argenta/app/__init__.py @@ -1,7 +1,5 @@ from argenta.app.autocompleter.entity import AutoCompleter as AutoCompleter from argenta.app.defaults import PredefinedMessages as PredefinedMessages -from argenta.app.dividing_line.models import \ - DynamicDividingLine as DynamicDividingLine -from argenta.app.dividing_line.models import \ - StaticDividingLine as StaticDividingLine +from argenta.app.dividing_line.models import DynamicDividingLine as DynamicDividingLine +from argenta.app.dividing_line.models import StaticDividingLine as StaticDividingLine from argenta.app.models import App as App diff --git a/src/argenta/app/autocompleter/entity.py b/src/argenta/app/autocompleter/entity.py index 83e68c8..7f1479f 100644 --- a/src/argenta/app/autocompleter/entity.py +++ b/src/argenta/app/autocompleter/entity.py @@ -6,10 +6,7 @@ from typing import Never class AutoCompleter: - def __init__( - self, history_filename: str | None = None, - autocomplete_button: str = "tab" - ) -> None: + def __init__(self, history_filename: str | None = None, autocomplete_button: str = "tab") -> None: """ Public. Configures and implements auto-completion of input command :param history_filename: the name of the file for saving the history of the autocompleter @@ -26,18 +23,12 @@ class AutoCompleter: :param state: the current cursor position is relative to the beginning of the line :return: the desired candidate as str or None """ - matches: list[str] = sorted( - cmd for cmd in _get_history_items() if cmd.startswith(text) - ) + matches: list[str] = sorted(cmd for cmd in _get_history_items() if cmd.startswith(text)) if len(matches) > 1: common_prefix = matches[0] for match in matches[1:]: i = 0 - while ( - i < len(common_prefix) - and i < len(match) - and common_prefix[i] == match[i] - ): + while i < len(common_prefix) and i < len(match) and common_prefix[i] == match[i]: i += 1 common_prefix = common_prefix[:i] if state == 0: @@ -61,7 +52,7 @@ class AutoCompleter: else: for line in all_commands: readline.add_history(line) - + if not self.history_filename: for line in all_commands: readline.add_history(line) @@ -85,19 +76,17 @@ class AutoCompleter: pretty_history.append(line) with open(self.history_filename, "w") as history_file: _ = history_file.write("\n".join(pretty_history)) - - + + def _is_command_exist(command: str, existing_commands: list[str], ignore_command_register: bool) -> bool: if ignore_command_register: return command.lower() in existing_commands return command in existing_commands + def _get_history_items() -> list[str] | list[Never]: """ Private. Returns a list of all commands entered by the user :return: all commands entered by the user as list[str] | list[Never] """ - return [ - readline.get_history_item(i) - for i in range(1, readline.get_current_history_length() + 1) - ] + return [readline.get_history_item(i) for i in range(1, readline.get_current_history_length() + 1)] diff --git a/src/argenta/app/defaults.py b/src/argenta/app/defaults.py index 8bead5e..cf0f73d 100644 --- a/src/argenta/app/defaults.py +++ b/src/argenta/app/defaults.py @@ -7,6 +7,7 @@ class PredefinedMessages(StrEnum): """ Public. A dataclass with predetermined messages for quick use """ + USAGE = "[b dim]Usage[/b dim]: [i] <[green]flags[/green]>[/i]" HELP = "[b dim]Help[/b dim]: [i][/i] [b red]--help[/b red]" AUTOCOMPLETE = "[b dim]Autocomplete[/b dim]: [i][/i] [bold]" diff --git a/src/argenta/app/dividing_line/__init__.py b/src/argenta/app/dividing_line/__init__.py index 289c51c..73b9894 100644 --- a/src/argenta/app/dividing_line/__init__.py +++ b/src/argenta/app/dividing_line/__init__.py @@ -1,4 +1,2 @@ -from argenta.app.dividing_line.models import \ - DynamicDividingLine as DynamicDividingLine -from argenta.app.dividing_line.models import \ - StaticDividingLine as StaticDividingLine +from argenta.app.dividing_line.models import DynamicDividingLine as DynamicDividingLine +from argenta.app.dividing_line.models import StaticDividingLine as StaticDividingLine diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index d1faa31..328a87d 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -5,22 +5,25 @@ import re from contextlib import redirect_stdout from typing import Never, TypeAlias -from art import \ - text2art # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType] +from art import text2art from rich.console import Console from rich.markup import escape from argenta.app.autocompleter import AutoCompleter -from argenta.app.dividing_line.models import (DynamicDividingLine, - StaticDividingLine) -from argenta.app.protocols import (DescriptionMessageGenerator, - EmptyCommandHandler, - NonStandardBehaviorHandler, Printer) +from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine +from argenta.app.protocols import ( + DescriptionMessageGenerator, + EmptyCommandHandler, + NonStandardBehaviorHandler, + Printer, +) from argenta.app.registered_routers.entity import RegisteredRouters -from argenta.command.exceptions import (EmptyInputCommandException, - InputCommandException, - RepeatedInputFlagsException, - UnprocessedInputFlagException) +from argenta.command.exceptions import ( + EmptyInputCommandException, + InputCommandException, + RepeatedInputFlagsException, + UnprocessedInputFlagException, +) from argenta.command.models import Command, InputCommand from argenta.response import Response from argenta.router import Router @@ -40,7 +43,7 @@ class BaseApp: system_router_title: str | None, ignore_command_register: bool, dividing_line: StaticDividingLine | DynamicDividingLine, - repeat_command_groups: bool, + repeat_command_groups_printing: bool, override_system_messages: bool, autocompleter: AutoCompleter, print_func: Printer, @@ -51,7 +54,7 @@ class BaseApp: self._system_router_title: str | None = system_router_title self._dividing_line: StaticDividingLine | DynamicDividingLine = dividing_line self._ignore_command_register: bool = ignore_command_register - self._repeat_command_groups_description: bool = repeat_command_groups + self._repeat_command_groups_printing_description: bool = repeat_command_groups_printing self._override_system_messages: bool = override_system_messages self._autocompleter: AutoCompleter = autocompleter @@ -73,25 +76,21 @@ class BaseApp: else self._matching_default_triggers_with_routers ) - self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = ( - lambda _: print_func(f"Incorrect flag syntax: {_}") + self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = lambda _: print_func( + f"Incorrect flag syntax: {_}" ) - self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = ( - lambda _: print_func(f"Repeated input flags: {_}") + self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = lambda _: print_func( + f"Repeated input flags: {_}" ) - self._empty_input_command_handler: EmptyCommandHandler = lambda: print_func( - "Empty input command" + self._empty_input_command_handler: EmptyCommandHandler = lambda: print_func("Empty input command") + self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = lambda _: print_func( + f"Unknown command: {_.trigger}" ) - self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = ( - lambda _: print_func(f"Unknown command: {_.trigger}") - ) - self._exit_command_handler: NonStandardBehaviorHandler[Response] = ( - lambda _: print_func(self._farewell_message) + self._exit_command_handler: NonStandardBehaviorHandler[Response] = lambda _: print_func( + self._farewell_message ) - def set_description_message_pattern( - self, _: DescriptionMessageGenerator, / - ) -> None: + def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None: """ Public. Sets the output pattern of the available commands :param _: output pattern of the available commands @@ -99,9 +98,7 @@ class BaseApp: """ self._description_message_gen = _ - def set_incorrect_input_syntax_handler( - self, _: NonStandardBehaviorHandler[str], / - ) -> None: + def set_incorrect_input_syntax_handler(self, _: NonStandardBehaviorHandler[str], /) -> None: """ Public. Sets the handler for incorrect flags when entering a command :param _: handler for incorrect flags when entering a command @@ -109,9 +106,7 @@ class BaseApp: """ self._incorrect_input_syntax_handler = _ - def set_repeated_input_flags_handler( - self, _: NonStandardBehaviorHandler[str], / - ) -> None: + def set_repeated_input_flags_handler(self, _: NonStandardBehaviorHandler[str], /) -> None: """ Public. Sets the handler for repeated flags when entering a command :param _: handler for repeated flags when entering a command @@ -119,9 +114,7 @@ class BaseApp: """ self._repeated_input_flags_handler = _ - def set_unknown_command_handler( - self, _: NonStandardBehaviorHandler[InputCommand], / - ) -> None: + def set_unknown_command_handler(self, _: NonStandardBehaviorHandler[InputCommand], /) -> None: """ Public. Sets the handler for unknown commands when entering a command :param _: handler for unknown commands when entering a command @@ -137,9 +130,7 @@ class BaseApp: """ self._empty_input_command_handler = _ - def set_exit_command_handler( - self, _: NonStandardBehaviorHandler[Response], / - ) -> None: + def set_exit_command_handler(self, _: NonStandardBehaviorHandler[Response], /) -> None: """ Public. Sets the handler for exit command when entering a command :param _: handler for exit command when entering a command @@ -175,11 +166,7 @@ class BaseApp: clear_text = re.sub(r"\u001b\[[0-9;]*m", "", text) max_length_line = max([len(line) for line in clear_text.split("\n")]) max_length_line = ( - max_length_line - if 10 <= max_length_line <= 80 - else 80 - if max_length_line > 80 - else 10 + max_length_line if 10 <= max_length_line <= 80 else 80 if max_length_line > 80 else 10 ) self._print_func( @@ -196,15 +183,11 @@ class BaseApp: elif isinstance(self._dividing_line, StaticDividingLine): # pyright: ignore[reportUnnecessaryIsInstance] self._print_func( - self._dividing_line.get_full_static_line( - is_override=self._override_system_messages - ) + self._dividing_line.get_full_static_line(is_override=self._override_system_messages) ) print(text.strip("\n")) self._print_func( - self._dividing_line.get_full_static_line( - is_override=self._override_system_messages - ) + self._dividing_line.get_full_static_line(is_override=self._override_system_messages) ) else: @@ -238,14 +221,10 @@ class BaseApp: """ input_command_trigger = command.trigger if self._ignore_command_register: - if input_command_trigger.lower() in list( - self._current_matching_triggers_with_routers.keys() - ): + if input_command_trigger.lower() in list(self._current_matching_triggers_with_routers.keys()): return False else: - if input_command_trigger in list( - self._current_matching_triggers_with_routers.keys() - ): + if input_command_trigger in list(self._current_matching_triggers_with_routers.keys()): return False return True @@ -303,9 +282,7 @@ class BaseApp: :return: None """ self._prompt = f"[italic dim bold]{self._prompt}" - self._initial_message = ( - "\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n" - ) + self._initial_message = "\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n" self._farewell_message = ( "[bold red]\n\n" + str(text2art(self._farewell_message, font="chanky")) # pyright: ignore[reportUnknownArgumentType] @@ -323,20 +300,14 @@ class BaseApp: self._repeated_input_flags_handler = lambda raw_command: self._print_func( f"[red bold]Repeated input flags: {escape(raw_command)}" ) - self._empty_input_command_handler = lambda: self._print_func( - "[red bold]Empty input command" - ) + self._empty_input_command_handler = lambda: self._print_func("[red bold]Empty input command") def unknown_command_handler(command: InputCommand) -> None: cmd_trg: str = command.trigger mst_sim_cmd: str | None = self._most_similar_command(cmd_trg) - first_part_of_text = ( - f"[red]Unknown command:[/red] [blue]{escape(cmd_trg)}[/blue]" - ) + first_part_of_text = f"[red]Unknown command:[/red] [blue]{escape(cmd_trg)}[/blue]" second_part_of_text = ( - ("[red], most similar:[/red] " + ("[blue]" + mst_sim_cmd + "[/blue]")) - if mst_sim_cmd - else "" + ("[red], most similar:[/red] " + ("[blue]" + mst_sim_cmd + "[/blue]")) if mst_sim_cmd else "" ) self._print_func(first_part_of_text + second_part_of_text) @@ -356,13 +327,9 @@ class BaseApp: for trigger in combined: self._matching_default_triggers_with_routers[trigger] = router_entity - self._matching_lower_triggers_with_routers[trigger.lower()] = ( - router_entity - ) + self._matching_lower_triggers_with_routers[trigger.lower()] = router_entity - self._autocompleter.initial_setup( - list(self._current_matching_triggers_with_routers.keys()) - ) + self._autocompleter.initial_setup(list(self._current_matching_triggers_with_routers.keys())) seen = {} for item in list(self._current_matching_triggers_with_routers.keys()): @@ -382,7 +349,7 @@ class BaseApp: self._print_func(message) if self._messages_on_startup: print("\n") - if not self._repeat_command_groups_description: + if not self._repeat_command_groups_printing_description: self._print_command_group_description() @@ -405,7 +372,7 @@ class App(BaseApp): 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, @@ -420,7 +387,7 @@ class App(BaseApp): :param system_router_title: system router title :param ignore_command_register: whether to ignore the case of the entered commands :param dividing_line: the entity of the dividing line - :param repeat_command_groups: whether to repeat the available commands and their description + :param repeat_command_groups_printing: whether to repeat the available commands and their description :param override_system_messages: whether to redefine the default formatting of system messages :param autocompleter: the entity of the autocompleter :param print_func: system messages text output function @@ -434,7 +401,7 @@ class App(BaseApp): system_router_title=system_router_title, ignore_command_register=ignore_command_register, dividing_line=dividing_line, - repeat_command_groups=repeat_command_groups, + repeat_command_groups_printing=repeat_command_groups_printing, override_system_messages=override_system_messages, autocompleter=autocompleter, print_func=print_func, @@ -447,15 +414,13 @@ class App(BaseApp): """ self._pre_cycle_setup() while True: - if self._repeat_command_groups_description: + if self._repeat_command_groups_printing_description: self._print_command_group_description() raw_command: str = Console().input(self._prompt) try: - input_command: InputCommand = InputCommand.parse( - raw_command=raw_command - ) + input_command: InputCommand = InputCommand.parse(raw_command=raw_command) except InputCommandException as error: with redirect_stdout(io.StringIO()) as stderr: self._error_handler(error, raw_command) @@ -466,8 +431,7 @@ class App(BaseApp): if self._is_exit_command(input_command): system_router.finds_appropriate_handler(input_command) self._autocompleter.exit_setup( - list(self._current_matching_triggers_with_routers.keys()), - self._ignore_command_register + list(self._current_matching_triggers_with_routers.keys()), self._ignore_command_register ) return @@ -478,24 +442,18 @@ class App(BaseApp): self._print_framed_text(stdout_res) continue - processing_router = self._current_matching_triggers_with_routers[ - input_command.trigger.lower() - ] + processing_router = self._current_matching_triggers_with_routers[input_command.trigger.lower()] if processing_router.disable_redirect_stdout: dividing_line_unit_part: str = self._dividing_line.get_unit_part() self._print_func( - StaticDividingLine( - dividing_line_unit_part - ).get_full_static_line( + StaticDividingLine(dividing_line_unit_part).get_full_static_line( is_override=self._override_system_messages ) ) processing_router.finds_appropriate_handler(input_command) self._print_func( - StaticDividingLine( - dividing_line_unit_part - ).get_full_static_line( + StaticDividingLine(dividing_line_unit_part).get_full_static_line( is_override=self._override_system_messages ) ) diff --git a/src/argenta/app/protocols.py b/src/argenta/app/protocols.py index c66829a..8300be5 100644 --- a/src/argenta/app/protocols.py +++ b/src/argenta/app/protocols.py @@ -2,13 +2,14 @@ __all__ = ["NonStandardBehaviorHandler", "EmptyCommandHandler", "Printer", "Desc from typing import Protocol, TypeVar -T = TypeVar('T', contravariant=True) # noqa: WPS111 +T = TypeVar("T", contravariant=True) # noqa: WPS111 class NonStandardBehaviorHandler(Protocol[T]): def __call__(self, __param: T) -> None: raise NotImplementedError + class EmptyCommandHandler(Protocol): def __call__(self) -> None: raise NotImplementedError diff --git a/src/argenta/command/exceptions.py b/src/argenta/command/exceptions.py index 60477d9..6a83aaf 100644 --- a/src/argenta/command/exceptions.py +++ b/src/argenta/command/exceptions.py @@ -1,8 +1,8 @@ __all__ = [ - "InputCommandException", - "UnprocessedInputFlagException", - "RepeatedInputFlagsException", - "EmptyInputCommandException", + "InputCommandException", + "UnprocessedInputFlagException", + "RepeatedInputFlagsException", + "EmptyInputCommandException", ] from abc import ABC, abstractmethod @@ -15,6 +15,7 @@ class InputCommandException(ABC, Exception): """ Private. Base exception class for all exceptions raised when parse input command """ + @override @abstractmethod def __str__(self) -> str: @@ -25,6 +26,7 @@ class UnprocessedInputFlagException(InputCommandException): """ Private. Raised when an unprocessed input flag is detected """ + @override def __str__(self) -> str: return "Unprocessed Input Flags" @@ -42,16 +44,14 @@ class RepeatedInputFlagsException(InputCommandException): @override def __str__(self) -> str: string_entity: str = self.flag.string_entity - return ( - "Repeated Input Flags\n" - f"Duplicate flag was detected in the input: '{string_entity}'" - ) + return f"Repeated Input Flags\nDuplicate flag was detected in the input: '{string_entity}'" class EmptyInputCommandException(InputCommandException): """ Private. Raised when an empty input command is detected """ + @override def __str__(self) -> str: return "Input Command is empty" diff --git a/src/argenta/command/flag/defaults.py b/src/argenta/command/flag/defaults.py index 5123741..1fb7a97 100644 --- a/src/argenta/command/flag/defaults.py +++ b/src/argenta/command/flag/defaults.py @@ -9,23 +9,21 @@ DEFAULT_PREFIX: Literal["-", "--", "---"] = "-" class PredefinedFlags: - HELP = Flag(name="help", possible_values=PossibleValues.NEITHER) - SHORT_HELP = Flag(name="H", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER) + HELP = Flag(name="help", possible_values=PossibleValues.NEITHER) + SHORT_HELP = Flag(name="H", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER) - INFO = Flag(name="info", possible_values=PossibleValues.NEITHER) # noqa: WPS110 - SHORT_INFO = Flag(name="I", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER) + INFO = Flag(name="info", possible_values=PossibleValues.NEITHER) # noqa: WPS110 + SHORT_INFO = Flag(name="I", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER) - ALL = Flag(name="all", possible_values=PossibleValues.NEITHER) - SHORT_ALL = Flag(name="A", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER) + ALL = Flag(name="all", possible_values=PossibleValues.NEITHER) + SHORT_ALL = Flag(name="A", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER) - HOST = Flag( - name="host", possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") - ) - SHORT_HOST = Flag( - name="H", - prefix=DEFAULT_PREFIX, - possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"), - ) + HOST = Flag(name="host", possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")) + SHORT_HOST = Flag( + name="H", + prefix=DEFAULT_PREFIX, + possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"), + ) - PORT = Flag(name="port", possible_values=re.compile(r"^\d{1,5}$")) - SHORT_PORT = Flag(name="P", prefix=DEFAULT_PREFIX, possible_values=re.compile(r"^\d{1,5}$")) + PORT = Flag(name="port", possible_values=re.compile(r"^\d{1,5}$")) + SHORT_PORT = Flag(name="P", prefix=DEFAULT_PREFIX, possible_values=re.compile(r"^\d{1,5}$")) diff --git a/src/argenta/command/flag/models.py b/src/argenta/command/flag/models.py index e9d90d4..db99fb8 100644 --- a/src/argenta/command/flag/models.py +++ b/src/argenta/command/flag/models.py @@ -6,19 +6,21 @@ from typing import Literal, override class PossibleValues(Enum): - NEITHER = 'NEITHER' - ALL = 'ALL' + NEITHER = "NEITHER" + ALL = "ALL" class ValidationStatus(Enum): - VALID = 'VALID' - INVALID = 'INVALID' - UNDEFINED = 'UNDEFINED' + VALID = "VALID" + INVALID = "INVALID" + UNDEFINED = "UNDEFINED" class Flag: def __init__( - self, name: str, *, + self, + name: str, + *, prefix: Literal["-", "--", "---"] = "--", possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL, ) -> None: @@ -65,7 +67,7 @@ class Flag: @override def __repr__(self) -> str: - return f'Flag' + return f"Flag" @override def __eq__(self, other: object) -> bool: @@ -77,10 +79,12 @@ class Flag: class InputFlag: def __init__( - self, name: str, *, - prefix: Literal['-', '--', '---'] = '--', + self, + name: str, + *, + prefix: Literal["-", "--", "---"] = "--", input_value: str | None, - status: ValidationStatus | None + status: ValidationStatus | None, ): """ Public. The entity of the flag of the entered command @@ -90,7 +94,7 @@ class InputFlag: :return: None """ self.name: str = name - self.prefix: Literal['-', '--', '---'] = prefix + self.prefix: Literal["-", "--", "---"] = prefix self.input_value: str | None = input_value self.status: ValidationStatus | None = status @@ -105,17 +109,15 @@ class InputFlag: @override def __str__(self) -> str: - return f'{self.string_entity} {self.input_value}' + return f"{self.string_entity} {self.input_value}" @override def __repr__(self) -> str: - return f'InputFlag' + return f"InputFlag" @override def __eq__(self, other: object) -> bool: if isinstance(other, InputFlag): - return ( - self.name == other.name - ) + return self.name == other.name else: raise NotImplementedError diff --git a/src/argenta/command/models.py b/src/argenta/command/models.py index 462192d..f19e085 100644 --- a/src/argenta/command/models.py +++ b/src/argenta/command/models.py @@ -1,13 +1,12 @@ -__all__ = [ - "Command", - "InputCommand" -] +__all__ = ["Command", "InputCommand"] from typing import Literal, Never, Self, cast -from argenta.command.exceptions import (EmptyInputCommandException, - RepeatedInputFlagsException, - UnprocessedInputFlagException) +from argenta.command.exceptions import ( + EmptyInputCommandException, + RepeatedInputFlagsException, + UnprocessedInputFlagException, +) from argenta.command.flag.flags.models import Flags, InputFlags from argenta.command.flag.models import Flag, InputFlag, ValidationStatus @@ -23,7 +22,8 @@ DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags() class Command: def __init__( self, - trigger: str, *, + trigger: str, + *, description: str | None = None, flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS, aliases: list[str] | None = None, @@ -40,9 +40,7 @@ class Command: self.description: str = description if description else "Command without description" self.aliases: list[str] = aliases if aliases else [] - def validate_input_flag( - self, flag: InputFlag - ) -> ValidationStatus: + def validate_input_flag(self, flag: InputFlag) -> ValidationStatus: """ Private. Validates the input flag :param flag: input flag for validation @@ -60,8 +58,7 @@ class Command: class InputCommand: - def __init__(self, trigger: str, *, - input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS): + def __init__(self, trigger: str, *, input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS): """ Private. The model of the input command, after parsing :param trigger:the trigger of the command @@ -69,7 +66,9 @@ class InputCommand: :return: None """ self.trigger: str = trigger - self.input_flags: InputFlags = input_flags if isinstance(input_flags, InputFlags) else InputFlags([input_flags]) + self.input_flags: InputFlags = ( + input_flags if isinstance(input_flags, InputFlags) else InputFlags([input_flags]) + ) @classmethod def parse(cls, raw_command: str) -> Self: @@ -108,13 +107,13 @@ class CommandParser: continue input_flag = InputFlag( - name=crnt_flg_name[crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1:], + name=crnt_flg_name[crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1 :], prefix=cast( Literal["-", "--", "---"], - crnt_flg_name[:crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1], + crnt_flg_name[: crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1], ), input_value=crnt_flg_val, - status=None + status=None, ) if input_flag in self._parsed_input_flags: @@ -125,8 +124,7 @@ class CommandParser: return (self._parsed_input_flags, crnt_flg_name, crnt_flg_val) - def _is_next_token_value(self, current_index: int, - _tokens: list[str] | list[Never]) -> bool: + def _is_next_token_value(self, current_index: int, _tokens: list[str] | list[Never]) -> bool: next_index = current_index + 1 if next_index >= len(_tokens): return False @@ -134,17 +132,16 @@ class CommandParser: next_token = _tokens[next_index] return not next_token.startswith(MIN_FLAG_PREFIX) + def _parse_single_token( - token: str, - crnt_flag_name: str | None, - crnt_flag_val: str | None + token: str, crnt_flag_name: str | None, crnt_flag_val: str | None ) -> tuple[str | None, str | None]: if not token.startswith(MIN_FLAG_PREFIX): if not crnt_flag_name or crnt_flag_val: raise UnprocessedInputFlagException return crnt_flag_name, token - prefix = token[:token.rfind(MIN_FLAG_PREFIX)] + prefix = token[: token.rfind(MIN_FLAG_PREFIX)] if len(token) < 2 or len(prefix) > 2: raise UnprocessedInputFlagException diff --git a/src/argenta/di/integration.py b/src/argenta/di/integration.py index da26b6c..4c73a3a 100644 --- a/src/argenta/di/integration.py +++ b/src/argenta/di/integration.py @@ -5,8 +5,8 @@ from typing import Any, Callable, TypeVar from dishka import Container, FromDishka from dishka.integrations.base import is_dishka_injected, wrap_injection -from argenta.app import App -from argenta.response import Response +from argenta.app.models import App +from argenta.response.entity import Response T = TypeVar("T") @@ -25,9 +25,7 @@ def setup_dishka(app: App, container: Container, *, auto_inject: bool = False) - Response.patch_by_container(container) -def _get_container_from_response( - args: tuple[Any, ...], kwargs: dict[str, Any] -) -> Container: +def _get_container_from_response(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Container: for arg in args: if isinstance(arg, Response): if hasattr(arg, "_dishka_container"): diff --git a/src/argenta/di/providers.py b/src/argenta/di/providers.py index af6f379..18191de 100644 --- a/src/argenta/di/providers.py +++ b/src/argenta/di/providers.py @@ -1,5 +1,5 @@ __all__ = [ - 'SystemProvider', + "SystemProvider", ] from dishka import Provider, Scope, provide @@ -10,13 +10,9 @@ from argenta.orchestrator.argparser.entity import ArgSpace class SystemProvider(Provider): - def __init__(self, arg_parser: ArgParser): - super().__init__() - self._arg_parser: ArgParser = arg_parser - @provide(scope=Scope.APP) - def get_argspace(self) -> ArgSpace: - return self._arg_parser.parsed_argspace + def get_argspace(self, arg_parser: ArgParser) -> ArgSpace: + return arg_parser.parsed_argspace @provide(scope=Scope.APP) def get_data_bridge(self) -> DataBridge: diff --git a/src/argenta/metrics/__init__.py b/src/argenta/metrics/__init__.py index e97a8ca..9888ab8 100644 --- a/src/argenta/metrics/__init__.py +++ b/src/argenta/metrics/__init__.py @@ -1,2 +1 @@ -from argenta.metrics.main import \ - get_time_of_pre_cycle_setup as get_time_of_pre_cycle_setup +from argenta.metrics.main import get_time_of_pre_cycle_setup as get_time_of_pre_cycle_setup diff --git a/src/argenta/metrics/main.py b/src/argenta/metrics/main.py index b32b526..d87c945 100644 --- a/src/argenta/metrics/main.py +++ b/src/argenta/metrics/main.py @@ -1,5 +1,5 @@ __all__ = [ - 'get_time_of_pre_cycle_setup', + "get_time_of_pre_cycle_setup", ] import io @@ -17,6 +17,6 @@ def get_time_of_pre_cycle_setup(app: App) -> float: """ start = time() with redirect_stdout(io.StringIO()): - app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage] + app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage] end = time() return end - start diff --git a/src/argenta/orchestrator/argparser/__init__.py b/src/argenta/orchestrator/argparser/__init__.py index 906c5dc..6466f13 100644 --- a/src/argenta/orchestrator/argparser/__init__.py +++ b/src/argenta/orchestrator/argparser/__init__.py @@ -1,6 +1,4 @@ -from argenta.orchestrator.argparser.arguments import \ - BooleanArgument as BooleanArgument -from argenta.orchestrator.argparser.arguments import \ - ValueArgument as ValueArgument +from argenta.orchestrator.argparser.arguments import BooleanArgument as BooleanArgument +from argenta.orchestrator.argparser.arguments import ValueArgument as ValueArgument from argenta.orchestrator.argparser.entity import ArgParser as ArgParser from argenta.orchestrator.argparser.entity import ArgSpace as ArgSpace diff --git a/src/argenta/orchestrator/argparser/arguments/__init__.py b/src/argenta/orchestrator/argparser/arguments/__init__.py index b214840..d2058a3 100644 --- a/src/argenta/orchestrator/argparser/arguments/__init__.py +++ b/src/argenta/orchestrator/argparser/arguments/__init__.py @@ -1,6 +1,3 @@ -from argenta.orchestrator.argparser.arguments.models import \ - BooleanArgument as BooleanArgument -from argenta.orchestrator.argparser.arguments.models import \ - InputArgument as InputArgument -from argenta.orchestrator.argparser.arguments.models import \ - ValueArgument as ValueArgument +from argenta.orchestrator.argparser.arguments.models import BooleanArgument as BooleanArgument +from argenta.orchestrator.argparser.arguments.models import InputArgument as InputArgument +from argenta.orchestrator.argparser.arguments.models import ValueArgument as ValueArgument diff --git a/src/argenta/orchestrator/argparser/arguments/models.py b/src/argenta/orchestrator/argparser/arguments/models.py index 421e891..98e9480 100644 --- a/src/argenta/orchestrator/argparser/arguments/models.py +++ b/src/argenta/orchestrator/argparser/arguments/models.py @@ -1,8 +1,4 @@ -__all__ = [ - 'BooleanArgument', - 'ValueArgument', - 'InputArgument' -] +__all__ = ["BooleanArgument", "ValueArgument", "InputArgument"] from typing import Literal @@ -11,10 +7,8 @@ class BaseArgument: """ Private. Base class for all arguments """ - def __init__(self, name: str, *, - help: str, - is_deprecated: bool, - prefix: Literal["-", "--", "---"]): + + def __init__(self, name: str, *, help: str, is_deprecated: bool, prefix: Literal["-", "--", "---"]): """ Public. Boolean argument, does not require a value :param name: name of the argument @@ -33,13 +27,17 @@ class BaseArgument: class ValueArgument(BaseArgument): - def __init__(self, name: str, *, - prefix: Literal["-", "--", "---"] = "--", - help: str = "Help message for the value argument", - possible_values: list[str] | None = None, - default: str | None = None, - is_required: bool = False, - is_deprecated: bool = False): + def __init__( + self, + name: str, + *, + prefix: Literal["-", "--", "---"] = "--", + help: str = "Help message for the value argument", + possible_values: list[str] | None = None, + default: str | None = None, + is_required: bool = False, + is_deprecated: bool = False, + ): """ Public. Value argument, must have the value :param name: name of the argument @@ -58,10 +56,14 @@ class ValueArgument(BaseArgument): class BooleanArgument(BaseArgument): - def __init__(self, name: str, *, - prefix: Literal["-", "--", "---"] = "--", - help: str = "Help message for the boolean argument", - is_deprecated: bool = False): + def __init__( + self, + name: str, + *, + prefix: Literal["-", "--", "---"] = "--", + help: str = "Help message for the boolean argument", + is_deprecated: bool = False, + ): """ Public. Boolean argument, does not require a value :param name: name of the argument @@ -74,9 +76,7 @@ class BooleanArgument(BaseArgument): class InputArgument: - def __init__(self, name: str, - value: str | Literal[True], - founder_class: type[BaseArgument]) -> None: + def __init__(self, name: str, value: str | Literal[True], founder_class: type[BaseArgument]) -> None: self.name: str = name self.value: str | Literal[True] = value self.founder_class: type[BaseArgument] = founder_class diff --git a/src/argenta/orchestrator/argparser/entity.py b/src/argenta/orchestrator/argparser/entity.py index aeab40b..f05b841 100644 --- a/src/argenta/orchestrator/argparser/entity.py +++ b/src/argenta/orchestrator/argparser/entity.py @@ -6,10 +6,12 @@ __all__ = [ from argparse import ArgumentParser, Namespace from typing import Never, Self -from argenta.orchestrator.argparser.arguments.models import (BaseArgument, - BooleanArgument, - InputArgument, - ValueArgument) +from argenta.orchestrator.argparser.arguments.models import ( + BaseArgument, + BooleanArgument, + InputArgument, + ValueArgument, +) class ArgSpace: @@ -17,16 +19,16 @@ class ArgSpace: self.all_arguments = all_arguments @classmethod - def from_namespace(cls, namespace: Namespace, - processed_args: list[ValueArgument | BooleanArgument]) -> Self: - name_type_paired_args: dict[str, type[BaseArgument]] = { - arg.name: type(arg) - for arg in processed_args - } - return cls([InputArgument(name=name, - value=value, - founder_class=name_type_paired_args[name]) - for name, value in vars(namespace).items()]) + def from_namespace( + cls, namespace: Namespace, processed_args: list[ValueArgument | BooleanArgument] + ) -> Self: + name_type_paired_args: dict[str, type[BaseArgument]] = {arg.name: type(arg) for arg in processed_args} + return cls( + [ + InputArgument(name=name, value=value, founder_class=name_type_paired_args[name]) + for name, value in vars(namespace).items() + ] + ) def get_by_name(self, name: str) -> InputArgument | None: for arg in self.all_arguments: @@ -41,7 +43,8 @@ class ArgSpace: class ArgParser: def __init__( self, - processed_args: list[ValueArgument | BooleanArgument], *, + processed_args: list[ValueArgument | BooleanArgument], + *, name: str = "Argenta", description: str = "Argenta available arguments", epilog: str = "github.com/koloideal/Argenta | made by kolo", @@ -57,28 +60,30 @@ class ArgParser: self.description: str = description self.epilog: str = epilog self.processed_args: list[ValueArgument | BooleanArgument] = processed_args - + self.parsed_argspace: ArgSpace = ArgSpace([]) self._core: ArgumentParser = ArgumentParser(prog=name, description=description, epilog=epilog) self._register_args(processed_args) def _parse_args(self) -> None: - self.parsed_argspace = ArgSpace.from_namespace(namespace=self._core.parse_args(), - processed_args=self.processed_args) - + self.parsed_argspace = ArgSpace.from_namespace( + namespace=self._core.parse_args(), processed_args=self.processed_args + ) + def _register_args(self, processed_args: list[ValueArgument | BooleanArgument]) -> None: for arg in processed_args: if isinstance(arg, BooleanArgument): - _ = self._core.add_argument(arg.string_entity, - action=arg.action, - help=arg.help, - deprecated=arg.is_deprecated) + _ = self._core.add_argument( + arg.string_entity, action=arg.action, help=arg.help, deprecated=arg.is_deprecated + ) else: - _ = self._core.add_argument(arg.string_entity, - action=arg.action, - help=arg.help, - default=arg.default, - choices=arg.possible_values, - required=arg.is_required, - deprecated=arg.is_deprecated) + _ = self._core.add_argument( + arg.string_entity, + action=arg.action, + help=arg.help, + default=arg.default, + choices=arg.possible_values, + required=arg.is_required, + deprecated=arg.is_deprecated, + ) diff --git a/src/argenta/orchestrator/entity.py b/src/argenta/orchestrator/entity.py index f6fc7f1..e4646a1 100644 --- a/src/argenta/orchestrator/entity.py +++ b/src/argenta/orchestrator/entity.py @@ -11,9 +11,12 @@ DEFAULT_ARGPARSER: ArgParser = ArgParser(processed_args=[]) class Orchestrator: - def __init__(self, arg_parser: ArgParser = DEFAULT_ARGPARSER, - custom_providers: list[Provider] = [], - auto_inject_handlers: bool = True): + def __init__( + self, + arg_parser: ArgParser = DEFAULT_ARGPARSER, + custom_providers: list[Provider] = [], + auto_inject_handlers: bool = True, + ): """ Public. An orchestrator and configurator that defines the behavior of an integrated system, one level higher than the App :param arg_parser: Cmd argument parser and configurator at startup @@ -31,7 +34,9 @@ class Orchestrator: :param app: a running application :return: None """ - container = make_container(SystemProvider(self._arg_parser), *self._custom_providers) + container = make_container( + SystemProvider(), *self._custom_providers, context={ArgParser: self._arg_parser} + ) setup_dishka(app, container, auto_inject=self._auto_inject_handlers) app.run_polling() diff --git a/src/argenta/response/status.py b/src/argenta/response/status.py index 8fe548f..c736de0 100644 --- a/src/argenta/response/status.py +++ b/src/argenta/response/status.py @@ -10,12 +10,12 @@ class ResponseStatus(Enum): UNDEFINED_AND_INVALID_FLAGS = "UNDEFINED_AND_INVALID_FLAGS" @classmethod - def from_flags(cls, *, has_invalid_value_flags: bool, has_undefined_flags: bool) -> 'ResponseStatus': + def from_flags(cls, *, has_invalid_value_flags: bool, has_undefined_flags: bool) -> "ResponseStatus": key = (has_invalid_value_flags, has_undefined_flags) status_map: dict[tuple[bool, bool], ResponseStatus] = { - (True, True): cls.UNDEFINED_AND_INVALID_FLAGS, - (True, False): cls.INVALID_VALUE_FLAGS, - (False, True): cls.UNDEFINED_FLAGS, + (True, True): cls.UNDEFINED_AND_INVALID_FLAGS, + (True, False): cls.INVALID_VALUE_FLAGS, + (False, True): cls.UNDEFINED_FLAGS, (False, False): cls.ALL_FLAGS_VALID, } return status_map[key] diff --git a/src/argenta/router/command_handler/entity.py b/src/argenta/router/command_handler/entity.py index 62865d2..300c458 100644 --- a/src/argenta/router/command_handler/entity.py +++ b/src/argenta/router/command_handler/entity.py @@ -32,9 +32,7 @@ class CommandHandlers: Private. The model that unites all CommandHandler of the routers :param command_handlers: list of CommandHandlers for register """ - self.command_handlers: list[CommandHandler] = ( - command_handlers if command_handlers else [] - ) + self.command_handlers: list[CommandHandler] = command_handlers if command_handlers else [] def add_handler(self, command_handler: CommandHandler) -> None: """ diff --git a/src/argenta/router/entity.py b/src/argenta/router/entity.py index a18f73a..260808a 100644 --- a/src/argenta/router/entity.py +++ b/src/argenta/router/entity.py @@ -1,7 +1,6 @@ __all__ = ["Router"] -from inspect import (get_annotations, getfullargspec, getsourcefile, - getsourcelines) +from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines from typing import Callable, TypeAlias from rich.console import Console @@ -10,11 +9,12 @@ from argenta.command import Command, InputCommand from argenta.command.flag import ValidationStatus from argenta.command.flag.flags import Flags, InputFlags from argenta.response import Response, ResponseStatus -from argenta.router.command_handler.entity import (CommandHandler, - CommandHandlers) -from argenta.router.exceptions import (RepeatedFlagNameException, - RequiredArgumentNotPassedException, - TriggerContainSpacesException) +from argenta.router.command_handler.entity import CommandHandler, CommandHandlers +from argenta.router.exceptions import ( + RepeatedFlagNameException, + RequiredArgumentNotPassedException, + TriggerContainSpacesException, +) HandlerFunc: TypeAlias = Callable[..., None] @@ -78,9 +78,7 @@ class Router: if input_command_name.lower() in handle_command.aliases: self.process_input_command(input_command_flags, command_handler) - def process_input_command( - self, input_command_flags: InputFlags, command_handler: CommandHandler - ) -> None: + def process_input_command(self, input_command_flags: InputFlags, command_handler: CommandHandler) -> None: """ Private. Processes input command with the appropriate handler :param input_command_flags: input command flags as InputFlags @@ -90,9 +88,7 @@ class Router: handle_command = command_handler.handled_command if handle_command.registered_flags.flags: if input_command_flags.flags: - response: Response = _structuring_input_flags( - handle_command, input_command_flags - ) + response: Response = _structuring_input_flags(handle_command, input_command_flags) command_handler.handling(response) else: response = Response(ResponseStatus.ALL_FLAGS_VALID) @@ -103,9 +99,7 @@ class Router: for input_flag in input_command_flags: input_flag.status = ValidationStatus.UNDEFINED undefined_flags.add_flag(input_flag) - response = Response( - ResponseStatus.UNDEFINED_FLAGS, input_flags=undefined_flags - ) + response = Response(ResponseStatus.UNDEFINED_FLAGS, input_flags=undefined_flags) command_handler.handling(response) else: response = Response(ResponseStatus.ALL_FLAGS_VALID) @@ -142,15 +136,11 @@ class CommandDecorator: def __call__(self, handler_func: Callable[..., None]) -> Callable[..., None]: _validate_func_args(handler_func) - self.router.command_handlers.add_handler( - CommandHandler(handler_func, self.command) - ) + self.router.command_handlers.add_handler(CommandHandler(handler_func, self.command)) return handler_func -def _structuring_input_flags( - handled_command: Command, input_flags: InputFlags -) -> Response: +def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response: """ Private. Validates flags of input command :param handled_command: entity of the handled command diff --git a/translator.py b/translator.py deleted file mode 100644 index e0d73ef..0000000 --- a/translator.py +++ /dev/null @@ -1,71 +0,0 @@ -import polib -import deepl -import getopt -import sys -import re - -DEEPL_API_TOKEN = 'ADD YOUR API KEY HERE!' - -global argv -global opts -global args - -argv = sys.argv[1:] -opts, args = getopt.getopt(argv, "f:l:") - -def translate(text, lang): - # Define a dictionary to hold the mappings of tokens to placeholders - placeholders = {} - - # Use a regular expression to find all the tokens - tokens = re.findall(r'%\((.*?)\)s', text) - - # Replace each token with a unique placeholder - for i, token in enumerate(tokens): - placeholder = f'__PLACEHOLDER_{i}__' - placeholders[placeholder] = f'%({token})s' - text = text.replace(f'%({token})s', placeholder) - - # Perform the translation - translator = deepl.Translator(DEEPL_API_TOKEN) - translated_text = str(translator.translate_text(text, target_lang=lang)) - - # Replace the placeholders back with the original tokens - for placeholder, token in placeholders.items(): - translated_text = translated_text.replace(placeholder, token) - - return translated_text - -def get_filename(): - # read arguments from command line - for opt, arg in opts: - if opt in ['-f']: - filename = arg - if not filename: - print('Please enter the filename of the PO file e.g. /directory/django.po:') - filename = input() - return filename - -def get_target_language(): - # read arguments from command line - for opt, arg in opts: - if opt in ['-l']: - lang = arg - if not lang: - print('Please enter two letter ISO language code e.g. DE:') - lang = input() - return lang - -def process_file(filename, lang): - po = polib.pofile(filename) - for entry in po.untranslated_entries(): - if not entry.msgstr: - print(entry.msgid) - print('translating...') - entry.msgstr = translate(entry.msgid, lang) - print(entry.msgstr) - print('\n') - po.save(filename) - -if __name__ == '__main__': - process_file(get_filename(), get_target_language()) \ No newline at end of file