From b956e6a57c4dd36d670097a3eccf7dc092348fec Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 5 Feb 2024 14:17:43 +0100 Subject: [PATCH] stubtest: Private parameters can be omitted (#16507) Fixes #16443 --- CHANGELOG.md | 3 ++ mypy/stubtest.py | 14 ++++++-- mypy/test/teststubtest.py | 72 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d79b415f5d..bae881656865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Next release +Stubtest will ignore private function/method parameters when they are missing from the stub. Private parameters +names start with a single underscore and have a default (PR [16507](https://github.com/python/mypy/pull/16507)). + ## Mypy 1.8 We’ve just uploaded mypy 1.8 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). Mypy is a static type checker for Python. This release includes new features, performance improvements and bug fixes. You can install it as follows: diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 225c1c35c1be..0e8a1c3ceac2 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -940,7 +940,8 @@ def _verify_signature( elif len(stub.pos) < len(runtime.pos): for runtime_arg in runtime.pos[len(stub.pos) :]: if runtime_arg.name not in stub.kwonly: - yield f'stub does not have argument "{runtime_arg.name}"' + if not _is_private_parameter(runtime_arg): + yield f'stub does not have argument "{runtime_arg.name}"' else: yield f'runtime argument "{runtime_arg.name}" is not keyword-only' @@ -980,7 +981,8 @@ def _verify_signature( ): yield f'stub argument "{arg}" is not keyword-only' else: - yield f'stub does not have argument "{arg}"' + if not _is_private_parameter(runtime.kwonly[arg]): + yield f'stub does not have argument "{arg}"' # Checks involving **kwargs if stub.varkw is None and runtime.varkw is not None: @@ -995,6 +997,14 @@ def _verify_signature( yield f'runtime does not have **kwargs argument "{stub.varkw.variable.name}"' +def _is_private_parameter(arg: inspect.Parameter) -> bool: + return ( + arg.name.startswith("_") + and not arg.name.startswith("__") + and arg.default is not inspect.Parameter.empty + ) + + @verify.register(nodes.FuncItem) def verify_funcitem( stub: nodes.FuncItem, runtime: MaybeMissing[Any], object_path: list[str] diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 55f35200a7f5..418308e2e65e 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -346,6 +346,78 @@ def test_arg_kind(self) -> Iterator[Case]: error="stub_posonly_570", ) + @collect_cases + def test_private_parameters(self) -> Iterator[Case]: + # Private parameters can optionally be omitted. + yield Case( + stub="def priv_pos_arg_missing() -> None: ...", + runtime="def priv_pos_arg_missing(_p1=None): pass", + error=None, + ) + yield Case( + stub="def multi_priv_args() -> None: ...", + runtime="def multi_priv_args(_p='', _q=''): pass", + error=None, + ) + yield Case( + stub="def priv_kwarg_missing() -> None: ...", + runtime="def priv_kwarg_missing(*, _p2=''): pass", + error=None, + ) + # But if they are included, they must be correct. + yield Case( + stub="def priv_pos_arg_wrong(_p: int = ...) -> None: ...", + runtime="def priv_pos_arg_wrong(_p=None): pass", + error="priv_pos_arg_wrong", + ) + yield Case( + stub="def priv_kwarg_wrong(*, _p: int = ...) -> None: ...", + runtime="def priv_kwarg_wrong(*, _p=None): pass", + error="priv_kwarg_wrong", + ) + # Private parameters must have a default and start with exactly one + # underscore. + yield Case( + stub="def pos_arg_no_default() -> None: ...", + runtime="def pos_arg_no_default(_np): pass", + error="pos_arg_no_default", + ) + yield Case( + stub="def kwarg_no_default() -> None: ...", + runtime="def kwarg_no_default(*, _np): pass", + error="kwarg_no_default", + ) + yield Case( + stub="def double_underscore_pos_arg() -> None: ...", + runtime="def double_underscore_pos_arg(__np = None): pass", + error="double_underscore_pos_arg", + ) + yield Case( + stub="def double_underscore_kwarg() -> None: ...", + runtime="def double_underscore_kwarg(*, __np = None): pass", + error="double_underscore_kwarg", + ) + # But spot parameters that are accidentally not marked kw-only and + # vice-versa. + yield Case( + stub="def priv_arg_is_kwonly(_p=...) -> None: ...", + runtime="def priv_arg_is_kwonly(*, _p=''): pass", + error="priv_arg_is_kwonly", + ) + yield Case( + stub="def priv_arg_is_positional(*, _p=...) -> None: ...", + runtime="def priv_arg_is_positional(_p=''): pass", + error="priv_arg_is_positional", + ) + # Private parameters not at the end of the parameter list must be + # included so that users can pass the following arguments using + # positional syntax. + yield Case( + stub="def priv_args_not_at_end(*, q='') -> None: ...", + runtime="def priv_args_not_at_end(_p='', q=''): pass", + error="priv_args_not_at_end", + ) + @collect_cases def test_default_presence(self) -> Iterator[Case]: yield Case(