From df1c13c16b27b7e403e6da01b06ab871a0c45347 Mon Sep 17 00:00:00 2001 From: Muhammad Imtiaz Date: Tue, 26 May 2026 23:00:02 -0400 Subject: [PATCH 1/2] refactor: replace click with typer for command-line interface --- pyproject.toml | 1 - src/robocop/config/parser.py | 4 ++-- src/robocop/formatter/runner.py | 2 +- src/robocop/formatter/utils/misc.py | 4 ++-- src/robocop/run.py | 31 ++++++++++++++++++++++------- src/robocop/runtime/resolver.py | 8 ++++---- tests/config/test_extend_config.py | 6 +++--- tests/formatter/__init__.py | 4 ++-- tests/linter/utils/__init__.py | 6 +++--- tests/test_cli.py | 4 ++-- uv.lock | 2 -- 11 files changed, 43 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 901a169ef..45bdc61c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ maintainers = [ ] requires-python = ">=3.10" dependencies = [ - "click>=8.0.0", "jinja2>=3.1.4", "robotframework>=5.0,<7.5", "typer>=0.12.5", diff --git a/src/robocop/config/parser.py b/src/robocop/config/parser.py index a519f96aa..94e98bf88 100644 --- a/src/robocop/config/parser.py +++ b/src/robocop/config/parser.py @@ -6,8 +6,8 @@ from pathlib import Path from typing import Any -import click import typer +from typer._click.exceptions import FileError try: import tomllib as toml # type: ignore[import-not-found] @@ -141,7 +141,7 @@ def load_toml_file(config_path: Path) -> dict[str, Any]: config: dict[str, Any] = toml.load(tf) return config except (toml.TOMLDecodeError, OSError) as e: - raise click.FileError(filename=str(config_path), hint=f"Error reading configuration file: {e}") from None + raise FileError(filename=str(config_path), hint=f"Error reading configuration file: {e}") from None def merge_dicts(dict1: dict[str, Any], dict2: dict[str, Any]) -> dict[str, Any]: diff --git a/src/robocop/formatter/runner.py b/src/robocop/formatter/runner.py index 1d8fba2b9..18996dd82 100644 --- a/src/robocop/formatter/runner.py +++ b/src/robocop/formatter/runner.py @@ -50,7 +50,7 @@ def run(self) -> int: # if str(source) == "-": # stdin = True # if self.config.verbose: - # click.echo("Loading file from stdin") + # typer.echo("Loading file from stdin") # source = self.load_from_stdin() if self.config.verbose: print(f"Formatting {source_file.path} file") diff --git a/src/robocop/formatter/utils/misc.py b/src/robocop/formatter/utils/misc.py index f0612c1ef..bd30ec766 100644 --- a/src/robocop/formatter/utils/misc.py +++ b/src/robocop/formatter/utils/misc.py @@ -5,7 +5,7 @@ import re from typing import TYPE_CHECKING -import click +import typer from rich.markup import escape from robot.api.parsing import Comment, End, If, IfHeader, ModelVisitor, Token from robot.parsing.model import Statement @@ -19,7 +19,7 @@ def validate_regex(value: str | None) -> re.Pattern[str] | None: try: return re.compile(value) if value is not None else None except re.error: - raise click.BadParameter("Not a valid regular expression") from None + raise typer.BadParameter("Not a valid regular expression") from None def decorate_diff_with_color(contents: list[str]) -> list[str]: diff --git a/src/robocop/run.py b/src/robocop/run.py index 7c92a3a75..dd75a423f 100644 --- a/src/robocop/run.py +++ b/src/robocop/run.py @@ -1,12 +1,11 @@ import textwrap +from importlib import metadata from pathlib import Path from typing import Annotated, Any -import click import typer from rich.console import Console -from robocop import __version__ from robocop.config import defaults, manager, parser, schema from robocop.formatter.runner import RobocopFormatter from robocop.linter import rules_list @@ -20,11 +19,7 @@ class CliWithVersion(typer.core.TyperGroup): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - click.version_option(version=__version__)(self) - - def list_commands(self, ctx: click.Context) -> list[str]: # noqa: ARG002 + def list_commands(self, ctx: Any) -> list[str]: # noqa: ARG002 """Return the list of commands in the set order.""" commands = ["check", "check-project", "format", "list", "docs"] for command in self.commands: @@ -45,6 +40,28 @@ def list_commands(self, ctx: click.Context) -> list[str]: # noqa: ARG002 app.add_typer(list_app, name="list") +def version_callback(value: bool | None) -> None: + if not value: + return + typer.echo(f"robocop, version {metadata.version('robotframework-robocop')}") + raise typer.Exit + + +@app.callback() +def main_callback( + version: Annotated[ + bool | None, + typer.Option( + "--version", + help="Show the version and exit.", + callback=version_callback, + is_eager=True, + ), + ] = None, +) -> None: + """Run Robocop.""" + + config_option = Annotated[ Path | None, typer.Option( diff --git a/src/robocop/runtime/resolver.py b/src/robocop/runtime/resolver.py index 3ad9ed781..24510f1f1 100644 --- a/src/robocop/runtime/resolver.py +++ b/src/robocop/runtime/resolver.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import TYPE_CHECKING, NamedTuple -import click +import typer try: from robot.api import Languages # RF 6.0 @@ -164,7 +164,7 @@ def check_unmatched_filters(self) -> None: ] if errors: - click.echo("\n".join(errors), err=True) + typer.echo("\n".join(errors), err=True) def is_checker(checker_class_def: tuple[str, type]) -> bool: @@ -189,14 +189,14 @@ def can_run_in_robot_version(formatter: Formatter, overwritten: bool, target_ver if overwritten: # --select FormatterDisabledInVersion or --configure FormatterDisabledInVersion.enabled=True if target_version == ROBOT_VERSION.major: - click.echo( + typer.echo( f"{formatter.__class__.__name__} formatter requires Robot Framework {min_version}.* " f"version but you have {ROBOT_VERSION} installed. " f"Upgrade installed Robot Framework if you want to use this formatter.", err=True, ) else: - click.echo( + typer.echo( f"{formatter.__class__.__name__} formatter requires Robot Framework {min_version}.* " f"version but you set --target-version rf{target_version}. " f"Set --target-version to {min_version} or do not forcefully enable this formatter " diff --git a/tests/config/test_extend_config.py b/tests/config/test_extend_config.py index 2bc4171fe..114b42873 100644 --- a/tests/config/test_extend_config.py +++ b/tests/config/test_extend_config.py @@ -1,9 +1,9 @@ import textwrap from pathlib import Path -import click import pytest import typer +from typer._click.exceptions import FileError from robocop.config.parser import read_toml_config from robocop.config.schema import RawConfig @@ -115,7 +115,7 @@ def test_invalid_toml_file(self): # Arrange config_path = DATA_DIR / "extends" / "invalid.toml" # Act - with pytest.raises(click.FileError) as exc_info: + with pytest.raises(FileError) as exc_info: read_toml_config(config_path) # Assert assert ( @@ -199,7 +199,7 @@ def test_extend_does_not_exist(self, tmp_path): """, ) # Act - with pytest.raises(click.FileError) as exc_info: + with pytest.raises(FileError) as exc_info: read_toml_config(config_path) # Assert assert "Error reading configuration file: [Errno 2] No such file or directory:" in exc_info.value.message diff --git a/tests/formatter/__init__.py b/tests/formatter/__init__.py index c389a62c7..e82312c49 100644 --- a/tests/formatter/__init__.py +++ b/tests/formatter/__init__.py @@ -4,8 +4,8 @@ from difflib import unified_diff from pathlib import Path -import click import pytest +import typer from packaging import version from packaging.specifiers import SpecifierSet from rich.console import Console @@ -91,7 +91,7 @@ def run_tidy( source_path = self.FORMATTERS_DIR / self.FORMATTER_NAME / "source" else: source_path = self.FORMATTERS_DIR / self.FORMATTER_NAME / "source" / source - with pytest.raises(click.exceptions.Exit) as exc_info: + with pytest.raises(typer.Exit) as exc_info: format_files( sources=[source_path], select=select, diff --git a/tests/linter/utils/__init__.py b/tests/linter/utils/__init__.py index d7699e942..b41dd5a29 100644 --- a/tests/linter/utils/__init__.py +++ b/tests/linter/utils/__init__.py @@ -12,8 +12,8 @@ from typing import TYPE_CHECKING from unittest import mock -import click.exceptions import pytest +import typer from rich.console import Console from robocop.run import check_files, check_project @@ -132,7 +132,7 @@ def check_rule( configure.append(f"print_issues.output_format={output_format}") with isolated_output() as output, working_directory(test_data): try: - with pytest.raises(click.exceptions.Exit) as exc_info: + with pytest.raises(typer.Exit) as exc_info: test_fn( sources=paths, select=select, @@ -202,7 +202,7 @@ def check_rule_fix( with isolated_output() as output, working_directory(test_data): try: - with pytest.raises(click.exceptions.Exit): + with pytest.raises(typer.Exit): check_files( sources=paths, select=select, diff --git a/tests/test_cli.py b/tests/test_cli.py index c47655242..fe69f8281 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,12 +1,12 @@ """Test CLI commands / options common for linter and formatter.""" +from importlib import metadata from pathlib import Path from unittest.mock import patch import pytest from typer.testing import CliRunner -from robocop import __version__ from robocop.run import app from robocop.version_handling import ROBOT_VERSION from tests import working_directory @@ -15,7 +15,7 @@ def test_version(): runner = CliRunner() result = runner.invoke(app, ["--version"]) - assert result.stdout == f"robocop, version {__version__}\n" + assert result.stdout == f"robocop, version {metadata.version('robotframework-robocop')}\n" def test_print_docs_rule(): diff --git a/uv.lock b/uv.lock index dc3076eee..cab62e396 100644 --- a/uv.lock +++ b/uv.lock @@ -2062,7 +2062,6 @@ wheels = [ name = "robotframework-robocop" source = { editable = "." } dependencies = [ - { name = "click" }, { name = "jinja2" }, { name = "msgpack" }, { name = "pathspec" }, @@ -2111,7 +2110,6 @@ mcp = [ [package.metadata] requires-dist = [ - { name = "click", specifier = ">=8.0.0" }, { name = "fastmcp", marker = "extra == 'mcp'", specifier = ">=3.0.0" }, { name = "jinja2", specifier = ">=3.1.4" }, { name = "msgpack", specifier = ">=1.0.0" }, From f015878298faa8855f048050b9db1aaf724914a2 Mon Sep 17 00:00:00 2001 From: Muhammad Imtiaz Date: Tue, 26 May 2026 23:10:55 -0400 Subject: [PATCH 2/2] fix: update typer dependency to version 0.26.0 and add annotated-doc package --- pyproject.toml | 2 +- uv.lock | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 45bdc61c4..7f1cbc94e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ requires-python = ">=3.10" dependencies = [ "jinja2>=3.1.4", "robotframework>=5.0,<7.5", - "typer>=0.12.5", + "typer>=0.26.0", "rich>=10.11.0", "tomli==2.2.1; python_version < '3.11'", "tomli-w>=1.0", diff --git a/uv.lock b/uv.lock index cab62e396..a738160e7 100644 --- a/uv.lock +++ b/uv.lock @@ -36,6 +36,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -2120,7 +2129,7 @@ requires-dist = [ { name = "robotframework", specifier = ">=5.0,<7.5" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = "==2.2.1" }, { name = "tomli-w", specifier = ">=1.0" }, - { name = "typer", specifier = ">=0.12.5" }, + { name = "typer", specifier = ">=0.26.0" }, { name = "typing-extensions", specifier = ">=4.15.0" }, ] provides-extras = ["mcp"] @@ -2427,17 +2436,17 @@ wheels = [ [[package]] name = "typer" -version = "0.20.1" +version = "0.26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, { name = "shellingham" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/c1/933d30fd7a123ed981e2a1eedafceab63cb379db0402e438a13bc51bbb15/typer-0.20.1.tar.gz", hash = "sha256:68585eb1b01203689c4199bc440d6be616f0851e9f0eb41e4a778845c5a0fd5b", size = 105968, upload-time = "2025-12-19T16:48:56.302Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/26/8e9a4f2c98caefcf4ac25788d48939516a9dd4265fcf9bdd578a2a1b55dd/typer-0.26.1.tar.gz", hash = "sha256:537d27ae686d82967f6383382a952cb32ba4768898541effccb69ca75bbd5d23", size = 198884, upload-time = "2026-05-26T17:49:07.912Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/52/1f2df7e7d1be3d65ddc2936d820d4a3d9777a54f4204f5ca46b8513eff77/typer-0.20.1-py3-none-any.whl", hash = "sha256:4b3bde918a67c8e03d861aa02deca90a95bbac572e71b1b9be56ff49affdb5a8", size = 47381, upload-time = "2025-12-19T16:48:53.679Z" }, + { url = "https://files.pythonhosted.org/packages/be/27/8a22d4833fe8aa0836ce7fa59096ad50d7e93b83be6d5383f11f9a140d54/typer-0.26.1-py3-none-any.whl", hash = "sha256:933e4f0083521f3c57d6a5aedf3b073271b2f95a19761b171b494dd6fdb21ff6", size = 123097, upload-time = "2026-05-26T17:49:09.065Z" }, ] [[package]]