Repository: python/mypy

Issue:
[stubtest] Ignore private arguments
Private arguments to functions are occasionally marked with a single leading underscore. Stubtest should ignore arguments, if:

* They start with a single underline character.
* Have a default.
* Are not present in the stubs.


Discussion:

Resolving patch:
diff --git a/mypy/stubtest.py b/mypy/stubtest.py
--- 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]


Test patch:
diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py
--- 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(