Skip to content

Commit dd305fe

Browse files
authored
🐛 fix(type): correct argparse override signatures for ty 0.0.33 (#3932)
1 parent 3aa3cd5 commit dd305fe

4 files changed

Lines changed: 52 additions & 21 deletions

File tree

‎docs/changelog/3932.bugfix.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Correct type annotations for ``ArgumentParser.parse_args`` and ``parse_known_args`` overrides following `typeshed PR
2+
#15613 <https://github.com/python/typeshed/pull/15613>`_, which widened the ``args`` parameter from ``Sequence[str]`` to
3+
``Iterable[str]``. The narrower type in tox's overrides violated the Liskov substitution principle and caused
4+
``invalid-method-override`` errors with ``ty`` 0.0.33. Also correct the ``option_spec`` annotation in
5+
``docs/tox_conf.py`` to ``ClassVar[dict[str, Callable[[str], Any]]]`` matching the docutils stubs type.

‎docs/tox_conf.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from sphinx.util.logging import getLogger
1111

1212
if TYPE_CHECKING:
13+
from collections.abc import Callable
14+
1315
from docutils.parsers.rst.states import RSTState, RSTStateMachine
1416
from sphinx.domains.std import StandardDomain
1517

@@ -19,7 +21,7 @@
1921
class ToxConfig(SphinxDirective):
2022
name = "conf"
2123
has_content = True
22-
option_spec: ClassVar[dict[str, Any]] = {
24+
option_spec: ClassVar[dict[str, Callable[[str], Any]]] = {
2325
"keys": unchanged_required,
2426
"version_added": unchanged,
2527
"version_deprecated": unchanged,

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ test = [
104104
]
105105
type = [
106106
"ty>=0.0.19",
107+
"types-docutils>=0.21",
107108
{ include-group = "docs" },
108109
{ include-group = "release" },
109110
{ include-group = "test" },

‎src/tox/config/cli/parser.py‎

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from argparse import SUPPRESS, Action, ArgumentDefaultsHelpFormatter, ArgumentError, ArgumentParser, Namespace
1111
from pathlib import Path
1212
from types import UnionType
13-
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
13+
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast, overload
1414

1515
from colorama import Fore
1616

@@ -26,10 +26,12 @@
2626
from typing_extensions import Self
2727

2828
if TYPE_CHECKING:
29-
from collections.abc import Callable, Sequence
29+
from collections.abc import Callable, Iterable, Sequence
3030

3131
from tox.session.state import State
3232

33+
_N = TypeVar("_N", bound=Namespace)
34+
3335

3436
class ArgumentParserWithEnvAndConfig(ArgumentParser):
3537
"""Argument parser which updates its defaults by checking the configuration files and environmental variables."""
@@ -80,20 +82,29 @@ def get_type(action: Action) -> type[Any]:
8082
raise TypeError(action)
8183
return of_type
8284

83-
def parse_args( # avoid defining all overloads
85+
@overload
86+
def parse_args(self, args: Iterable[str] | None = None, namespace: None = None) -> Namespace: ...
87+
88+
@overload
89+
def parse_args(self, args: Iterable[str] | None, namespace: _N) -> _N: ...
90+
91+
@overload
92+
def parse_args(self, *, namespace: _N) -> _N: ...
93+
94+
def parse_args(
8495
self,
85-
args: Sequence[str] | None = None,
86-
namespace: Namespace | None = None,
87-
) -> Namespace:
88-
res, argv = self.parse_known_args(args, namespace)
96+
args: Iterable[str] | None = None,
97+
namespace: _N | None = None,
98+
) -> _N:
99+
res, argv = self.parse_known_args(list(args) if args is not None else None, namespace)
89100
if argv:
90101
self.error(
91102
f"unrecognized arguments: {' '.join(argv)}\n"
92103
"hint: if you tried to pass arguments to a command use -- to separate them from tox ones",
93104
)
94105
if getattr(res, "no_capture", False) and getattr(res, "result_json", None):
95106
self.error("argument -i/--no-capture: not allowed with argument --result-json")
96-
return cast("Namespace", res)
107+
return cast("_N", res)
97108

98109

99110
class HelpFormatter(ArgumentDefaultsHelpFormatter):
@@ -374,29 +385,41 @@ def _add_base_options(self) -> None:
374385
add_core_arguments(self)
375386
self.fix_defaults()
376387

388+
@overload
389+
def parse_known_args(
390+
self, args: Iterable[str] | None = None, namespace: None = None
391+
) -> tuple[Parsed, list[str]]: ...
392+
393+
@overload
394+
def parse_known_args(self, args: Iterable[str] | None, namespace: _N) -> tuple[_N, list[str]]: ...
395+
396+
@overload
397+
def parse_known_args(self, *, namespace: _N) -> tuple[_N, list[str]]: ...
398+
377399
def parse_known_args(
378400
self,
379-
args: Sequence[str] | None = None,
380-
namespace: Parsed | None = None,
381-
) -> tuple[Parsed, list[str]]:
382-
if args is None:
383-
args = sys.argv[1:]
401+
args: Iterable[str] | None = None,
402+
namespace: _N | None = None,
403+
) -> tuple[_N, list[str]]:
404+
args_list: list[str] = list(args) if args is not None else sys.argv[1:]
384405
cmd_at: int | None = None
385-
if self._cmd is not None and args:
386-
for at, arg in enumerate(args):
406+
if self._cmd is not None and args_list:
407+
for at, arg in enumerate(args_list):
387408
if arg in self._cmd.choices:
388409
cmd_at = at
389410
break
390411
else:
391412
cmd_at = None
392413
if cmd_at is not None: # if we found a command move it to the start
393-
args = args[cmd_at], *args[:cmd_at], *args[cmd_at + 1 :]
394-
elif tuple(args) not in {("--help",), ("-h",)} and (self._cmd is not None and "legacy" in self._cmd.choices):
414+
args_list = [args_list[cmd_at], *args_list[:cmd_at], *args_list[cmd_at + 1 :]]
415+
elif tuple(args_list) not in {("--help",), ("-h",)} and (
416+
self._cmd is not None and "legacy" in self._cmd.choices
417+
):
395418
# on help no mangling needed, and we also want to insert once we have legacy to insert
396-
args = "legacy", *args
419+
args_list = ["legacy", *args_list]
397420
result = Parsed() if namespace is None else namespace
398-
_, args = super().parse_known_args(args, namespace=result)
399-
return result, args
421+
_, remainder = super().parse_known_args(args_list, namespace=result)
422+
return cast("tuple[_N, list[str]]", (result, remainder))
400423

401424

402425
def add_core_arguments(parser: ArgumentParser) -> None:

0 commit comments

Comments
 (0)