From fe0aa92cda81fae0ec59dd94c591c0aa99cebb67 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Fri, 6 Dec 2024 12:33:17 -0800 Subject: [PATCH 1/9] stubtest: get better signatures for __init__ of C classes When an __init__ method has the generic C-class signature, check the underlying class for a better signature. --- mypy/stubtest.py | 197 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 36cd0a213d4d..579640ee516a 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -7,6 +7,7 @@ from __future__ import annotations import argparse +import ast import collections.abc import copy import enum @@ -14,6 +15,7 @@ import importlib import importlib.machinery import inspect +import itertools import os import pkgutil import re @@ -1526,7 +1528,202 @@ def is_read_only_property(runtime: object) -> bool: return isinstance(runtime, property) and runtime.fset is None +def _signature_fromstr( + cls: type[inspect.Signature], obj: Any, s: str, skip_bound_arg: bool = True +) -> inspect.Signature: + """Private helper to parse content of '__text_signature__' + and return a Signature based on it. + + This is a copy of inspect._signature_fromstr from 3.13, which we need + for python/cpython#115270, an important fix for working with + built-in instance methods. + """ + Parameter = cls._parameter_cls # type: ignore[attr-defined] + + if sys.version_info >= (3, 12): + clean_signature, self_parameter = inspect._signature_strip_non_python_syntax(s) # type: ignore[attr-defined] + else: + clean_signature, self_parameter, last_positional_only = inspect._signature_strip_non_python_syntax(s) # type: ignore[attr-defined] + + program = "def foo" + clean_signature + ": pass" + + try: + module_ast = ast.parse(program) + except SyntaxError: + module_ast = None + + if not isinstance(module_ast, ast.Module): + raise ValueError("{!r} builtin has invalid signature".format(obj)) + + f = module_ast.body[0] + assert isinstance(f, ast.FunctionDef) + + parameters = [] + empty = Parameter.empty + + module = None + module_dict: dict[str, Any] = {} + + module_name = getattr(obj, "__module__", None) + if not module_name: + objclass = getattr(obj, "__objclass__", None) + module_name = getattr(objclass, "__module__", None) + + if module_name: + module = sys.modules.get(module_name, None) + if module: + module_dict = module.__dict__ + sys_module_dict = sys.modules.copy() + + def parse_name(node: ast.arg) -> str: + assert isinstance(node, ast.arg) + if node.annotation is not None: + raise ValueError("Annotations are not currently supported") + return node.arg + + def wrap_value(s: str) -> ast.Constant: + try: + value = eval(s, module_dict) + except NameError: + try: + value = eval(s, sys_module_dict) + except NameError: + raise ValueError + + if isinstance(value, (str, int, float, bytes, bool, type(None))): + return ast.Constant(value) + raise ValueError + + class RewriteSymbolics(ast.NodeTransformer): + def visit_Attribute(self, node: ast.Attribute) -> Any: + a = [] + n: ast.expr = node + while isinstance(n, ast.Attribute): + a.append(n.attr) + n = n.value + if not isinstance(n, ast.Name): + raise ValueError + a.append(n.id) + value = ".".join(reversed(a)) + return wrap_value(value) + + def visit_Name(self, node: ast.Name) -> Any: + if not isinstance(node.ctx, ast.Load): + raise ValueError() + return wrap_value(node.id) + + def visit_BinOp(self, node: ast.BinOp) -> Any: + # Support constant folding of a couple simple binary operations + # commonly used to define default values in text signatures + left = self.visit(node.left) + right = self.visit(node.right) + if not isinstance(left, ast.Constant) or not isinstance(right, ast.Constant): + raise ValueError + if isinstance(node.op, ast.Add): + return ast.Constant(left.value + right.value) + elif isinstance(node.op, ast.Sub): + return ast.Constant(left.value - right.value) + elif isinstance(node.op, ast.BitOr): + return ast.Constant(left.value | right.value) + raise ValueError + + def p(name_node: ast.arg, default_node: Any, default: Any = empty) -> None: + name = parse_name(name_node) + if default_node and default_node is not inspect._empty: + try: + default_node = RewriteSymbolics().visit(default_node) + default = ast.literal_eval(default_node) + except ValueError: + raise ValueError("{!r} builtin has invalid signature".format(obj)) from None + parameters.append(Parameter(name, kind, default=default, annotation=empty)) + + # non-keyword-only parameters + if sys.version_info >= (3, 12): + total_non_kw_args = len(f.args.posonlyargs) + len(f.args.args) + required_non_kw_args = total_non_kw_args - len(f.args.defaults) + defaults = itertools.chain(itertools.repeat(None, required_non_kw_args), f.args.defaults) + + kind = Parameter.POSITIONAL_ONLY + for name, default in zip(f.args.posonlyargs, defaults): + p(name, default) + + kind = Parameter.POSITIONAL_OR_KEYWORD + for name, default in zip(f.args.args, defaults): + p(name, default) + + else: + args = reversed(f.args.args) + defaults = reversed(f.args.defaults) + iter = itertools.zip_longest(args, defaults, fillvalue=None) + if last_positional_only is not None: + kind = Parameter.POSITIONAL_ONLY + else: + kind = Parameter.POSITIONAL_OR_KEYWORD + for i, (name, default) in enumerate(reversed(list(iter))): + p(name, default) + if i == last_positional_only: + kind = Parameter.POSITIONAL_OR_KEYWORD + + # *args + if f.args.vararg: + kind = Parameter.VAR_POSITIONAL + p(f.args.vararg, empty) + + # keyword-only arguments + kind = Parameter.KEYWORD_ONLY + for name, default in zip(f.args.kwonlyargs, f.args.kw_defaults): + p(name, default) + + # **kwargs + if f.args.kwarg: + kind = Parameter.VAR_KEYWORD + p(f.args.kwarg, empty) + + if self_parameter is not None: + # Possibly strip the bound argument: + # - We *always* strip first bound argument if + # it is a module. + # - We don't strip first bound argument if + # skip_bound_arg is False. + assert parameters + _self = getattr(obj, "__self__", None) + self_isbound = _self is not None + self_ismodule = inspect.ismodule(_self) + if self_isbound and (self_ismodule or skip_bound_arg): + parameters.pop(0) + else: + # for builtins, self parameter is always positional-only! + p = parameters[0].replace(kind=Parameter.POSITIONAL_ONLY) + parameters[0] = p + + return cls(parameters, return_annotation=cls.empty) + + def safe_inspect_signature(runtime: Any) -> inspect.Signature | None: + if ( + hasattr(runtime, "__name__") + and runtime.__name__ == "__init__" + and hasattr(runtime, "__text_signature__") + and runtime.__text_signature__ == "($self, /, *args, **kwargs)" + and hasattr(runtime, "__objclass__") + and runtime.__objclass__ is not object + and hasattr(runtime.__objclass__, "__text_signature__") + and runtime.__objclass__.__text_signature__ is not None + ): + # This is an __init__ method with the generic C-class signature. + # In this case, the underlying class usually has a better signature, + # which we can convert into an __init__ signature by adding $self + # at the start. If we hit an error, failover to the normal + # path without trying to recover. + if "/" in runtime.__objclass__.__text_signature__: + new_sig = f"($self, {runtime.__objclass__.__text_signature__[1:]}" + else: + new_sig = f"($self, /, {runtime.__objclass__.__text_signature__[1:]}" + try: + return _signature_fromstr(inspect.Signature, runtime, new_sig) + except Exception: + pass + try: try: return inspect.signature(runtime) From 331f9d7443c523182a1b0f2b3c31d909b2d4e3fd Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Fri, 6 Dec 2024 12:45:27 -0800 Subject: [PATCH 2/9] pipeline fixes --- mypy/stubtest.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 579640ee516a..4e1026afc8b3 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1587,15 +1587,15 @@ def wrap_value(s: str) -> ast.Constant: except NameError: try: value = eval(s, sys_module_dict) - except NameError: - raise ValueError + except NameError as err: + raise ValueError from err if isinstance(value, (str, int, float, bytes, bool, type(None))): return ast.Constant(value) raise ValueError class RewriteSymbolics(ast.NodeTransformer): - def visit_Attribute(self, node: ast.Attribute) -> Any: + def visit_Attribute(self, node: ast.Attribute) -> Any: # noqa: N802 a = [] n: ast.expr = node while isinstance(n, ast.Attribute): @@ -1607,12 +1607,12 @@ def visit_Attribute(self, node: ast.Attribute) -> Any: value = ".".join(reversed(a)) return wrap_value(value) - def visit_Name(self, node: ast.Name) -> Any: + def visit_Name(self, node: ast.Name) -> Any: # noqa: N802 if not isinstance(node.ctx, ast.Load): raise ValueError() return wrap_value(node.id) - def visit_BinOp(self, node: ast.BinOp) -> Any: + def visit_BinOp(self, node: ast.BinOp) -> Any: # noqa: N802 # Support constant folding of a couple simple binary operations # commonly used to define default values in text signatures left = self.visit(node.left) @@ -1660,6 +1660,7 @@ def p(name_node: ast.arg, default_node: Any, default: Any = empty) -> None: else: kind = Parameter.POSITIONAL_OR_KEYWORD for i, (name, default) in enumerate(reversed(list(iter))): + assert name is not None p(name, default) if i == last_positional_only: kind = Parameter.POSITIONAL_OR_KEYWORD From bfab0f8b1826ae5d565576f9558da2ab983e75f7 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Fri, 6 Dec 2024 14:12:13 -0800 Subject: [PATCH 3/9] don't use private inspect module stuff --- mypy/stubtest.py | 197 ++++------------------------------------------- 1 file changed, 15 insertions(+), 182 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 4e1026afc8b3..1bef4c801ffa 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -7,7 +7,6 @@ from __future__ import annotations import argparse -import ast import collections.abc import copy import enum @@ -15,7 +14,6 @@ import importlib import importlib.machinery import inspect -import itertools import os import pkgutil import re @@ -1528,178 +1526,6 @@ def is_read_only_property(runtime: object) -> bool: return isinstance(runtime, property) and runtime.fset is None -def _signature_fromstr( - cls: type[inspect.Signature], obj: Any, s: str, skip_bound_arg: bool = True -) -> inspect.Signature: - """Private helper to parse content of '__text_signature__' - and return a Signature based on it. - - This is a copy of inspect._signature_fromstr from 3.13, which we need - for python/cpython#115270, an important fix for working with - built-in instance methods. - """ - Parameter = cls._parameter_cls # type: ignore[attr-defined] - - if sys.version_info >= (3, 12): - clean_signature, self_parameter = inspect._signature_strip_non_python_syntax(s) # type: ignore[attr-defined] - else: - clean_signature, self_parameter, last_positional_only = inspect._signature_strip_non_python_syntax(s) # type: ignore[attr-defined] - - program = "def foo" + clean_signature + ": pass" - - try: - module_ast = ast.parse(program) - except SyntaxError: - module_ast = None - - if not isinstance(module_ast, ast.Module): - raise ValueError("{!r} builtin has invalid signature".format(obj)) - - f = module_ast.body[0] - assert isinstance(f, ast.FunctionDef) - - parameters = [] - empty = Parameter.empty - - module = None - module_dict: dict[str, Any] = {} - - module_name = getattr(obj, "__module__", None) - if not module_name: - objclass = getattr(obj, "__objclass__", None) - module_name = getattr(objclass, "__module__", None) - - if module_name: - module = sys.modules.get(module_name, None) - if module: - module_dict = module.__dict__ - sys_module_dict = sys.modules.copy() - - def parse_name(node: ast.arg) -> str: - assert isinstance(node, ast.arg) - if node.annotation is not None: - raise ValueError("Annotations are not currently supported") - return node.arg - - def wrap_value(s: str) -> ast.Constant: - try: - value = eval(s, module_dict) - except NameError: - try: - value = eval(s, sys_module_dict) - except NameError as err: - raise ValueError from err - - if isinstance(value, (str, int, float, bytes, bool, type(None))): - return ast.Constant(value) - raise ValueError - - class RewriteSymbolics(ast.NodeTransformer): - def visit_Attribute(self, node: ast.Attribute) -> Any: # noqa: N802 - a = [] - n: ast.expr = node - while isinstance(n, ast.Attribute): - a.append(n.attr) - n = n.value - if not isinstance(n, ast.Name): - raise ValueError - a.append(n.id) - value = ".".join(reversed(a)) - return wrap_value(value) - - def visit_Name(self, node: ast.Name) -> Any: # noqa: N802 - if not isinstance(node.ctx, ast.Load): - raise ValueError() - return wrap_value(node.id) - - def visit_BinOp(self, node: ast.BinOp) -> Any: # noqa: N802 - # Support constant folding of a couple simple binary operations - # commonly used to define default values in text signatures - left = self.visit(node.left) - right = self.visit(node.right) - if not isinstance(left, ast.Constant) or not isinstance(right, ast.Constant): - raise ValueError - if isinstance(node.op, ast.Add): - return ast.Constant(left.value + right.value) - elif isinstance(node.op, ast.Sub): - return ast.Constant(left.value - right.value) - elif isinstance(node.op, ast.BitOr): - return ast.Constant(left.value | right.value) - raise ValueError - - def p(name_node: ast.arg, default_node: Any, default: Any = empty) -> None: - name = parse_name(name_node) - if default_node and default_node is not inspect._empty: - try: - default_node = RewriteSymbolics().visit(default_node) - default = ast.literal_eval(default_node) - except ValueError: - raise ValueError("{!r} builtin has invalid signature".format(obj)) from None - parameters.append(Parameter(name, kind, default=default, annotation=empty)) - - # non-keyword-only parameters - if sys.version_info >= (3, 12): - total_non_kw_args = len(f.args.posonlyargs) + len(f.args.args) - required_non_kw_args = total_non_kw_args - len(f.args.defaults) - defaults = itertools.chain(itertools.repeat(None, required_non_kw_args), f.args.defaults) - - kind = Parameter.POSITIONAL_ONLY - for name, default in zip(f.args.posonlyargs, defaults): - p(name, default) - - kind = Parameter.POSITIONAL_OR_KEYWORD - for name, default in zip(f.args.args, defaults): - p(name, default) - - else: - args = reversed(f.args.args) - defaults = reversed(f.args.defaults) - iter = itertools.zip_longest(args, defaults, fillvalue=None) - if last_positional_only is not None: - kind = Parameter.POSITIONAL_ONLY - else: - kind = Parameter.POSITIONAL_OR_KEYWORD - for i, (name, default) in enumerate(reversed(list(iter))): - assert name is not None - p(name, default) - if i == last_positional_only: - kind = Parameter.POSITIONAL_OR_KEYWORD - - # *args - if f.args.vararg: - kind = Parameter.VAR_POSITIONAL - p(f.args.vararg, empty) - - # keyword-only arguments - kind = Parameter.KEYWORD_ONLY - for name, default in zip(f.args.kwonlyargs, f.args.kw_defaults): - p(name, default) - - # **kwargs - if f.args.kwarg: - kind = Parameter.VAR_KEYWORD - p(f.args.kwarg, empty) - - if self_parameter is not None: - # Possibly strip the bound argument: - # - We *always* strip first bound argument if - # it is a module. - # - We don't strip first bound argument if - # skip_bound_arg is False. - assert parameters - _self = getattr(obj, "__self__", None) - self_isbound = _self is not None - self_ismodule = inspect.ismodule(_self) - if self_isbound and (self_ismodule or skip_bound_arg): - parameters.pop(0) - else: - # for builtins, self parameter is always positional-only! - p = parameters[0].replace(kind=Parameter.POSITIONAL_ONLY) - parameters[0] = p - - return cls(parameters, return_annotation=cls.empty) - - def safe_inspect_signature(runtime: Any) -> inspect.Signature | None: if ( hasattr(runtime, "__name__") @@ -1713,15 +1539,22 @@ def safe_inspect_signature(runtime: Any) -> inspect.Signature | None: ): # This is an __init__ method with the generic C-class signature. # In this case, the underlying class usually has a better signature, - # which we can convert into an __init__ signature by adding $self - # at the start. If we hit an error, failover to the normal - # path without trying to recover. - if "/" in runtime.__objclass__.__text_signature__: - new_sig = f"($self, {runtime.__objclass__.__text_signature__[1:]}" - else: - new_sig = f"($self, /, {runtime.__objclass__.__text_signature__[1:]}" + # which we can convert into an __init__ signature by adding in the + # self parameter. + + # TODO: Handle classes who get __init__ from object, but would have + # a better signature on the actual objclass if we had access to it + # here. This would probably require a second parameter on + # safe_inspect_signature to pass in the original class that this + # runtime method object was collected from? try: - return _signature_fromstr(inspect.Signature, runtime, new_sig) + s = inspect.signature(runtime.__objclass__) + return s.replace( + parameters=[ + inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD), + *s.parameters.values(), + ] + ) except Exception: pass From ecf85554940fb2bdc029d08393ffb669c8805738 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Fri, 6 Dec 2024 14:31:23 -0800 Subject: [PATCH 4/9] handle when __objclass__ is object --- mypy/stubtest.py | 51 +++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 1bef4c801ffa..c400c3bb06d6 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -116,7 +116,7 @@ def __init__( self.stub_desc = stub_desc or str(getattr(stub_object, "type", stub_object)) if runtime_desc is None: - runtime_sig = safe_inspect_signature(runtime_object) + runtime_sig = safe_inspect_signature(runtime_object, object_path) if runtime_sig is None: self.runtime_desc = _truncate(repr(runtime_object), 100) else: @@ -1036,7 +1036,7 @@ def verify_funcitem( for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path): yield Error(object_path, "is inconsistent, " + message, stub, runtime) - signature = safe_inspect_signature(runtime) + signature = safe_inspect_signature(runtime, object_path) runtime_is_coroutine = inspect.iscoroutinefunction(runtime) if signature: @@ -1164,7 +1164,7 @@ def verify_overloadedfuncdef( # TODO: Should call _verify_final_method here, # but overloaded final methods in stubs cause a stubtest crash: see #14950 - signature = safe_inspect_signature(runtime) + signature = safe_inspect_signature(runtime, object_path) if not signature: return @@ -1526,14 +1526,15 @@ def is_read_only_property(runtime: object) -> bool: return isinstance(runtime, property) and runtime.fset is None -def safe_inspect_signature(runtime: Any) -> inspect.Signature | None: +def safe_inspect_signature( + runtime: Any, object_path: list[str] | None = None +) -> inspect.Signature | None: if ( hasattr(runtime, "__name__") and runtime.__name__ == "__init__" and hasattr(runtime, "__text_signature__") and runtime.__text_signature__ == "($self, /, *args, **kwargs)" and hasattr(runtime, "__objclass__") - and runtime.__objclass__ is not object and hasattr(runtime.__objclass__, "__text_signature__") and runtime.__objclass__.__text_signature__ is not None ): @@ -1541,22 +1542,32 @@ def safe_inspect_signature(runtime: Any) -> inspect.Signature | None: # In this case, the underlying class usually has a better signature, # which we can convert into an __init__ signature by adding in the # self parameter. + runtime_objclass = None + if runtime.__objclass__ is object: + if object_path: + module_name = ".".join(object_path[:-2]) + module = silent_import_module(module_name) + if module: + runtime_objclass = getattr(module, object_path[-2], None) + if not ( + hasattr(runtime_objclass, "__text_signature__") + and runtime_objclass.__text_signature__ is not None + ): + runtime_objclass = None + else: + runtime_objclass = runtime.__objclass__ - # TODO: Handle classes who get __init__ from object, but would have - # a better signature on the actual objclass if we had access to it - # here. This would probably require a second parameter on - # safe_inspect_signature to pass in the original class that this - # runtime method object was collected from? - try: - s = inspect.signature(runtime.__objclass__) - return s.replace( - parameters=[ - inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD), - *s.parameters.values(), - ] - ) - except Exception: - pass + if runtime_objclass is not None: + try: + s = inspect.signature(runtime_objclass) + return s.replace( + parameters=[ + inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD), + *s.parameters.values(), + ] + ) + except Exception: + pass try: try: From ac02dd59087672ef6ff023c425d0bd3b8cccca2e Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Fri, 6 Dec 2024 14:34:58 -0800 Subject: [PATCH 5/9] add a note --- mypy/stubtest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index c400c3bb06d6..712b55bfbc27 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1544,6 +1544,8 @@ def safe_inspect_signature( # self parameter. runtime_objclass = None if runtime.__objclass__ is object: + # When __objclass__ is object, use the object_path to look up + # the actual class that this __init__ method came from. if object_path: module_name = ".".join(object_path[:-2]) module = silent_import_module(module_name) From b833d09bfb26398825f8c926532348617889de14 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Fri, 6 Dec 2024 14:39:38 -0800 Subject: [PATCH 6/9] fix type check --- mypy/stubtest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 712b55bfbc27..dcc27f280f3f 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1552,7 +1552,8 @@ def safe_inspect_signature( if module: runtime_objclass = getattr(module, object_path[-2], None) if not ( - hasattr(runtime_objclass, "__text_signature__") + runtime_objclass is not None + and hasattr(runtime_objclass, "__text_signature__") and runtime_objclass.__text_signature__ is not None ): runtime_objclass = None From 14acaae3289e4483a0e99b3df25a763121a85dbd Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Sat, 7 Dec 2024 01:13:13 -0800 Subject: [PATCH 7/9] add validation of c-class __new__ methods --- mypy/stubtest.py | 81 +++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index dcc27f280f3f..8c2b5d481d6a 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -116,7 +116,7 @@ def __init__( self.stub_desc = stub_desc or str(getattr(stub_object, "type", stub_object)) if runtime_desc is None: - runtime_sig = safe_inspect_signature(runtime_object, object_path) + runtime_sig = safe_inspect_signature(runtime_object) if runtime_sig is None: self.runtime_desc = _truncate(repr(runtime_object), 100) else: @@ -837,6 +837,7 @@ def from_overloadedfuncdef(stub: nodes.OverloadedFuncDef) -> Signature[nodes.Arg or arg.pos_only or assume_positional_only or arg.variable.name.strip("_") == "self" + or (index == 0 and arg.variable.name.strip("_") == "cls") else arg.variable.name ) all_args.setdefault(name, []).append((arg, index)) @@ -907,6 +908,7 @@ def _verify_signature( and not stub_arg.pos_only and not stub_arg.variable.name.startswith("__") and stub_arg.variable.name.strip("_") != "self" + and stub_arg.variable.name.strip("_") != "cls" and not is_dunder(function_name, exclude_special=True) # noisy for dunder methods ): yield ( @@ -917,6 +919,7 @@ def _verify_signature( runtime_arg.kind != inspect.Parameter.POSITIONAL_ONLY and (stub_arg.pos_only or stub_arg.variable.name.startswith("__")) and stub_arg.variable.name.strip("_") != "self" + and stub_arg.variable.name.strip("_") != "cls" and not is_dunder(function_name, exclude_special=True) # noisy for dunder methods ): yield ( @@ -1036,7 +1039,7 @@ def verify_funcitem( for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path): yield Error(object_path, "is inconsistent, " + message, stub, runtime) - signature = safe_inspect_signature(runtime, object_path) + signature = safe_inspect_signature(runtime) runtime_is_coroutine = inspect.iscoroutinefunction(runtime) if signature: @@ -1164,7 +1167,7 @@ def verify_overloadedfuncdef( # TODO: Should call _verify_final_method here, # but overloaded final methods in stubs cause a stubtest crash: see #14950 - signature = safe_inspect_signature(runtime, object_path) + signature = safe_inspect_signature(runtime) if not signature: return @@ -1526,9 +1529,7 @@ def is_read_only_property(runtime: object) -> bool: return isinstance(runtime, property) and runtime.fset is None -def safe_inspect_signature( - runtime: Any, object_path: list[str] | None = None -) -> inspect.Signature | None: +def safe_inspect_signature(runtime: Any) -> inspect.Signature | None: if ( hasattr(runtime, "__name__") and runtime.__name__ == "__init__" @@ -1539,38 +1540,48 @@ def safe_inspect_signature( and runtime.__objclass__.__text_signature__ is not None ): # This is an __init__ method with the generic C-class signature. - # In this case, the underlying class usually has a better signature, + # In this case, the underlying class often has a better signature, # which we can convert into an __init__ signature by adding in the # self parameter. - runtime_objclass = None - if runtime.__objclass__ is object: - # When __objclass__ is object, use the object_path to look up - # the actual class that this __init__ method came from. - if object_path: - module_name = ".".join(object_path[:-2]) - module = silent_import_module(module_name) - if module: - runtime_objclass = getattr(module, object_path[-2], None) - if not ( - runtime_objclass is not None - and hasattr(runtime_objclass, "__text_signature__") - and runtime_objclass.__text_signature__ is not None - ): - runtime_objclass = None - else: - runtime_objclass = runtime.__objclass__ + try: + s = inspect.signature(runtime.__objclass__) + + parameter_kind = inspect.Parameter.POSITIONAL_OR_KEYWORD + if s.parameters: + first_parameter = next(iter(s.parameters.values())) + if first_parameter.kind == inspect.Parameter.POSITIONAL_ONLY: + parameter_kind = inspect.Parameter.POSITIONAL_ONLY + return s.replace( + parameters=[inspect.Parameter("self", parameter_kind), *s.parameters.values()] + ) + except Exception: + pass - if runtime_objclass is not None: - try: - s = inspect.signature(runtime_objclass) - return s.replace( - parameters=[ - inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD), - *s.parameters.values(), - ] - ) - except Exception: - pass + if ( + hasattr(runtime, "__name__") + and runtime.__name__ == "__new__" + and hasattr(runtime, "__text_signature__") + and runtime.__text_signature__ == "($type, *args, **kwargs)" + and hasattr(runtime, "__self__") + and hasattr(runtime.__self__, "__text_signature__") + and runtime.__self__.__text_signature__ is not None + ): + # This is a __new__ method with the generic C-class signature. + # In this case, the underlying class often has a better signature, + # which we can convert into a __new__ signature by adding in the + # cls parameter. + try: + s = inspect.signature(runtime.__self__) + parameter_kind = inspect.Parameter.POSITIONAL_OR_KEYWORD + if s.parameters: + first_parameter = next(iter(s.parameters.values())) + if first_parameter.kind == inspect.Parameter.POSITIONAL_ONLY: + parameter_kind = inspect.Parameter.POSITIONAL_ONLY + return s.replace( + parameters=[inspect.Parameter("cls", parameter_kind), *s.parameters.values()] + ) + except Exception: + pass try: try: From b6b928572bb074c144a0256a5ab42e0068be09ee Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Sat, 7 Dec 2024 01:19:24 -0800 Subject: [PATCH 8/9] fix type check --- mypy/stubtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 8c2b5d481d6a..154f1bacfa6d 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1546,7 +1546,7 @@ def safe_inspect_signature(runtime: Any) -> inspect.Signature | None: try: s = inspect.signature(runtime.__objclass__) - parameter_kind = inspect.Parameter.POSITIONAL_OR_KEYWORD + parameter_kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD if s.parameters: first_parameter = next(iter(s.parameters.values())) if first_parameter.kind == inspect.Parameter.POSITIONAL_ONLY: From 988320100136e8638f4c8295b5b5acc19dddcae2 Mon Sep 17 00:00:00 2001 From: Stephen Morton Date: Sat, 7 Dec 2024 04:36:04 -0800 Subject: [PATCH 9/9] don't get signatures for __new__ if it'll duplicate an __init__ --- mypy/stubtest.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 154f1bacfa6d..d0b4fbd9d3c2 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1570,18 +1570,30 @@ def safe_inspect_signature(runtime: Any) -> inspect.Signature | None: # In this case, the underlying class often has a better signature, # which we can convert into a __new__ signature by adding in the # cls parameter. - try: - s = inspect.signature(runtime.__self__) - parameter_kind = inspect.Parameter.POSITIONAL_OR_KEYWORD - if s.parameters: - first_parameter = next(iter(s.parameters.values())) - if first_parameter.kind == inspect.Parameter.POSITIONAL_ONLY: - parameter_kind = inspect.Parameter.POSITIONAL_ONLY - return s.replace( - parameters=[inspect.Parameter("cls", parameter_kind), *s.parameters.values()] - ) - except Exception: - pass + + # If the attached class has a valid __init__, skip recovering a + # signature for this __new__ method. + has_init = False + if ( + hasattr(runtime.__self__, "__init__") + and hasattr(runtime.__self__.__init__, "__objclass__") + and runtime.__self__.__init__.__objclass__ is runtime.__self__ + ): + has_init = True + + if not has_init: + try: + s = inspect.signature(runtime.__self__) + parameter_kind = inspect.Parameter.POSITIONAL_OR_KEYWORD + if s.parameters: + first_parameter = next(iter(s.parameters.values())) + if first_parameter.kind == inspect.Parameter.POSITIONAL_ONLY: + parameter_kind = inspect.Parameter.POSITIONAL_ONLY + return s.replace( + parameters=[inspect.Parameter("cls", parameter_kind), *s.parameters.values()] + ) + except Exception: + pass try: try: