From 1d6a5b1d2abf617b149e8bf8ff435f64dc507fd3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 18 Nov 2022 10:48:27 +0000 Subject: [PATCH 01/10] Fix daemon crash on malformed NamedTuple (#14119) Fixes #14098 Having invalid statements in a NamedTuple is almost like a syntax error, we can remove them after giving an error (without further analysis). This PR does almost exactly the same as https://github.com/python/mypy/pull/13963 did for TypedDicts. Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- mypy/nodes.py | 4 ++ mypy/semanal_namedtuple.py | 19 ++++-- mypy/semanal_typeddict.py | 2 + mypy/server/aststrip.py | 2 + test-data/unit/check-class-namedtuple.test | 2 - test-data/unit/fine-grained.test | 72 ++++++++++++++++++++++ 6 files changed, 94 insertions(+), 7 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index cf711c45f587..ebf2f5cb271a 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1068,6 +1068,7 @@ class ClassDef(Statement): "analyzed", "has_incompatible_baseclass", "deco_line", + "removed_statements", ) __match_args__ = ("name", "defs") @@ -1086,6 +1087,8 @@ class ClassDef(Statement): keywords: dict[str, Expression] analyzed: Expression | None has_incompatible_baseclass: bool + # Used by special forms like NamedTuple and TypedDict to store invalid statements + removed_statements: list[Statement] def __init__( self, @@ -1111,6 +1114,7 @@ def __init__( self.has_incompatible_baseclass = False # Used for error reporting (to keep backwad compatibility with pre-3.8) self.deco_line: int | None = None + self.removed_statements = [] def accept(self, visitor: StatementVisitor[T]) -> T: return visitor.visit_class_def(self) diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py index 04308db99e63..ec5f13d0fce0 100644 --- a/mypy/semanal_namedtuple.py +++ b/mypy/semanal_namedtuple.py @@ -32,6 +32,7 @@ NameExpr, PassStmt, RefExpr, + Statement, StrExpr, SymbolTable, SymbolTableNode, @@ -111,7 +112,7 @@ def analyze_namedtuple_classdef( if result is None: # This is a valid named tuple, but some types are incomplete. return True, None - items, types, default_items = result + items, types, default_items, statements = result if is_func_scope and "@" not in defn.name: defn.name += "@" + str(defn.line) existing_info = None @@ -123,6 +124,7 @@ def analyze_namedtuple_classdef( defn.analyzed = NamedTupleExpr(info, is_typed=True) defn.analyzed.line = defn.line defn.analyzed.column = defn.column + defn.defs.body = statements # All done: this is a valid named tuple with all types known. return True, info # This can't be a valid named tuple. @@ -130,24 +132,27 @@ def analyze_namedtuple_classdef( def check_namedtuple_classdef( self, defn: ClassDef, is_stub_file: bool - ) -> tuple[list[str], list[Type], dict[str, Expression]] | None: + ) -> tuple[list[str], list[Type], dict[str, Expression], list[Statement]] | None: """Parse and validate fields in named tuple class definition. - Return a three tuple: + Return a four tuple: * field names * field types * field default values + * valid statements or None, if any of the types are not ready. """ if self.options.python_version < (3, 6) and not is_stub_file: self.fail("NamedTuple class syntax is only supported in Python 3.6", defn) - return [], [], {} + return [], [], {}, [] if len(defn.base_type_exprs) > 1: self.fail("NamedTuple should be a single base", defn) items: list[str] = [] types: list[Type] = [] default_items: dict[str, Expression] = {} + statements: list[Statement] = [] for stmt in defn.defs.body: + statements.append(stmt) if not isinstance(stmt, AssignmentStmt): # Still allow pass or ... (for empty namedtuples). if isinstance(stmt, PassStmt) or ( @@ -160,9 +165,13 @@ def check_namedtuple_classdef( # And docstrings. if isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr): continue + statements.pop() + defn.removed_statements.append(stmt) self.fail(NAMEDTUP_CLASS_ERROR, stmt) elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr): # An assignment, but an invalid one. + statements.pop() + defn.removed_statements.append(stmt) self.fail(NAMEDTUP_CLASS_ERROR, stmt) else: # Append name and type in this case... @@ -199,7 +208,7 @@ def check_namedtuple_classdef( ) else: default_items[name] = stmt.rvalue - return items, types, default_items + return items, types, default_items, statements def check_namedtuple( self, node: Expression, var_name: str | None, is_func_scope: bool diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index e8be82bd41be..fb45dcc0dfc4 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -283,9 +283,11 @@ def analyze_typeddict_classdef_fields( ): statements.append(stmt) else: + defn.removed_statements.append(stmt) self.fail(TPDICT_CLASS_ERROR, stmt) elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr): # An assignment, but an invalid one. + defn.removed_statements.append(stmt) self.fail(TPDICT_CLASS_ERROR, stmt) else: name = stmt.lvalues[0].name diff --git a/mypy/server/aststrip.py b/mypy/server/aststrip.py index 1bfd820efb21..87ce63e9d543 100644 --- a/mypy/server/aststrip.py +++ b/mypy/server/aststrip.py @@ -140,6 +140,8 @@ def visit_class_def(self, node: ClassDef) -> None: ] with self.enter_class(node.info): super().visit_class_def(node) + node.defs.body.extend(node.removed_statements) + node.removed_statements = [] TypeState.reset_subtype_caches_for(node.info) # Kill the TypeInfo, since there is none before semantic analysis. node.info = CLASSDEF_NO_INFO diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test index 8e0545953bd8..8ae7f6555f9d 100644 --- a/test-data/unit/check-class-namedtuple.test +++ b/test-data/unit/check-class-namedtuple.test @@ -393,8 +393,6 @@ class X(typing.NamedTuple): [out] main:6: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" main:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" -main:7: error: Type cannot be declared in assignment to non-self attribute -main:7: error: "int" has no attribute "x" main:9: error: Non-default NamedTuple fields cannot follow default fields [builtins fixtures/list.pyi] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index a6d8f206fbba..c162f402486a 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -10205,3 +10205,75 @@ C [builtins fixtures/dict.pyi] [out] == + +[case testNamedTupleNestedCrash] +import m +[file m.py] +from typing import NamedTuple + +class NT(NamedTuple): + class C: ... + x: int + y: int + +[file m.py.2] +from typing import NamedTuple + +class NT(NamedTuple): + class C: ... + x: int + y: int +# change +[builtins fixtures/tuple.pyi] +[out] +m.py:4: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" +== +m.py:4: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" + +[case testNamedTupleNestedClassRecheck] +import n +[file n.py] +import m +x: m.NT +[file m.py] +from typing import NamedTuple +from f import A + +class NT(NamedTuple): + class C: ... + x: int + y: A + +[file f.py] +A = int +[file f.py.2] +A = str +[builtins fixtures/tuple.pyi] +[out] +m.py:5: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" +== +m.py:5: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" + +[case testTypedDictNestedClassRecheck] +import n +[file n.py] +import m +x: m.TD +[file m.py] +from typing_extensions import TypedDict +from f import A + +class TD(TypedDict): + class C: ... + x: int + y: A + +[file f.py] +A = int +[file f.py.2] +A = str +[builtins fixtures/dict.pyi] +[out] +m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type" +== +m.py:5: error: Invalid statement in TypedDict definition; expected "field_name: field_type" From a206096050d87db65aa8fcd3ab3f3d0dc2302036 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 18 Nov 2022 19:22:40 +0000 Subject: [PATCH 02/10] Enable lxml tests on Python 3.11 (#14134) Ref #12840 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 7fe486387f2f..399785ce4c1c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,7 @@ flake8==5.0.4 # must match version in .pre-commit-config.yaml flake8-bugbear==22.9.23 # must match version in .pre-commit-config.yaml flake8-noqa==1.2.9 # must match version in .pre-commit-config.yaml isort[colors]==5.10.1 # must match version in .pre-commit-config.yaml -lxml>=4.4.0; python_version<'3.11' +lxml>=4.9.1 psutil>=4.0 # pytest 6.2.3 does not support Python 3.10 pytest>=6.2.4 From 1cc4a7d38daac2aa641c9355a27820beba3542e1 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 18 Nov 2022 11:23:28 -0800 Subject: [PATCH 03/10] Revert ctypes patch on an ongoing basis (#14129) --- misc/sync-typeshed.py | 1 + 1 file changed, 1 insertion(+) diff --git a/misc/sync-typeshed.py b/misc/sync-typeshed.py index 878ffaa23bfb..8eeb9be7f4f8 100644 --- a/misc/sync-typeshed.py +++ b/misc/sync-typeshed.py @@ -187,6 +187,7 @@ def main() -> None: commits_to_cherry_pick = [ "780534b13722b7b0422178c049a1cbbf4ea4255b", # LiteralString reverts "5319fa34a8004c1568bb6f032a07b8b14cc95bed", # sum reverts + "0062994228fb62975c6cef4d2c80d00c7aa1c545", # ctypes reverts ] for commit in commits_to_cherry_pick: subprocess.run(["git", "cherry-pick", commit], check=True) From 05a3f7d8d61bc298809e5363d3a23aa16fe776d2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 18 Nov 2022 19:37:58 +0000 Subject: [PATCH 04/10] Correctly handle Enum name on Python 3.11 (#14133) Fixes #12483 Fixes https://github.com/python/typeshed/issues/7564 Ref #12841 The fix is straightforward. I can't use a unit test for this because there are some builtins fixtures that don't have tuple, so I can't do version check. --- mypy/semanal.py | 8 +++++++- test-data/unit/pythoneval.test | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 9b2b4ba44cce..538e37c030a9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1448,7 +1448,13 @@ def visit_decorator(self, dec: Decorator) -> None: dec.var.is_classmethod = True self.check_decorated_function_is_method("classmethod", dec) elif refers_to_fullname( - d, ("builtins.property", "abc.abstractproperty", "functools.cached_property") + d, + ( + "builtins.property", + "abc.abstractproperty", + "functools.cached_property", + "enum.property", + ), ): removed.append(i) dec.func.is_property = True diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index f6336b48ee7b..3f669246bb4e 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1712,3 +1712,26 @@ A = Type[int] | str B: TypeAlias = Type[int] | str [out] m.pyi:5: note: Revealed type is "typing._SpecialForm" + +[case testEnumNameWorkCorrectlyOn311] +# flags: --python-version 3.11 +import enum + +class E(enum.Enum): + X = 1 + Y = 2 + @enum.property + def foo(self) -> int: ... + +e: E +reveal_type(e.name) +reveal_type(e.value) +reveal_type(E.X.name) +reveal_type(e.foo) +reveal_type(E.Y.foo) +[out] +_testEnumNameWorkCorrectlyOn311.py:11: note: Revealed type is "builtins.str" +_testEnumNameWorkCorrectlyOn311.py:12: note: Revealed type is "Union[Literal[1]?, Literal[2]?]" +_testEnumNameWorkCorrectlyOn311.py:13: note: Revealed type is "Literal['X']?" +_testEnumNameWorkCorrectlyOn311.py:14: note: Revealed type is "builtins.int" +_testEnumNameWorkCorrectlyOn311.py:15: note: Revealed type is "builtins.int" From a2477ff0d0cb751f27a2b38d27ce6572ead03451 Mon Sep 17 00:00:00 2001 From: ChristianWitzler <57713653+ChristianWitzler@users.noreply.github.com> Date: Fri, 18 Nov 2022 21:00:08 +0100 Subject: [PATCH 05/10] Update code example in "Declaring decorators" (#14131) - Added missing cast import - Changed revealed type Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- docs/source/generics.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 59d4aa1a2dea..9a13e2a955c4 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -635,7 +635,7 @@ Before parameter specifications, here's how one might have annotated the decorat .. code-block:: python - from typing import Callable, TypeVar + from typing import Any, Callable, TypeVar, cast F = TypeVar('F', bound=Callable[..., Any]) @@ -650,8 +650,8 @@ and that would enable the following type checks: .. code-block:: python - reveal_type(a) # str - add_forty_two('x') # Type check error: incompatible type "str"; expected "int" + reveal_type(a) # Revealed type is "builtins.int" + add_forty_two('x') # Argument 1 to "add_forty_two" has incompatible type "str"; expected "int" Note that the ``wrapper()`` function is not type-checked. Wrapper From 6cd8e007923acef7a2899e85378bc4822472b848 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Nov 2022 02:00:33 +0000 Subject: [PATCH 06/10] Fix type query for recursive aliases (#14136) See https://github.com/python/mypy/pull/14130 for context. Btw it looks like these `Any` reports are quite broken in general. Some issues I found: * Many types are reported twice (even non-recursive) * Explicit `Any` in alias r.h.s are not counted (because of reckless `res = make_any_non_explicit(res)` in semanal.py) * For generic aliases we count their r.h.s. as containing `Any` from omitted generics I tried to fix these things, but it is not trivial, so maybe we can do it later in a separate PR. --- mypy/type_visitor.py | 24 ++++++++---------------- mypy/typeanal.py | 2 +- test-data/unit/reports.test | 24 +++++++++++++++++++++++- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index fe404cda0bec..0f5ac05e68ac 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -404,24 +404,16 @@ def visit_placeholder_type(self, t: PlaceholderType) -> T: return self.query_types(t.args) def visit_type_alias_type(self, t: TypeAliasType) -> T: + # Skip type aliases already visited types to avoid infinite recursion. + # TODO: Ideally we should fire subvisitors here (or use caching) if we care + # about duplicates. + if t in self.seen_aliases: + return self.strategy([]) + self.seen_aliases.add(t) if self.skip_alias_target: return self.query_types(t.args) return get_proper_type(t).accept(self) def query_types(self, types: Iterable[Type]) -> T: - """Perform a query for a list of types. - - Use the strategy to combine the results. - Skip type aliases already visited types to avoid infinite recursion. - """ - res: list[T] = [] - for t in types: - if isinstance(t, TypeAliasType): - # Avoid infinite recursion for recursive type aliases. - # TODO: Ideally we should fire subvisitors here (or use caching) if we care - # about duplicates. - if t in self.seen_aliases: - continue - self.seen_aliases.add(t) - res.append(t.accept(self)) - return self.strategy(res) + """Perform a query for a list of types using the strategy to combine the results.""" + return self.strategy([t.accept(self) for t in types]) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 18a63011c5bf..0dc1717d0724 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -450,7 +450,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ if fullname == "builtins.None": return NoneType() elif fullname == "typing.Any" or fullname == "builtins.Any": - return AnyType(TypeOfAny.explicit) + return AnyType(TypeOfAny.explicit, line=t.line, column=t.column) elif fullname in FINAL_TYPE_NAMES: self.fail( "Final can be only used as an outermost qualifier in a variable annotation", diff --git a/test-data/unit/reports.test b/test-data/unit/reports.test index a7ab6d754b2c..50dabb1fdea9 100644 --- a/test-data/unit/reports.test +++ b/test-data/unit/reports.test @@ -103,6 +103,28 @@ class A(object): +[case testNoCrashRecursiveAliasInReport] +# cmd: mypy --any-exprs-report report n.py + +[file n.py] +from typing import Union, List, Any, TypeVar + +Nested = List[Union[Any, Nested]] +T = TypeVar("T") +NestedGen = List[Union[T, NestedGen[T]]] + +x: Nested +y: NestedGen[int] +z: NestedGen[Any] + +[file report/any-exprs.txt] +[outfile report/types-of-anys.txt] + Name Unannotated Explicit Unimported Omitted Generics Error Special Form Implementation Artifact +----------------------------------------------------------------------------------------------------------------- + n 0 4 0 8 0 0 0 +----------------------------------------------------------------------------------------------------------------- +Total 0 4 0 8 0 0 0 + [case testTypeVarTreatedAsEmptyLine] # cmd: mypy --html-report report n.py @@ -480,7 +502,7 @@ namespace_packages = True -

folder.subfolder.something

+

folder.subfolder.something

From 56e9396e681d779cbb593d82d21c1fae76c6f430 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 18 Nov 2022 20:10:54 -0800 Subject: [PATCH 07/10] Make non-numeric non-empty FORCE_COLOR truthy (#14140) Fixes #14139 --- mypy/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/util.py b/mypy/util.py index e836aefb3c7e..04ed616ade07 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -520,7 +520,11 @@ def parse_gray_color(cup: bytes) -> str: def should_force_color() -> bool: - return bool(int(os.getenv("MYPY_FORCE_COLOR", os.getenv("FORCE_COLOR", "0")))) + env_var = os.getenv("MYPY_FORCE_COLOR", os.getenv("FORCE_COLOR", "0")) + try: + return bool(int(env_var)) + except ValueError: + return bool(env_var) class FancyFormatter: From e814c47a1950dba765207333cf91a61a2d9188ee Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Nov 2022 15:07:24 +0000 Subject: [PATCH 08/10] Fix incremental crash on generic function appearing in nested position (#14148) Fixes #14137 Fix is trivial, I just forgot to call `super()` in one of my previous PRs. --- mypy/checkmember.py | 1 + test-data/unit/check-incremental.test | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 08d4ff412e4e..554b49d3eda2 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -806,6 +806,7 @@ class FreezeTypeVarsVisitor(TypeTraverserVisitor): def visit_callable_type(self, t: CallableType) -> None: for v in t.variables: v.id.meta_level = 0 + super().visit_callable_type(t) def lookup_member_var_or_accessor(info: TypeInfo, name: str, is_lvalue: bool) -> SymbolNode | None: diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 5fca0f55a0d6..e5b69fb6fb9d 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6334,3 +6334,17 @@ reveal_type(D().meth) [out2] tmp/m.py:4: note: Revealed type is "def [Self <: lib.C] (self: Self`0, other: Self`0) -> Self`0" tmp/m.py:5: note: Revealed type is "def (other: m.D) -> m.D" + +[case testIncrementalNestedGenericCallableCrash] +from typing import TypeVar, Callable + +T = TypeVar("T") + +class B: + def foo(self) -> Callable[[T], T]: ... + +class C(B): + def __init__(self) -> None: + self.x = self.foo() +[out] +[out2] From f8d71f13d408198f81d55a6b57bf1d2c1b81a3c2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 19 Nov 2022 22:54:24 +0000 Subject: [PATCH 09/10] Make is_recursive and has_recursive_types() more consistent (#14147) While working on another PR I noticed that current behavior of `has_recursive_types()` is inconsistent, it returns `False` is there is a recursive type nested as an argument to a generic non-recursive alias. I wasn't able to find any situation where this actually matters, but I think it is better if this function behaves consistently. --- mypy/test/testtypes.py | 7 +++++++ mypy/test/typefixture.py | 10 +++++++--- mypy/types.py | 26 +++++++++++++++++++------- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 31bdd6690a7a..18948ee7f6d6 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -31,6 +31,7 @@ UninhabitedType, UnionType, get_proper_type, + has_recursive_types, ) @@ -157,6 +158,12 @@ def test_type_alias_expand_all(self) -> None: [self.fx.a, self.fx.a], Instance(self.fx.std_tuplei, [self.fx.a]) ) + def test_recursive_nested_in_non_recursive(self) -> None: + A, _ = self.fx.def_alias_1(self.fx.a) + NA = self.fx.non_rec_alias(Instance(self.fx.gi, [UnboundType("T")]), ["T"], [A]) + assert not NA.is_recursive + assert has_recursive_types(NA) + def test_indirection_no_infinite_recursion(self) -> None: A, _ = self.fx.def_alias_1(self.fx.a) visitor = TypeIndirectionVisitor() diff --git a/mypy/test/typefixture.py b/mypy/test/typefixture.py index 380da909893a..93e5e4b0b5ca 100644 --- a/mypy/test/typefixture.py +++ b/mypy/test/typefixture.py @@ -339,9 +339,13 @@ def def_alias_2(self, base: Instance) -> tuple[TypeAliasType, Type]: A.alias = AN return A, target - def non_rec_alias(self, target: Type) -> TypeAliasType: - AN = TypeAlias(target, "__main__.A", -1, -1) - return TypeAliasType(AN, []) + def non_rec_alias( + self, target: Type, alias_tvars: list[str] | None = None, args: list[Type] | None = None + ) -> TypeAliasType: + AN = TypeAlias(target, "__main__.A", -1, -1, alias_tvars=alias_tvars) + if args is None: + args = [] + return TypeAliasType(AN, args) class InterfaceTypeFixture(TypeFixture): diff --git a/mypy/types.py b/mypy/types.py index 1de294f9952d..6c08b24afd80 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -278,30 +278,42 @@ def _expand_once(self) -> Type: self.alias.target, self.alias.alias_tvars, self.args, self.line, self.column ) - def _partial_expansion(self) -> tuple[ProperType, bool]: + def _partial_expansion(self, nothing_args: bool = False) -> tuple[ProperType, bool]: # Private method mostly for debugging and testing. unroller = UnrollAliasVisitor(set()) - unrolled = self.accept(unroller) + if nothing_args: + alias = self.copy_modified(args=[UninhabitedType()] * len(self.args)) + else: + alias = self + unrolled = alias.accept(unroller) assert isinstance(unrolled, ProperType) return unrolled, unroller.recursed - def expand_all_if_possible(self) -> ProperType | None: + def expand_all_if_possible(self, nothing_args: bool = False) -> ProperType | None: """Attempt a full expansion of the type alias (including nested aliases). If the expansion is not possible, i.e. the alias is (mutually-)recursive, - return None. + return None. If nothing_args is True, replace all type arguments with an + UninhabitedType() (used to detect recursively defined aliases). """ - unrolled, recursed = self._partial_expansion() + unrolled, recursed = self._partial_expansion(nothing_args=nothing_args) if recursed: return None return unrolled @property def is_recursive(self) -> bool: + """Whether this type alias is recursive. + + Note this doesn't check generic alias arguments, but only if this alias + *definition* is recursive. The property value thus can be cached on the + underlying TypeAlias node. If you want to include all nested types, use + has_recursive_types() function. + """ assert self.alias is not None, "Unfixed type alias" is_recursive = self.alias._is_recursive if is_recursive is None: - is_recursive = self.expand_all_if_possible() is None + is_recursive = self.expand_all_if_possible(nothing_args=True) is None # We cache the value on the underlying TypeAlias node as an optimization, # since the value is the same for all instances of the same alias. self.alias._is_recursive = is_recursive @@ -3259,7 +3271,7 @@ def __init__(self) -> None: super().__init__(any) def visit_type_alias_type(self, t: TypeAliasType) -> bool: - return t.is_recursive + return t.is_recursive or self.query_types(t.args) def has_recursive_types(typ: Type) -> bool: From c660354846688ff8158d0f0178eb298171b74f5b Mon Sep 17 00:00:00 2001 From: Tom Schraitle Date: Mon, 21 Nov 2022 09:00:36 +0100 Subject: [PATCH 10/10] Avoid use of implicit optional in decorator factory docs (#14156) --- docs/source/generics.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 9a13e2a955c4..a5c7b8accaa8 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -724,7 +724,7 @@ achieved by combining with :py:func:`@overload `: .. code-block:: python - from typing import Any, Callable, TypeVar, overload + from typing import Any, Callable, Optional, TypeVar, overload F = TypeVar('F', bound=Callable[..., Any]) @@ -736,7 +736,7 @@ achieved by combining with :py:func:`@overload `: def atomic(*, savepoint: bool = True) -> Callable[[F], F]: ... # Implementation - def atomic(__func: Callable[..., Any] = None, *, savepoint: bool = True): + def atomic(__func: Optional[Callable[..., Any]] = None, *, savepoint: bool = True): def decorator(func: Callable[..., Any]): ... # Code goes here if __func is not None:
folder/subfolder/something.py