From 3f8c6cbf2d1d174065971d478e61144ed9c7a225 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sat, 10 Aug 2024 13:33:11 -0700 Subject: [PATCH 1/7] Resolve TypeVar upper bounds in functools.partial (#17660) Mostly fixes #17646 --- mypy/checker.py | 2 ++ test-data/unit/check-functools.test | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 82633a5b6c0e..db65660bbfbd 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -683,6 +683,8 @@ def extract_callable_type(self, inner_type: Type | None, ctx: Context) -> Callab inner_type = get_proper_type(inner_type) outer_type: CallableType | None = None if inner_type is not None and not isinstance(inner_type, AnyType): + if isinstance(inner_type, TypeVarLikeType): + inner_type = get_proper_type(inner_type.upper_bound) if isinstance(inner_type, TypeType): if isinstance(inner_type.item, Instance): inner_type = expand_type_by_instance( diff --git a/test-data/unit/check-functools.test b/test-data/unit/check-functools.test index 710d3e66dfad..9f8fbd42440b 100644 --- a/test-data/unit/check-functools.test +++ b/test-data/unit/check-functools.test @@ -541,3 +541,19 @@ p(1, "no") # E: Argument 2 to "A" has incompatible type "str"; expected "int" q: partial[A] = partial(A, 1) # OK [builtins fixtures/tuple.pyi] + +[case testFunctoolsPartialTypeVarBound] +from typing import Callable, TypeVar, Type +import functools + +T = TypeVar("T", bound=Callable[[str, int], str]) +S = TypeVar("S", bound=Type[int]) + +def foo(f: T) -> T: + g = functools.partial(f, "foo") + return f + +def bar(f: S) -> S: + g = functools.partial(f, "foo") + return f +[builtins fixtures/primitives.pyi] From 2632ea363191502b9ddb10553c7fedd553737b06 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sun, 11 Aug 2024 00:46:20 +0200 Subject: [PATCH 2/7] stubtest: Add support for cached_property (#17626) Fixes #17625 --- mypy/stubtest.py | 3 ++ mypy/test/teststubtest.py | 100 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index ca17ccfe2f5b..c54f83f33b00 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1224,6 +1224,9 @@ def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[s if isinstance(runtime, property): yield from _verify_final_method(stub.func, runtime.fget, MISSING) return + if isinstance(runtime, functools.cached_property): + yield from _verify_final_method(stub.func, runtime.func, MISSING) + return if inspect.isdatadescriptor(runtime): # It's enough like a property... return diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index c10c683ffdac..c6e92258e7f4 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -893,6 +893,106 @@ class FineAndDandy: error=None, ) + @collect_cases + def test_cached_property(self) -> Iterator[Case]: + yield Case( + stub=""" + from functools import cached_property + class Good: + @cached_property + def read_only_attr(self) -> int: ... + @cached_property + def read_only_attr2(self) -> int: ... + """, + runtime=""" + import functools as ft + from functools import cached_property + class Good: + @cached_property + def read_only_attr(self): return 1 + @ft.cached_property + def read_only_attr2(self): return 1 + """, + error=None, + ) + yield Case( + stub=""" + from functools import cached_property + class Bad: + @cached_property + def f(self) -> int: ... + """, + runtime=""" + class Bad: + def f(self) -> int: return 1 + """, + error="Bad.f", + ) + yield Case( + stub=""" + from functools import cached_property + class GoodCachedAttr: + @cached_property + def f(self) -> int: ... + """, + runtime=""" + class GoodCachedAttr: + f = 1 + """, + error=None, + ) + yield Case( + stub=""" + from functools import cached_property + class BadCachedAttr: + @cached_property + def f(self) -> str: ... + """, + runtime=""" + class BadCachedAttr: + f = 1 + """, + error="BadCachedAttr.f", + ) + yield Case( + stub=""" + from functools import cached_property + from typing import final + class FinalGood: + @cached_property + @final + def attr(self) -> int: ... + """, + runtime=""" + from functools import cached_property + from typing import final + class FinalGood: + @cached_property + @final + def attr(self): + return 1 + """, + error=None, + ) + yield Case( + stub=""" + from functools import cached_property + class FinalBad: + @cached_property + def attr(self) -> int: ... + """, + runtime=""" + from functools import cached_property + from typing_extensions import final + class FinalBad: + @cached_property + @final + def attr(self): + return 1 + """, + error="FinalBad.attr", + ) + @collect_cases def test_var(self) -> Iterator[Case]: yield Case(stub="x1: int", runtime="x1 = 5", error=None) From 3da16bd6004ec9685c476f77526abd36e7632813 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 11 Aug 2024 22:26:32 +0100 Subject: [PATCH 3/7] An alternative fix for a union-like literal string (#17639) It is unfortunate to add two extra slots to a common type (and I guess this is why it was rejected in the original PR), but all other alternatives I tried are hacky and/or dangerous. So, this is a price to pay for introducing a new type syntax. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- mypy/fastparse.py | 4 +--- mypy/typeanal.py | 6 +++++- mypy/types.py | 16 +++++++++++++--- test-data/unit/check-literal.test | 4 ++++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index ab7eef924bd1..abcce74c6064 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -331,12 +331,10 @@ def parse_type_string( """ try: _, node = parse_type_comment(f"({expr_string})", line=line, column=column, errors=None) - if isinstance(node, UnboundType) and node.original_str_expr is None: + if isinstance(node, (UnboundType, UnionType)) and node.original_str_expr is None: node.original_str_expr = expr_string node.original_str_fallback = expr_fallback_name return node - elif isinstance(node, UnionType): - return node else: return RawExpressionType(expr_string, expr_fallback_name, line, column) except (SyntaxError, ValueError): diff --git a/mypy/typeanal.py b/mypy/typeanal.py index f88c3f91d1c6..274b4b893a98 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1615,7 +1615,11 @@ def analyze_literal_type(self, t: UnboundType) -> Type: def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type] | None: # This UnboundType was originally defined as a string. - if isinstance(arg, UnboundType) and arg.original_str_expr is not None: + if ( + isinstance(arg, ProperType) + and isinstance(arg, (UnboundType, UnionType)) + and arg.original_str_expr is not None + ): assert arg.original_str_fallback is not None return [ LiteralType( diff --git a/mypy/types.py b/mypy/types.py index 72374c1555c9..618228c361f3 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -914,7 +914,7 @@ class UnboundType(ProperType): def __init__( self, - name: str | None, + name: str, args: Sequence[Type] | None = None, line: int = -1, column: int = -1, @@ -926,7 +926,6 @@ def __init__( super().__init__(line, column) if not args: args = [] - assert name is not None self.name = name self.args = tuple(args) # Should this type be wrapped in an Optional? @@ -2849,7 +2848,13 @@ def is_singleton_type(self) -> bool: class UnionType(ProperType): """The union type Union[T1, ..., Tn] (at least one type argument).""" - __slots__ = ("items", "is_evaluated", "uses_pep604_syntax") + __slots__ = ( + "items", + "is_evaluated", + "uses_pep604_syntax", + "original_str_expr", + "original_str_fallback", + ) def __init__( self, @@ -2868,6 +2873,11 @@ def __init__( self.is_evaluated = is_evaluated # uses_pep604_syntax is True if Union uses OR syntax (X | Y) self.uses_pep604_syntax = uses_pep604_syntax + # The meaning of these two is the same as for UnboundType. A UnionType can be + # return by type parser from a string "A|B", and we need to be able to fall back + # to plain string, when such a string appears inside a Literal[...]. + self.original_str_expr: str | None = None + self.original_str_fallback: str | None = None def can_be_true_default(self) -> bool: return any(item.can_be_true for item in self.items) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index e8d942f5d74d..6d76ce176aaf 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -12,8 +12,12 @@ reveal_type(g1) # N: Revealed type is "def (x: Literal['A['])" def f2(x: 'A B') -> None: pass # E: Invalid type comment or annotation def g2(x: Literal['A B']) -> None: pass +def h2(x: 'A|int') -> None: pass # E: Name "A" is not defined +def i2(x: Literal['A|B']) -> None: pass reveal_type(f2) # N: Revealed type is "def (x: Any)" reveal_type(g2) # N: Revealed type is "def (x: Literal['A B'])" +reveal_type(h2) # N: Revealed type is "def (x: Union[Any, builtins.int])" +reveal_type(i2) # N: Revealed type is "def (x: Literal['A|B'])" [builtins fixtures/tuple.pyi] [out] From f0c394a8b9ed44ef9830ac3a7b2d13df2f60f084 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 12 Aug 2024 19:21:50 +0300 Subject: [PATCH 4/7] Stubgen add `--version` (#17662) --- mypy/stubgen.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 4e6e3efc000b..f4a418838161 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -54,6 +54,7 @@ import mypy.parse import mypy.traverser import mypy.util +import mypy.version from mypy.build import build from mypy.errors import CompileError, Errors from mypy.find_sources import InvalidSourceList, create_source_list @@ -1847,6 +1848,9 @@ def parse_options(args: list[str]) -> Options: dest="files", help="generate stubs for given files or directories", ) + parser.add_argument( + "--version", action="version", version="%(prog)s " + mypy.version.__version__ + ) ns = parser.parse_args(args) From 5b46f1c4baeb6271cbf3fc4ab57f8d78b0232e3f Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 12 Aug 2024 19:40:32 +0300 Subject: [PATCH 5/7] [stubgen] Fix crash on literal class-level keywords (#17663) Closes https://github.com/python/mypy/issues/17661 --- mypy/stubgen.py | 22 ++++++++++++++++++++-- test-data/unit/stubgen.test | 10 ++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index f4a418838161..14417f55545e 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -305,9 +305,26 @@ def visit_name_expr(self, node: NameExpr) -> str: def visit_member_expr(self, o: MemberExpr) -> str: return self._visit_ref_expr(o) - def visit_str_expr(self, node: StrExpr) -> str: + def _visit_literal_node( + self, node: StrExpr | BytesExpr | IntExpr | FloatExpr | ComplexExpr + ) -> str: return repr(node.value) + def visit_str_expr(self, node: StrExpr) -> str: + return self._visit_literal_node(node) + + def visit_bytes_expr(self, node: BytesExpr) -> str: + return f"b{self._visit_literal_node(node)}" + + def visit_int_expr(self, node: IntExpr) -> str: + return self._visit_literal_node(node) + + def visit_float_expr(self, node: FloatExpr) -> str: + return self._visit_literal_node(node) + + def visit_complex_expr(self, node: ComplexExpr) -> str: + return self._visit_literal_node(node) + def visit_index_expr(self, node: IndexExpr) -> str: base_fullname = self.stubgen.get_fullname(node.base) if base_fullname == "typing.Union": @@ -805,7 +822,8 @@ def get_base_types(self, cdef: ClassDef) -> list[str]: for name, value in cdef.keywords.items(): if name == "metaclass": continue # handled separately - base_types.append(f"{name}={value.accept(p)}") + processed_value = value.accept(p) or "..." # at least, don't crash + base_types.append(f"{name}={processed_value}") return base_types def get_class_decorators(self, cdef: ClassDef) -> list[str]: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 94d0edb2ae37..fe0538159aa3 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -4405,3 +4405,13 @@ Y = int | None Z = Incomplete W = int | str | None R = type[int | str] | None + +[case testClassInheritanceWithKeywordsConstants] +class Test(Whatever, a=1, b='b', c=True, d=1.5, e=None, f=1j, g=b'123'): ... +[out] +class Test(Whatever, a=1, b='b', c=True, d=1.5, e=None, f=1j, g=b'123'): ... + +[case testClassInheritanceWithKeywordsDynamic] +class Test(Whatever, keyword=SomeName * 2, attr=SomeName.attr): ... +[out] +class Test(Whatever, keyword=SomeName * 2, attr=SomeName.attr): ... From 8332c6e7992fa9655b79ef55c410a805ad1e6d34 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 14 Aug 2024 10:08:43 +0300 Subject: [PATCH 6/7] Add optional stderr result to `run_stubtest` in tests (#17636) This adds a test for incorrect error code in `stubtest`'s config file. Refs https://github.com/python/mypy/issues/17628 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jelle Zijlstra --- mypy/test/teststubtest.py | 42 +++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index c6e92258e7f4..70687b499651 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -144,9 +144,9 @@ def __invert__(self: _T) -> _T: pass """ -def run_stubtest( +def run_stubtest_with_stderr( stub: str, runtime: str, options: list[str], config_file: str | None = None -) -> str: +) -> tuple[str, str]: with use_tmp_dir(TEST_MODULE_NAME) as tmp_dir: with open("builtins.pyi", "w") as f: f.write(stubtest_builtins_stub) @@ -163,13 +163,26 @@ def run_stubtest( f.write(config_file) options = options + ["--mypy-config-file", f"{TEST_MODULE_NAME}_config.ini"] output = io.StringIO() - with contextlib.redirect_stdout(output): + outerr = io.StringIO() + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(outerr): test_stubs(parse_options([TEST_MODULE_NAME] + options), use_builtins_fixtures=True) - return remove_color_code( - output.getvalue() - # remove cwd as it's not available from outside - .replace(os.path.realpath(tmp_dir) + os.sep, "").replace(tmp_dir + os.sep, "") - ) + filtered_output = remove_color_code( + output.getvalue() + # remove cwd as it's not available from outside + .replace(os.path.realpath(tmp_dir) + os.sep, "").replace(tmp_dir + os.sep, "") + ) + filtered_outerr = remove_color_code( + outerr.getvalue() + # remove cwd as it's not available from outside + .replace(os.path.realpath(tmp_dir) + os.sep, "").replace(tmp_dir + os.sep, "") + ) + return filtered_output, filtered_outerr + + +def run_stubtest( + stub: str, runtime: str, options: list[str], config_file: str | None = None +) -> str: + return run_stubtest_with_stderr(stub, runtime, options, config_file)[0] class Case: @@ -2590,6 +2603,19 @@ def test_config_file_error_codes(self) -> None: output = run_stubtest(stub=stub, runtime=runtime, options=[], config_file=config_file) assert output == "Success: no issues found in 1 module\n" + def test_config_file_error_codes_invalid(self) -> None: + runtime = "temp = 5\n" + stub = "temp: int\n" + config_file = "[mypy]\ndisable_error_code = not-a-valid-name\n" + output, outerr = run_stubtest_with_stderr( + stub=stub, runtime=runtime, options=[], config_file=config_file + ) + assert output == "Success: no issues found in 1 module\n" + assert outerr == ( + "test_module_config.ini: [mypy]: disable_error_code: " + "Invalid error code(s): not-a-valid-name\n" + ) + def test_config_file_wrong_incomplete_feature(self) -> None: runtime = "x = 1\n" stub = "x: int\n" From fe15ee69b9225f808f8ed735671b73c31ae1bed8 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 14 Aug 2024 22:34:18 +0100 Subject: [PATCH 7/7] [mypyc] Avoid uses of _PyObject_CallMethodOneArg on 3.13 (#17526) It's no longer available in CPython 3.13 betas. This fixes some dict primitives on 3.13. Work on mypyc/mypyc#1056. --- mypyc/lib-rt/dict_ops.c | 12 ++++++++++-- mypyc/lib-rt/pythonsupport.h | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/mypyc/lib-rt/dict_ops.c b/mypyc/lib-rt/dict_ops.c index 031df8f63c49..b33233521afd 100644 --- a/mypyc/lib-rt/dict_ops.c +++ b/mypyc/lib-rt/dict_ops.c @@ -78,7 +78,11 @@ PyObject *CPyDict_SetDefault(PyObject *dict, PyObject *key, PyObject *value) { return ret; } _Py_IDENTIFIER(setdefault); - return _PyObject_CallMethodIdObjArgs(dict, &PyId_setdefault, key, value, NULL); + PyObject *name = _PyUnicode_FromId(&PyId_setdefault); /* borrowed */ + if (name == NULL) { + return NULL; + } + return PyObject_CallMethodObjArgs(dict, name, key, value, NULL); } PyObject *CPyDict_SetDefaultWithNone(PyObject *dict, PyObject *key) { @@ -133,7 +137,11 @@ static inline int CPy_ObjectToStatus(PyObject *obj) { static int CPyDict_UpdateGeneral(PyObject *dict, PyObject *stuff) { _Py_IDENTIFIER(update); - PyObject *res = _PyObject_CallMethodIdOneArg(dict, &PyId_update, stuff); + PyObject *name = _PyUnicode_FromId(&PyId_update); /* borrowed */ + if (name == NULL) { + return -1; + } + PyObject *res = PyObject_CallMethodOneArg(dict, name, stuff); return CPy_ObjectToStatus(res); } diff --git a/mypyc/lib-rt/pythonsupport.h b/mypyc/lib-rt/pythonsupport.h index 8edc9abcf9f8..bf7e5203758d 100644 --- a/mypyc/lib-rt/pythonsupport.h +++ b/mypyc/lib-rt/pythonsupport.h @@ -401,6 +401,8 @@ _CPyObject_HasAttrId(PyObject *v, _Py_Identifier *name) { _PyObject_CallMethodIdObjArgs((self), (name), NULL) #define _PyObject_CallMethodIdOneArg(self, name, arg) \ _PyObject_CallMethodIdObjArgs((self), (name), (arg), NULL) +#define PyObject_CallMethodOneArg(self, name, arg) \ + PyObject_CallMethodObjArgs((self), (name), (arg), NULL) #endif #if CPY_3_13_FEATURES