From 2ced3fffb9733a3ce21f0e4beb8261616a353f74 Mon Sep 17 00:00:00 2001 From: mdevolde Date: Mon, 22 Jun 2026 23:44:46 +0200 Subject: [PATCH] feat(cli): add categories enable/disable flags --- CHANGELOG.md | 1 + docs/source/references/cli.rst | 13 ++++- src/language_tool_python/__main__.py | 36 +++++++++++- tests/test_cli.py | 82 +++++++++++++++++++++++++++- 4 files changed, 127 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d467be0..b0c3a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/), ## [Unreleased] ### Added +- Added `-D`/`--disable-categories` and `-E`/`--enable-categories` CLI options to disable or enable LanguageTool rule categories (e.g. `TYPOS`, `GRAMMAR`). `--enabled-only` now also applies to categories specified via `--enable-categories`. - Added `premium_key` property to `language_tool_python.server.LanguageTool` to attach a premium API key for LanguageTool API requests. - Added `premium_username` property to `language_tool_python.server.LanguageTool` to attach a username for premium LanguageTool API requests. - Added `language_tool_python.match.is_check_match` type guard function to verify that a value is a `CheckMatch` (type from `language_tool_python._internals`). diff --git a/docs/source/references/cli.rst b/docs/source/references/cli.rst index f88ac73..ba335c0 100644 --- a/docs/source/references/cli.rst +++ b/docs/source/references/cli.rst @@ -36,8 +36,13 @@ Options - Comma-separated list of rule IDs to disable. * - ``-e, --enable RULES`` - Comma-separated list of rule IDs to enable. + * - ``-D, --disable-categories CATEGORIES`` + - Comma-separated list of category IDs to disable (e.g. ``TYPOS``, ``GRAMMAR``). + * - ``-E, --enable-categories CATEGORIES`` + - Comma-separated list of category IDs to enable. * - ``--enabled-only`` - - Run only the rules listed with ``--enable``, ignoring all others. + - Run only the rules listed with ``--enable`` and the categories listed with + ``--enable-categories``, ignoring all others. * - ``-p, --picky`` - Enable stricter (picky) checking mode. * - ``-a, --apply`` @@ -91,6 +96,12 @@ Examples # Disable specific rules language_tool_python -l en-US -d RULE_ID1,RULE_ID2 input.txt + # Disable an entire category + language_tool_python -l en-US -D TYPOS input.txt + + # Disable multiple categories + language_tool_python -l en-US -D TYPOS,GRAMMAR input.txt + # Run only one specific rule language_tool_python -l en-US --enabled-only -e MORFOLOGIK_RULE_EN_US input.txt diff --git a/src/language_tool_python/__main__.py b/src/language_tool_python/__main__.py index 14b8015..8ee561a 100644 --- a/src/language_tool_python/__main__.py +++ b/src/language_tool_python/__main__.py @@ -85,6 +85,8 @@ class CliArgs(argparse.Namespace): mother_tongue: str | None disable: set[str] enable: set[str] + disable_categories: set[str] + enable_categories: set[str] enabled_only: bool picky: bool apply: bool @@ -137,10 +139,29 @@ def parse_args(argv: Sequence[str] | None = None) -> CliArgs: default=set[str](), help="list of rule IDs to be enabled", ) + parser.add_argument( + "-D", + "--disable-categories", + metavar="CATEGORIES", + type=get_rules, + action=RulesAction, + default=set[str](), + help="list of category IDs to be disabled", + ) + parser.add_argument( + "-E", + "--enable-categories", + metavar="CATEGORIES", + type=get_rules, + action=RulesAction, + default=set[str](), + help="list of category IDs to be enabled", + ) parser.add_argument( "--enabled-only", action="store_true", - help="disable all rules except those specified in --enable", + help="disable all rules and categories except those specified in " + "--enable or --enable-categories", ) parser.add_argument( "-p", @@ -185,8 +206,11 @@ def parse_args(argv: Sequence[str] | None = None) -> CliArgs: if args.disable: parser.error("--enabled-only cannot be used with --disable") - if not args.enable: - parser.error("--enabled-only requires --enable") + if args.disable_categories: + parser.error("--enabled-only cannot be used with --disable-categories") + + if not args.enable and not args.enable_categories: + parser.error("--enabled-only requires --enable or --enable-categories") return args @@ -230,6 +254,10 @@ def __call__( cli_args.disable.update(rule_values) elif self.dest == "enable": cli_args.enable.update(rule_values) + elif self.dest == "disable_categories": + cli_args.disable_categories.update(rule_values) + elif self.dest == "enable_categories": + cli_args.enable_categories.update(rule_values) else: err = f"unexpected rules destination: {self.dest}" raise ValueError(err) @@ -362,6 +390,8 @@ def process_file( lang_tool.disabled_rules.update(args.disable) lang_tool.enabled_rules.update(args.enable) + lang_tool.disabled_categories.update(args.disable_categories) + lang_tool.enabled_categories.update(args.enable_categories) lang_tool.enabled_rules_only = args.enabled_only if args.picky: diff --git a/tests/test_cli.py b/tests/test_cli.py index 672876b..18da908 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -7,7 +7,7 @@ import pytest import language_tool_python -from language_tool_python.__main__ import main +from language_tool_python.__main__ import main, parse_args @pytest.mark.parametrize( @@ -38,6 +38,31 @@ True, ), (["-l", "en-US", "--ignore-lines=^#", "-"], '# These are "dumb".\n', True), + ( + ["-l", "en-US", "-D", "TYPOS", "-"], + "This is noot okay.\n", + True, + ), + ( + ["-l", "en-US", "--disable-categories=TYPOS", "-"], + "This is noot okay.\n", + True, + ), + ( + ["-l", "en-US", "-E", "TYPOS", "-"], + "This is okay.\n", + True, + ), + ( + ["-l", "en-US", "--enabled-only", "-E", "TYPOS", "-"], + "This is noot okay.\n", + False, + ), + ( + ["-l", "en-US", "--enabled-only", "-E", "TYPOS", "-"], + "This is okay.\n", + True, + ), ], ) def test_cli_exit_codes( @@ -132,6 +157,61 @@ def test_cli_remote_error(remote_server: tuple[str, int]) -> None: assert code != 0 +def test_parse_args_enabled_only_with_enable_categories() -> None: + """Test that --enabled-only is accepted when only --enable-categories is provided. + + :raises AssertionError: If parse_args raises an error for this valid combination. + """ + args = parse_args(["-l", "en-US", "--enabled-only", "-E", "TYPOS", "file.txt"]) + assert args.enabled_only is True + assert args.enable_categories == {"TYPOS"} + + +def test_parse_args_enabled_only_rejects_disable_categories() -> None: + """Test that --enabled-only cannot be combined with --disable-categories. + + :raises SystemExit: Expected, as argparse calls sys.exit on error. + """ + with pytest.raises(SystemExit): + parse_args( + ["-l", "en-US", "--enabled-only", "-e", "RULE", "-D", "TYPOS", "file.txt"] + ) + + +def test_parse_args_enabled_only_requires_enable_or_enable_categories() -> None: + """Test that --enabled-only requires at least --enable or --enable-categories. + + :raises SystemExit: Expected, as argparse calls sys.exit on error. + """ + with pytest.raises(SystemExit): + parse_args(["-l", "en-US", "--enabled-only", "file.txt"]) + + +def test_parse_args_categories() -> None: + """Test that --disable-categories and --enable-categories are parsed correctly. + + :raises AssertionError: If the parsed category sets do not match the expected + values. + """ + args = parse_args( + ["-l", "en-US", "-D", "TYPOS,GRAMMAR", "-E", "PUNCTUATION", "file.txt"] + ) + assert args.disable_categories == {"TYPOS", "GRAMMAR"} + assert args.enable_categories == {"PUNCTUATION"} + + +def test_parse_args_categories_multiple_flags() -> None: + """Test that repeated -D/-E flags accumulate into the same set. + + :raises AssertionError: If the category sets do not accumulate correctly. + """ + args = parse_args( + ["-l", "en-US", "-D", "TYPOS", "-D", "GRAMMAR", "-E", "PUNCTUATION", "file.txt"] + ) + assert args.disable_categories == {"TYPOS", "GRAMMAR"} + assert args.enable_categories == {"PUNCTUATION"} + + def main_with_stdin(argv: list[str], stdin: str) -> int: """Execute the main CLI with simulated stdin input.