From c07ee92371ff07c05f349e2ddf84430514a8a2b5 Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 7 Feb 2026 01:24:37 +0300 Subject: [PATCH] benchs --- CHANGELOG.md | 20 ++ pyproject.toml | 24 +- src/argenta/app/models.py | 5 +- src/argenta/app/presentation/renderers.py | 3 +- src/argenta/app/presentation/viewers.py | 12 +- tests/unit_tests/test_app.py | 10 +- tests/unit_tests/test_autocompleter.py | 326 +++++++++++++++++++++- tests/unit_tests/test_command.py | 5 + tests/unit_tests/test_flag.py | 43 +++ tests/unit_tests/test_router.py | 11 + tests/unit_tests/test_viewers.py | 12 + uv.lock | 61 ++++ 12 files changed, 512 insertions(+), 20 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..887f187 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ + + +## 1.2.0 — 2026-02-07 + +### Added + +- 100% coverage of the code base with tests +- 100% coverage with typhints +- 100% coverage of public API documentation in two languages - Russian and English +- cli attributes: highlighting valid commands, redesigned input history with auto-completion, interactive autocomplete selection menu for multiple candidates +- a metrics module that allows you to test the performance of various library units +- implementing a dependency injection pattern through an ioc container +- implementation of a context object for transferring data between handlers within a session +- adding a changelog + +### Changed + +- increased performance by several times (there will be real numbers in the next releases) +- reworking the internal API, highlighting different layers and reducing connectivity +- reworking the README and adding a translation for it diff --git a/pyproject.toml b/pyproject.toml index b179e94..1f25c0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "argenta" -version = "1.1.2" +version = "1.2.0" description = "Python library for building modular CLI applications" authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }] -requires-python = ">=3.12" +requires-python = ">=3.12,<3.15" readme = "README.md" license = { text = "MIT" } dependencies = [ @@ -14,6 +14,13 @@ dependencies = [ ] [dependency-groups] +dev = [ + {include-group = "linters"}, + {include-group = "typecheckers"}, + {include-group = "docs"}, + {include-group = "tests"}, + "scriv>=1.8.0", +] linters = [ "isort>=7.0.0", "ruff>=0.12.12", @@ -65,6 +72,19 @@ omit = [ "src/argenta/metrics/*" ] +[tool.scriv] +format = "md" +output_file = "CHANGELOG.md" +fragment_directory = "changelog.d" +categories = [ + "Added", + "Changed", + "Deprecated", + "Removed", + "Fixed", +] +md_header_level = "2" + [tool.mypy] disable_error_code = "import-untyped" diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index f599a5d..017ff1c 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -52,10 +52,7 @@ class BaseApp(BehaviorHandlersSettersMixin): self.registered_routers: RegisteredRouters = RegisteredRouters() self._messages_on_startup: list[str] = [] - if self._override_system_messages: - self._renderer: Renderer = PlainRenderer() - else: - self._renderer: Renderer = RichRenderer() + self._renderer: Renderer = PlainRenderer() if self._override_system_messages else RichRenderer() self._viewer: Viewer = Viewer( printer=self._printer, diff --git a/src/argenta/app/presentation/renderers.py b/src/argenta/app/presentation/renderers.py index 379466d..0df27ef 100644 --- a/src/argenta/app/presentation/renderers.py +++ b/src/argenta/app/presentation/renderers.py @@ -137,7 +137,8 @@ class PlainRenderer(Renderer): def render_text_for_description_message_generator(command: str, description: str) -> str: return f"{command} *=*=* {description}" - def render_messages_on_startup(self, messages: Iterable[str]) -> str: + @staticmethod + def render_messages_on_startup(messages: Iterable[str]) -> str: return "\n" + "\n".join(messages) @staticmethod diff --git a/src/argenta/app/presentation/viewers.py b/src/argenta/app/presentation/viewers.py index 29f6cf5..44eb168 100644 --- a/src/argenta/app/presentation/viewers.py +++ b/src/argenta/app/presentation/viewers.py @@ -77,19 +77,19 @@ class Viewer: if max_length_line > 100 else 10 ) - dividing_line_as_str: str = self._dividing_line.get_full_dynamic_line( + dynamic_dividing_line_as_str: str = self._dividing_line.get_full_dynamic_line( length=max_length_line, is_override=self._override_system_messages ) - self._printer(dividing_line_as_str + "\n") + self._printer(dynamic_dividing_line_as_str + "\n") self._printer(Text.from_ansi(stdout_result.strip("\n")).markup) - self._printer('\n' + dividing_line_as_str) + self._printer('\n' + dynamic_dividing_line_as_str) case (StaticDividingLine() as dividing_line, bool()) | (DynamicDividingLine() as dividing_line, True): - dividing_line_as_str: str = StaticDividingLine(dividing_line.get_unit_part()).get_full_static_line( + static_dividing_line_as_str: str = StaticDividingLine(dividing_line.get_unit_part()).get_full_static_line( is_override=self._override_system_messages ) - self._printer(dividing_line_as_str + '\n') + self._printer(static_dividing_line_as_str + '\n') output_text_generator() - self._printer('\n' + dividing_line_as_str) + self._printer('\n' + static_dividing_line_as_str) case _: raise NotImplementedError(f"Dividing line with type {self._dividing_line} is not implemented") diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py index a419716..3c14e22 100644 --- a/tests/unit_tests/test_app.py +++ b/tests/unit_tests/test_app.py @@ -618,11 +618,17 @@ def test_app_handlers_work_with_multiple_routers() -> None: app.set_unknown_command_handler(custom_handler) - # Both commands should be known assert not app._is_unknown_command(InputCommand('cmd1')) assert not app._is_unknown_command(InputCommand('cmd2')) - # Unknown command should trigger handler assert app._is_unknown_command(InputCommand('unknown')) app._unknown_command_handler(InputCommand('unknown')) assert call_tracker['called'] + + +def test_process_exist_and_valid_command_raises_runtime_error_when_router_not_found() -> None: + app = App() + app._pre_cycle_setup() + + with pytest.raises(RuntimeError, match="Router for 'nonexistent' not found. Panic!"): + app._process_exist_and_valid_command(InputCommand('nonexistent')) diff --git a/tests/unit_tests/test_autocompleter.py b/tests/unit_tests/test_autocompleter.py index b7022a3..c900a2c 100644 --- a/tests/unit_tests/test_autocompleter.py +++ b/tests/unit_tests/test_autocompleter.py @@ -1,4 +1,12 @@ +import os +import sys +import tempfile +from typing import Any, Callable +from unittest.mock import MagicMock, patch + import pytest +from prompt_toolkit import HTML +from prompt_toolkit.completion import CompleteEvent from prompt_toolkit.document import Document from prompt_toolkit.history import InMemoryHistory @@ -75,7 +83,7 @@ def test_history_completer_returns_matching_commands() -> None: completer = HistoryCompleter(history, {"status"}) doc = Document("sta") - completions = list(completer.get_completions(doc, None)) + completions = list(completer.get_completions(doc, CompleteEvent())) completion_texts = [c.text for c in completions] assert "start server" in completion_texts @@ -91,7 +99,7 @@ def test_history_completer_returns_all_when_empty_input() -> None: completer = HistoryCompleter(history, {"status"}) doc = Document("") - completions = list(completer.get_completions(doc, None)) + completions = list(completer.get_completions(doc, CompleteEvent())) completion_texts = [c.text for c in completions] assert len(completion_texts) == 3 @@ -107,7 +115,7 @@ def test_history_completer_returns_empty_when_no_matches() -> None: completer = HistoryCompleter(history, {"stop"}) doc = Document("xyz") - completions = list(completer.get_completions(doc, None)) + completions = list(completer.get_completions(doc, CompleteEvent())) assert len(completions) == 0 @@ -119,7 +127,7 @@ def test_history_completer_deduplicates_commands() -> None: completer = HistoryCompleter(history, {"start"}) doc = Document("sta") - completions = list(completer.get_completions(doc, None)) + completions = list(completer.get_completions(doc, CompleteEvent())) assert len(completions) == 1 @@ -132,7 +140,7 @@ def test_history_completer_sorts_results() -> None: completer = HistoryCompleter(history, set()) doc = Document("st") - completions = list(completer.get_completions(doc, None)) + completions = list(completer.get_completions(doc, CompleteEvent())) completion_texts = [c.text for c in completions] assert completion_texts == ["start", "status", "stop"] @@ -160,3 +168,311 @@ def test_find_common_prefix_with_empty_list() -> None: matches: list[str] = [] prefix = HistoryCompleter._find_common_prefix(matches) assert prefix == "" + + +def test_command_lexer_handles_out_of_range_lineno() -> None: + lexer = CommandLexer({"start", "stop"}) + doc = Document("start") + get_line_tokens = lexer.lex_document(doc) + tokens = get_line_tokens(1) + assert tokens == [] + + +def test_history_completer_returns_early_when_no_matches() -> None: + history = InMemoryHistory() + completer = HistoryCompleter(history, {"start", "stop"}) + doc = Document("xyz") + + result = completer.get_completions(doc, CompleteEvent()) + completions = list(result) + assert completions == [] + + +def test_autocompleter_initial_setup_with_commands() -> None: + completer = AutoCompleter() + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession') as mock_session: + completer.initial_setup({"start", "stop", "status"}) + + assert completer._session is not None + assert completer._fallback_mode is False + mock_session.assert_called_once() + + +def test_autocompleter_initial_setup_with_history_file() -> None: + with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f: + history_file = f.name + + try: + completer = AutoCompleter(history_filename=history_file) + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession'), \ + patch('argenta.app.autocompleter.entity.ThreadedHistory') as mock_threaded_history: + completer.initial_setup({"start", "stop"}) + + assert completer._session is not None + assert completer._fallback_mode is False + mock_threaded_history.assert_called_once() + finally: + if os.path.exists(history_file): + os.unlink(history_file) + + +def test_autocompleter_initial_setup_without_history_file() -> None: + completer = AutoCompleter(history_filename=None) + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession'), \ + patch('argenta.app.autocompleter.entity.InMemoryHistory') as mock_in_memory: + completer.initial_setup({"start", "stop"}) + + assert completer._session is not None + assert completer._fallback_mode is False + mock_in_memory.assert_called_once() + + +def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None: + completer = AutoCompleter(autocomplete_button="c-space") + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession'): + completer.initial_setup({"start", "stop"}) + + assert completer._session is not None + assert completer.autocomplete_button == "c-space" + + +def test_autocompleter_initial_setup_without_auto_suggestions() -> None: + completer = AutoCompleter(auto_suggestions=False) + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession') as mock_session: + completer.initial_setup({"start", "stop"}) + + assert completer._session is not None + call_kwargs = mock_session.call_args[1] + assert call_kwargs['auto_suggest'] is None + + +def test_autocompleter_initial_setup_without_command_highlighting() -> None: + completer = AutoCompleter(command_highlighting=False) + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession') as mock_session: + completer.initial_setup({"start", "stop"}) + + assert completer._session is not None + call_kwargs = mock_session.call_args[1] + assert call_kwargs['style'] is None + assert call_kwargs['lexer'] is None + + +def test_autocompleter_key_binding_handler_with_complete_state() -> None: + completer = AutoCompleter() + + captured_handler: Callable[[Any], None] | None = None + + def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]: + def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]: + nonlocal captured_handler + captured_handler = func + return func + return decorator + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession'), \ + patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class: + + mock_kb = MagicMock() + mock_kb.add = capture_kb_add + mock_kb_class.return_value = mock_kb + + completer.initial_setup({"start", "stop"}) + + assert captured_handler is not None + + mock_event = MagicMock() + mock_buff = MagicMock() + mock_buff.complete_state = True + mock_event.app.current_buffer = mock_buff + + captured_handler(mock_event) + + mock_buff.complete_next.assert_called_once() + + +def test_autocompleter_key_binding_handler_no_completions() -> None: + completer = AutoCompleter() + + captured_handler: Callable[[Any], None] | None = None + + def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]: + def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]: + nonlocal captured_handler + captured_handler = func + return func + return decorator + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession'), \ + patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class: + + mock_kb = MagicMock() + mock_kb.add = capture_kb_add + mock_kb_class.return_value = mock_kb + + completer.initial_setup({"start", "stop"}) + + mock_event = MagicMock() + mock_buff = MagicMock() + mock_buff.complete_state = False + mock_completer = MagicMock() + mock_completer.get_completions.return_value = iter([]) + mock_buff.completer = mock_completer + mock_event.app.current_buffer = mock_buff + + assert captured_handler is not None + captured_handler(mock_event) + + mock_buff.start_completion.assert_not_called() + mock_buff.apply_completion.assert_not_called() + + +def test_autocompleter_key_binding_handler_single_completion() -> None: + completer = AutoCompleter() + + captured_handler: Callable[[Any], None] | None = None + + def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]: + def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]: + nonlocal captured_handler + captured_handler = func + return func + return decorator + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession'), \ + patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class: + + mock_kb = MagicMock() + mock_kb.add = capture_kb_add + mock_kb_class.return_value = mock_kb + + completer.initial_setup({"start", "stop"}) + + mock_event = MagicMock() + mock_buff = MagicMock() + mock_buff.complete_state = False + mock_completion = MagicMock() + mock_completer = MagicMock() + mock_completer.get_completions.return_value = iter([mock_completion]) + mock_buff.completer = mock_completer + mock_event.app.current_buffer = mock_buff + + assert captured_handler is not None + captured_handler(mock_event) + + mock_buff.apply_completion.assert_called_once_with(mock_completion) + mock_buff.start_completion.assert_not_called() + + +def test_autocompleter_key_binding_handler_multiple_completions() -> None: + completer = AutoCompleter() + + captured_handler: Callable[[Any], None] | None = None + + def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]: + def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]: + nonlocal captured_handler + captured_handler = func + return func + return decorator + + with patch.object(sys.stdin, 'isatty', return_value=True), \ + patch('argenta.app.autocompleter.entity.PromptSession'), \ + patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class: + + mock_kb = MagicMock() + mock_kb.add = capture_kb_add + mock_kb_class.return_value = mock_kb + + completer.initial_setup({"start", "stop"}) + + mock_event = MagicMock() + mock_buff = MagicMock() + mock_buff.complete_state = False + mock_completion1 = MagicMock() + mock_completion2 = MagicMock() + mock_completer = MagicMock() + mock_completer.get_completions.return_value = iter([mock_completion1, mock_completion2]) + mock_buff.completer = mock_completer + mock_event.app.current_buffer = mock_buff + + assert captured_handler is not None + captured_handler(mock_event) + + mock_buff.start_completion.assert_called_once_with(select_first=False) + mock_buff.apply_completion.assert_not_called() + + +def test_autocompleter_prompt_in_fallback_mode_with_string() -> None: + completer = AutoCompleter() + + with patch.object(sys.stdin, 'isatty', return_value=False): + completer.initial_setup({"start", "stop"}) + + assert completer._fallback_mode is True + + with patch('builtins.input', return_value='test input'): + result = completer.prompt(">>> ") + + assert result == 'test input' + + +def test_autocompleter_prompt_in_fallback_mode_with_html() -> None: + completer = AutoCompleter() + + with patch.object(sys.stdin, 'isatty', return_value=False): + completer.initial_setup({"start", "stop"}) + + assert completer._fallback_mode is True + + with patch('builtins.input', return_value='test input'): + result = completer.prompt(HTML(">>> ")) + + assert result == 'test input' + + +def test_autocompleter_prompt_with_html_in_normal_mode() -> None: + completer = AutoCompleter() + + mock_session = MagicMock() + mock_session.prompt.return_value = 'test result' + completer._session = mock_session + completer._fallback_mode = False + + html_prompt = HTML(">>> ") + result = completer.prompt(html_prompt) + + assert result == 'test result' + mock_session.prompt.assert_called_once() + call_args = mock_session.prompt.call_args + assert call_args[0][0] == html_prompt + + +def test_autocompleter_prompt_with_string_in_normal_mode() -> None: + completer = AutoCompleter() + + mock_session = MagicMock() + mock_session.prompt.return_value = 'test result' + completer._session = mock_session + completer._fallback_mode = False + + result = completer.prompt(">>> ") + + assert result == 'test result' + mock_session.prompt.assert_called_once() + call_args = mock_session.prompt.call_args + assert isinstance(call_args[0][0], HTML) diff --git a/tests/unit_tests/test_command.py b/tests/unit_tests/test_command.py index ba661d7..a6a8be0 100644 --- a/tests/unit_tests/test_command.py +++ b/tests/unit_tests/test_command.py @@ -58,6 +58,11 @@ def test_parse_raises_error_for_empty_command() -> None: with pytest.raises(EmptyInputCommandException): InputCommand.parse('') + +def test_parse_raises_error_slash_on_the_end() -> None: + with pytest.raises(UnprocessedInputFlagException): + InputCommand.parse('ssh --host 192.168.0.3\\') + # ============================================================================ # Tests for flag validation - valid flags diff --git a/tests/unit_tests/test_flag.py b/tests/unit_tests/test_flag.py index a398281..120b55d 100644 --- a/tests/unit_tests/test_flag.py +++ b/tests/unit_tests/test_flag.py @@ -164,6 +164,49 @@ def test_input_flags_get_by_name_returns_none_for_missing_flag() -> None: assert input_flags.get_flag_by_name('case') is None +def test_input_flags_get_by_name_with_status_finds_matching_flag() -> None: + from argenta.command.flag import ValidationStatus + + flag1 = InputFlag(name='test', input_value='valid', status=ValidationStatus.VALID) + flag2 = InputFlag(name='other', input_value='invalid', status=ValidationStatus.INVALID) + input_flags = InputFlags([flag1, flag2]) + + result = input_flags.get_flag_by_name('test', with_status=ValidationStatus.VALID) + assert result == flag1 + + +def test_input_flags_get_by_name_with_status_returns_none_when_status_mismatch() -> None: + from argenta.command.flag import ValidationStatus + + flag = InputFlag(name='test', input_value='value', status=ValidationStatus.VALID) + input_flags = InputFlags([flag]) + + result = input_flags.get_flag_by_name('test', with_status=ValidationStatus.INVALID) + assert result is None + + +def test_input_flags_get_by_name_with_status_returns_default_when_not_found() -> None: + from argenta.command.flag import ValidationStatus + + flag = InputFlag(name='test', input_value='value', status=ValidationStatus.VALID) + input_flags = InputFlags([flag]) + + result = input_flags.get_flag_by_name('missing', with_status=ValidationStatus.VALID, default='default_value') + assert result == 'default_value' + + +def test_input_flags_get_by_name_with_status_filters_by_both_name_and_status() -> None: + from argenta.command.flag import ValidationStatus + + flag1 = InputFlag(name='test', input_value='value1', status=ValidationStatus.VALID) + flag2 = InputFlag(name='test', input_value='value2', status=ValidationStatus.INVALID) + flag3 = InputFlag(name='other', input_value='value3', status=ValidationStatus.VALID) + input_flags = InputFlags([flag1, flag2, flag3]) + + result = input_flags.get_flag_by_name('test', with_status=ValidationStatus.INVALID) + assert result == flag2 + + # ============================================================================ # Tests for InputFlags collection - equality and containment # ============================================================================ diff --git a/tests/unit_tests/test_router.py b/tests/unit_tests/test_router.py index 66362f3..0a0a656 100644 --- a/tests/unit_tests/test_router.py +++ b/tests/unit_tests/test_router.py @@ -248,6 +248,17 @@ def test_finds_appropriate_handler_executes_handler_with_flags_by_alias(capsys: assert "Hello World!" in output.out +def test_finds_appropriate_handler_raises_runtime_error_when_handler_not_found() -> None: + router = Router() + + @router.command('hello') + def handler(_res: Response) -> None: + pass + + with pytest.raises(RuntimeError, match="Handler for 'unknown' command not found. Panic!"): + router.finds_appropriate_handler(InputCommand('unknown')) + + # ============================================================================ # Tests for alias and trigger collision detection # ============================================================================ diff --git a/tests/unit_tests/test_viewers.py b/tests/unit_tests/test_viewers.py index 0101650..a519f0d 100644 --- a/tests/unit_tests/test_viewers.py +++ b/tests/unit_tests/test_viewers.py @@ -141,3 +141,15 @@ class TestViewer: mock_output_generator.assert_called_once() assert mock_printer.call_count >= 2 + + def test_view_framed_text_with_unimplemented_dividing_line(self, mock_printer: Mock, mock_output_generator: Mock): + class NotImplementedDividingLine: + pass + + renderer = PlainRenderer() + dividing_line = NotImplementedDividingLine() + viewer = Viewer(mock_printer, renderer, dividing_line, False) + + with pytest.raises(NotImplementedError): + viewer.view_framed_text_from_generator(mock_output_generator, is_stdout_redirected_by_router=True) + diff --git a/uv.lock b/uv.lock index 7ed2bdf..64639d7 100644 --- a/uv.lock +++ b/uv.lock @@ -49,6 +49,22 @@ dependencies = [ ] [package.dev-dependencies] +dev = [ + { name = "esbonio" }, + { name = "isort" }, + { name = "mypy" }, + { name = "pyfakefs" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "ruff" }, + { name = "scriv" }, + { name = "shibuya" }, + { name = "sphinx" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-intl" }, + { name = "wemake-python-styleguide" }, +] docs = [ { name = "esbonio" }, { name = "shibuya" }, @@ -85,6 +101,22 @@ requires-dist = [ ] [package.metadata.requires-dev] +dev = [ + { name = "esbonio", specifier = ">=1.0.0" }, + { name = "isort", specifier = ">=7.0.0" }, + { name = "mypy", specifier = ">=1.14.1" }, + { name = "pyfakefs", specifier = ">=5.5.0" }, + { name = "pytest", specifier = ">=8.3.2" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.12.12" }, + { name = "scriv", specifier = ">=1.8.0" }, + { name = "shibuya", specifier = ">=2025.9.25" }, + { name = "sphinx", specifier = ">=8.2.3" }, + { name = "sphinx-autobuild", specifier = ">=2025.8.25" }, + { name = "sphinx-intl", specifier = ">=2.3.2" }, + { name = "wemake-python-styleguide", specifier = ">=0.17.0" }, +] docs = [ { name = "esbonio", specifier = ">=1.0.0" }, { name = "shibuya", specifier = ">=2025.9.25" }, @@ -228,6 +260,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, ] +[[package]] +name = "click-log" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/32/228be4f971e4bd556c33d52a22682bfe318ffe57a1ddb7a546f347a90260/click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975", size = 9985, upload-time = "2022-03-13T11:10:15.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756", size = 4273, upload-time = "2022-03-13T11:10:17.594Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -1208,6 +1252,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142, upload-time = "2025-10-07T18:21:53.577Z" }, ] +[[package]] +name = "scriv" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "click-log" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/9a/2ef2209e0672b264a2f2574dc88ea3cd9cfc9adfecbfd3165a900980ec8c/scriv-1.8.0.tar.gz", hash = "sha256:7b1a105dd411ac541998250fc8594742419f94cee984ca1257c5ebf5af21918b", size = 98160, upload-time = "2025-12-30T00:01:10.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" }, +] + [[package]] name = "shibuya" version = "2025.9.25"