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: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy