88import re
99import sys
1010import traceback
11- from collections .abc import Sequence
1211from importlib .metadata import PackageNotFoundError , version
1312from logging .config import dictConfig
1413from pathlib import Path
15- from typing import TYPE_CHECKING , cast
14+ from typing import TYPE_CHECKING , TypedDict , cast
1615
1716from ._compat import toml_loads
1817from .exceptions import LanguageToolError
1918from .server import LanguageTool
2019
2120if TYPE_CHECKING :
2221 from collections .abc import Sequence
22+ from typing import TextIO
23+
24+
25+ class _PyProjectProject (TypedDict ):
26+ version : str
27+
28+
29+ class _PyProject (TypedDict ):
30+ project : _PyProjectProject
31+
32+
33+ def _load_pyproject_and_logconfig (path : Path ) -> dict [str , object ]:
34+ """Load a TOML file as a typed dictionary.
35+
36+ :param path: The path to the TOML file to load.
37+ :type path: Path
38+ :return: The contents of the TOML file as a dictionary.
39+ :rtype: dict[str, object]
40+ """
41+ with path .open ("rb" ) as f :
42+ return cast ("dict[str, object]" , toml_loads (f .read ().decode ("utf-8" )))
43+
44+
45+ def _read_project_version (pyproject : Path ) -> str :
46+ """Read the package version from pyproject.toml.
47+
48+ :param pyproject: The path to the pyproject.toml file.
49+ :type pyproject: Path
50+ :return: The package version.
51+ :rtype: str
52+ """
53+ pyproject_config = cast ("_PyProject" , _load_pyproject_and_logconfig (pyproject ))
54+ return pyproject_config ["project" ]["version" ]
55+
2356
2457try :
2558 __version__ = version ("language_tool_python" )
2861except PackageNotFoundError :
2962 project_root = Path (__file__ ).resolve ().parent .parent
3063 pyproject = project_root / "pyproject.toml"
31- with pyproject .open ("rb" ) as f :
32- __version__ = toml_loads (f .read ().decode ("utf-8" ))["project" ]["version" ]
64+ __version__ = _read_project_version (pyproject )
3365
3466
3567logger = logging .getLogger (__name__ )
36- with (
37- importlib .resources .as_file (
38- importlib .resources .files ("language_tool_python" ).joinpath ("logging.toml" ),
39- ) as config_path ,
40- config_path .open ("rb" ) as f ,
41- ):
42- log_config = toml_loads (f .read ().decode ("utf-8" ))
68+ with importlib .resources .as_file (
69+ importlib .resources .files ("language_tool_python" ).joinpath ("logging.toml" ),
70+ ) as config_path :
71+ log_config = _load_pyproject_and_logconfig (config_path )
4372dictConfig (log_config )
4473
74+ RULE_RE : re .Pattern [str ] = re .compile (r"[\w-]+" )
75+
4576
46- def parse_args (argv : Sequence [str ] | None = None ) -> argparse .Namespace :
77+ class CliArgs (argparse .Namespace ):
78+ """Typed command-line arguments."""
79+
80+ files : list [str ]
81+ encoding : str | None
82+ language : str | None
83+ mother_tongue : str | None
84+ disable : set [str ]
85+ enable : set [str ]
86+ enabled_only : bool
87+ picky : bool
88+ apply : bool
89+ spell_check : bool
90+ ignore_lines : str | None
91+ remote_host : str | None
92+ remote_port : str | None
93+ verbose : bool
94+
95+
96+ def parse_args (argv : Sequence [str ] | None = None ) -> CliArgs :
4797 """Parse command line arguments.
4898
4999 :return: parsed arguments
50- :rtype: argparse.Namespace
100+ :rtype: CliArgs
51101 """
52102 parser = argparse .ArgumentParser (
53103 description = __doc__ .strip () if __doc__ else None ,
@@ -73,7 +123,7 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
73123 metavar = "RULES" ,
74124 type = get_rules ,
75125 action = RulesAction ,
76- default = set (),
126+ default = set [ str ] (),
77127 help = "list of rule IDs to be disabled" ,
78128 )
79129 parser .add_argument (
@@ -82,7 +132,7 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
82132 metavar = "RULES" ,
83133 type = get_rules ,
84134 action = RulesAction ,
85- default = set (),
135+ default = set [ str ] (),
86136 help = "list of rule IDs to be enabled" ,
87137 )
88138 parser .add_argument (
@@ -126,7 +176,8 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
126176 parser .add_argument ("--remote-port" , help = "port of the remote LanguageTool server" )
127177 parser .add_argument ("--verbose" , action = "store_true" , help = "enable verbose output" )
128178
129- args = parser .parse_args (argv )
179+ args = CliArgs ()
180+ parser .parse_args (argv , namespace = args )
130181
131182 if args .enabled_only :
132183 if args .disable :
@@ -165,15 +216,21 @@ def __call__(
165216 :param _parser: The ArgumentParser object which contains this action.
166217 :type _parser: argparse.ArgumentParser
167218 :param namespace: The namespace object that will be returned by parse_args().
168- :type namespace: argparse.Namespace
219+ :type namespace: CliArgs
169220 :param values: The argument values associated with the action.
170221 :type values: str | Sequence[object] | None
171222 :param _option_string: The option string that was used to invoke this action.
172223 :type _option_string: str | None
173224 """
174- getattr (namespace , self .dest ).update (
175- cast ("set[str]" , values ),
176- )
225+ cli_args = cast ("CliArgs" , namespace )
226+ rule_values = cast ("set[str]" , values )
227+ if self .dest == "disable" :
228+ cli_args .disable .update (rule_values )
229+ elif self .dest == "enable" :
230+ cli_args .enable .update (rule_values )
231+ else :
232+ err = f"unexpected rules destination: { self .dest } "
233+ raise ValueError (err )
177234
178235
179236def get_rules (rules : str ) -> set [str ]:
@@ -184,7 +241,8 @@ def get_rules(rules: str) -> set[str]:
184241 :return: A set of rule IDs.
185242 :rtype: set[str]
186243 """
187- return {rule .upper () for rule in re .findall (r"[\w\-]+" , rules )}
244+ rule_ids = cast ("list[str]" , RULE_RE .findall (rules ))
245+ return {rule .upper () for rule in rule_ids }
188246
189247
190248def get_text (
@@ -223,11 +281,11 @@ def print_exception(exc: Exception, debug: bool) -> None:
223281 print (exc , file = sys .stderr )
224282
225283
226- def get_remote_server (args : argparse . Namespace ) -> str | None :
284+ def get_remote_server (args : CliArgs ) -> str | None :
227285 """Build the remote server address from parsed arguments.
228286
229287 :param args: Parsed command-line arguments.
230- :type args: argparse.Namespace
288+ :type args: CliArgs
231289 :return: The remote server address in the format "host:port" or None if no remote
232290 host is specified.
233291 :rtype: str | None
@@ -242,18 +300,19 @@ def get_remote_server(args: argparse.Namespace) -> str | None:
242300 return remote_server
243301
244302
245- def get_input_text (filename : str , args : argparse . Namespace ) -> str :
303+ def get_input_text (filename : str , args : CliArgs ) -> str :
246304 """Read input text from a file or stdin.
247305
248306 :param filename: The name of the file to read or "-" for stdin.
249307 :type filename: str
250308 :param args: Parsed command-line arguments.
251- :type args: argparse.Namespace
309+ :type args: CliArgs
252310 :return: The input text as a string.
253311 :rtype: str
254312 """
255313 if filename == "-" :
256- raw = sys .stdin .read ()
314+ stdin = cast ("TextIO" , sys .stdin )
315+ raw = stdin .read ()
257316 if args .ignore_lines :
258317 return "" .join (
259318 "\n " if re .match (args .ignore_lines , line ) else line
@@ -267,15 +326,15 @@ def get_input_text(filename: str, args: argparse.Namespace) -> str:
267326
268327def process_file (
269328 filename : str ,
270- args : argparse . Namespace ,
329+ args : CliArgs ,
271330 remote_server : str | None ,
272331) -> int :
273332 """Check a single input file and return the resulting status.
274333
275334 :param filename: The name of the file to check or "-" for stdin.
276335 :type filename: str
277336 :param args: Parsed command-line arguments.
278- :type args: argparse.Namespace
337+ :type args: CliArgs
279338 :param remote_server: The remote server address or None.
280339 :type remote_server: str | None
281340 :return: The resulting status.
0 commit comments