Skip to content

Commit 1cd9216

Browse files
Merge pull request #1 from python/master
Fix daemon crash on malformed NamedTuple (python#14119)
2 parents 15c37df + c660354 commit 1cd9216

20 files changed

+213
-40
lines changed

docs/source/generics.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ achieved by combining with :py:func:`@overload <typing.overload>`:
724724

725725
.. code-block:: python
726726
727-
from typing import Any, Callable, TypeVar, overload
727+
from typing import Any, Callable, Optional, TypeVar, overload
728728
729729
F = TypeVar('F', bound=Callable[..., Any])
730730
@@ -736,7 +736,7 @@ achieved by combining with :py:func:`@overload <typing.overload>`:
736736
def atomic(*, savepoint: bool = True) -> Callable[[F], F]: ...
737737
738738
# Implementation
739-
def atomic(__func: Callable[..., Any] = None, *, savepoint: bool = True):
739+
def atomic(__func: Optional[Callable[..., Any]] = None, *, savepoint: bool = True):
740740
def decorator(func: Callable[..., Any]):
741741
... # Code goes here
742742
if __func is not None:

misc/sync-typeshed.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ def main() -> None:
187187
commits_to_cherry_pick = [
188188
"780534b13722b7b0422178c049a1cbbf4ea4255b", # LiteralString reverts
189189
"5319fa34a8004c1568bb6f032a07b8b14cc95bed", # sum reverts
190+
"0062994228fb62975c6cef4d2c80d00c7aa1c545", # ctypes reverts
190191
]
191192
for commit in commits_to_cherry_pick:
192193
subprocess.run(["git", "cherry-pick", commit], check=True)

mypy/checkmember.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@ class FreezeTypeVarsVisitor(TypeTraverserVisitor):
806806
def visit_callable_type(self, t: CallableType) -> None:
807807
for v in t.variables:
808808
v.id.meta_level = 0
809+
super().visit_callable_type(t)
809810

810811

811812
def lookup_member_var_or_accessor(info: TypeInfo, name: str, is_lvalue: bool) -> SymbolNode | None:

mypy/nodes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,7 @@ class ClassDef(Statement):
10681068
"analyzed",
10691069
"has_incompatible_baseclass",
10701070
"deco_line",
1071+
"removed_statements",
10711072
)
10721073

10731074
__match_args__ = ("name", "defs")
@@ -1086,6 +1087,8 @@ class ClassDef(Statement):
10861087
keywords: dict[str, Expression]
10871088
analyzed: Expression | None
10881089
has_incompatible_baseclass: bool
1090+
# Used by special forms like NamedTuple and TypedDict to store invalid statements
1091+
removed_statements: list[Statement]
10891092

10901093
def __init__(
10911094
self,
@@ -1111,6 +1114,7 @@ def __init__(
11111114
self.has_incompatible_baseclass = False
11121115
# Used for error reporting (to keep backwad compatibility with pre-3.8)
11131116
self.deco_line: int | None = None
1117+
self.removed_statements = []
11141118

11151119
def accept(self, visitor: StatementVisitor[T]) -> T:
11161120
return visitor.visit_class_def(self)

mypy/semanal.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1448,7 +1448,13 @@ def visit_decorator(self, dec: Decorator) -> None:
14481448
dec.var.is_classmethod = True
14491449
self.check_decorated_function_is_method("classmethod", dec)
14501450
elif refers_to_fullname(
1451-
d, ("builtins.property", "abc.abstractproperty", "functools.cached_property")
1451+
d,
1452+
(
1453+
"builtins.property",
1454+
"abc.abstractproperty",
1455+
"functools.cached_property",
1456+
"enum.property",
1457+
),
14521458
):
14531459
removed.append(i)
14541460
dec.func.is_property = True

mypy/semanal_namedtuple.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
NameExpr,
3333
PassStmt,
3434
RefExpr,
35+
Statement,
3536
StrExpr,
3637
SymbolTable,
3738
SymbolTableNode,
@@ -111,7 +112,7 @@ def analyze_namedtuple_classdef(
111112
if result is None:
112113
# This is a valid named tuple, but some types are incomplete.
113114
return True, None
114-
items, types, default_items = result
115+
items, types, default_items, statements = result
115116
if is_func_scope and "@" not in defn.name:
116117
defn.name += "@" + str(defn.line)
117118
existing_info = None
@@ -123,31 +124,35 @@ def analyze_namedtuple_classdef(
123124
defn.analyzed = NamedTupleExpr(info, is_typed=True)
124125
defn.analyzed.line = defn.line
125126
defn.analyzed.column = defn.column
127+
defn.defs.body = statements
126128
# All done: this is a valid named tuple with all types known.
127129
return True, info
128130
# This can't be a valid named tuple.
129131
return False, None
130132

131133
def check_namedtuple_classdef(
132134
self, defn: ClassDef, is_stub_file: bool
133-
) -> tuple[list[str], list[Type], dict[str, Expression]] | None:
135+
) -> tuple[list[str], list[Type], dict[str, Expression], list[Statement]] | None:
134136
"""Parse and validate fields in named tuple class definition.
135137
136-
Return a three tuple:
138+
Return a four tuple:
137139
* field names
138140
* field types
139141
* field default values
142+
* valid statements
140143
or None, if any of the types are not ready.
141144
"""
142145
if self.options.python_version < (3, 6) and not is_stub_file:
143146
self.fail("NamedTuple class syntax is only supported in Python 3.6", defn)
144-
return [], [], {}
147+
return [], [], {}, []
145148
if len(defn.base_type_exprs) > 1:
146149
self.fail("NamedTuple should be a single base", defn)
147150
items: list[str] = []
148151
types: list[Type] = []
149152
default_items: dict[str, Expression] = {}
153+
statements: list[Statement] = []
150154
for stmt in defn.defs.body:
155+
statements.append(stmt)
151156
if not isinstance(stmt, AssignmentStmt):
152157
# Still allow pass or ... (for empty namedtuples).
153158
if isinstance(stmt, PassStmt) or (
@@ -160,9 +165,13 @@ def check_namedtuple_classdef(
160165
# And docstrings.
161166
if isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr):
162167
continue
168+
statements.pop()
169+
defn.removed_statements.append(stmt)
163170
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
164171
elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
165172
# An assignment, but an invalid one.
173+
statements.pop()
174+
defn.removed_statements.append(stmt)
166175
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
167176
else:
168177
# Append name and type in this case...
@@ -199,7 +208,7 @@ def check_namedtuple_classdef(
199208
)
200209
else:
201210
default_items[name] = stmt.rvalue
202-
return items, types, default_items
211+
return items, types, default_items, statements
203212

204213
def check_namedtuple(
205214
self, node: Expression, var_name: str | None, is_func_scope: bool

mypy/semanal_typeddict.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,11 @@ def analyze_typeddict_classdef_fields(
283283
):
284284
statements.append(stmt)
285285
else:
286+
defn.removed_statements.append(stmt)
286287
self.fail(TPDICT_CLASS_ERROR, stmt)
287288
elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
288289
# An assignment, but an invalid one.
290+
defn.removed_statements.append(stmt)
289291
self.fail(TPDICT_CLASS_ERROR, stmt)
290292
else:
291293
name = stmt.lvalues[0].name

mypy/server/aststrip.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ def visit_class_def(self, node: ClassDef) -> None:
140140
]
141141
with self.enter_class(node.info):
142142
super().visit_class_def(node)
143+
node.defs.body.extend(node.removed_statements)
144+
node.removed_statements = []
143145
TypeState.reset_subtype_caches_for(node.info)
144146
# Kill the TypeInfo, since there is none before semantic analysis.
145147
node.info = CLASSDEF_NO_INFO

mypy/test/testtypes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
UninhabitedType,
3232
UnionType,
3333
get_proper_type,
34+
has_recursive_types,
3435
)
3536

3637

@@ -157,6 +158,12 @@ def test_type_alias_expand_all(self) -> None:
157158
[self.fx.a, self.fx.a], Instance(self.fx.std_tuplei, [self.fx.a])
158159
)
159160

161+
def test_recursive_nested_in_non_recursive(self) -> None:
162+
A, _ = self.fx.def_alias_1(self.fx.a)
163+
NA = self.fx.non_rec_alias(Instance(self.fx.gi, [UnboundType("T")]), ["T"], [A])
164+
assert not NA.is_recursive
165+
assert has_recursive_types(NA)
166+
160167
def test_indirection_no_infinite_recursion(self) -> None:
161168
A, _ = self.fx.def_alias_1(self.fx.a)
162169
visitor = TypeIndirectionVisitor()

mypy/test/typefixture.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,9 +339,13 @@ def def_alias_2(self, base: Instance) -> tuple[TypeAliasType, Type]:
339339
A.alias = AN
340340
return A, target
341341

342-
def non_rec_alias(self, target: Type) -> TypeAliasType:
343-
AN = TypeAlias(target, "__main__.A", -1, -1)
344-
return TypeAliasType(AN, [])
342+
def non_rec_alias(
343+
self, target: Type, alias_tvars: list[str] | None = None, args: list[Type] | None = None
344+
) -> TypeAliasType:
345+
AN = TypeAlias(target, "__main__.A", -1, -1, alias_tvars=alias_tvars)
346+
if args is None:
347+
args = []
348+
return TypeAliasType(AN, args)
345349

346350

347351
class InterfaceTypeFixture(TypeFixture):

mypy/type_visitor.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -404,24 +404,16 @@ def visit_placeholder_type(self, t: PlaceholderType) -> T:
404404
return self.query_types(t.args)
405405

406406
def visit_type_alias_type(self, t: TypeAliasType) -> T:
407+
# Skip type aliases already visited types to avoid infinite recursion.
408+
# TODO: Ideally we should fire subvisitors here (or use caching) if we care
409+
# about duplicates.
410+
if t in self.seen_aliases:
411+
return self.strategy([])
412+
self.seen_aliases.add(t)
407413
if self.skip_alias_target:
408414
return self.query_types(t.args)
409415
return get_proper_type(t).accept(self)
410416

411417
def query_types(self, types: Iterable[Type]) -> T:
412-
"""Perform a query for a list of types.
413-
414-
Use the strategy to combine the results.
415-
Skip type aliases already visited types to avoid infinite recursion.
416-
"""
417-
res: list[T] = []
418-
for t in types:
419-
if isinstance(t, TypeAliasType):
420-
# Avoid infinite recursion for recursive type aliases.
421-
# TODO: Ideally we should fire subvisitors here (or use caching) if we care
422-
# about duplicates.
423-
if t in self.seen_aliases:
424-
continue
425-
self.seen_aliases.add(t)
426-
res.append(t.accept(self))
427-
return self.strategy(res)
418+
"""Perform a query for a list of types using the strategy to combine the results."""
419+
return self.strategy([t.accept(self) for t in types])

mypy/typeanal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
450450
if fullname == "builtins.None":
451451
return NoneType()
452452
elif fullname == "typing.Any" or fullname == "builtins.Any":
453-
return AnyType(TypeOfAny.explicit)
453+
return AnyType(TypeOfAny.explicit, line=t.line, column=t.column)
454454
elif fullname in FINAL_TYPE_NAMES:
455455
self.fail(
456456
"Final can be only used as an outermost qualifier in a variable annotation",

mypy/types.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,30 +278,42 @@ def _expand_once(self) -> Type:
278278
self.alias.target, self.alias.alias_tvars, self.args, self.line, self.column
279279
)
280280

281-
def _partial_expansion(self) -> tuple[ProperType, bool]:
281+
def _partial_expansion(self, nothing_args: bool = False) -> tuple[ProperType, bool]:
282282
# Private method mostly for debugging and testing.
283283
unroller = UnrollAliasVisitor(set())
284-
unrolled = self.accept(unroller)
284+
if nothing_args:
285+
alias = self.copy_modified(args=[UninhabitedType()] * len(self.args))
286+
else:
287+
alias = self
288+
unrolled = alias.accept(unroller)
285289
assert isinstance(unrolled, ProperType)
286290
return unrolled, unroller.recursed
287291

288-
def expand_all_if_possible(self) -> ProperType | None:
292+
def expand_all_if_possible(self, nothing_args: bool = False) -> ProperType | None:
289293
"""Attempt a full expansion of the type alias (including nested aliases).
290294
291295
If the expansion is not possible, i.e. the alias is (mutually-)recursive,
292-
return None.
296+
return None. If nothing_args is True, replace all type arguments with an
297+
UninhabitedType() (used to detect recursively defined aliases).
293298
"""
294-
unrolled, recursed = self._partial_expansion()
299+
unrolled, recursed = self._partial_expansion(nothing_args=nothing_args)
295300
if recursed:
296301
return None
297302
return unrolled
298303

299304
@property
300305
def is_recursive(self) -> bool:
306+
"""Whether this type alias is recursive.
307+
308+
Note this doesn't check generic alias arguments, but only if this alias
309+
*definition* is recursive. The property value thus can be cached on the
310+
underlying TypeAlias node. If you want to include all nested types, use
311+
has_recursive_types() function.
312+
"""
301313
assert self.alias is not None, "Unfixed type alias"
302314
is_recursive = self.alias._is_recursive
303315
if is_recursive is None:
304-
is_recursive = self.expand_all_if_possible() is None
316+
is_recursive = self.expand_all_if_possible(nothing_args=True) is None
305317
# We cache the value on the underlying TypeAlias node as an optimization,
306318
# since the value is the same for all instances of the same alias.
307319
self.alias._is_recursive = is_recursive
@@ -3259,7 +3271,7 @@ def __init__(self) -> None:
32593271
super().__init__(any)
32603272

32613273
def visit_type_alias_type(self, t: TypeAliasType) -> bool:
3262-
return t.is_recursive
3274+
return t.is_recursive or self.query_types(t.args)
32633275

32643276

32653277
def has_recursive_types(typ: Type) -> bool:

mypy/util.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,11 @@ def parse_gray_color(cup: bytes) -> str:
520520

521521

522522
def should_force_color() -> bool:
523-
return bool(int(os.getenv("MYPY_FORCE_COLOR", os.getenv("FORCE_COLOR", "0"))))
523+
env_var = os.getenv("MYPY_FORCE_COLOR", os.getenv("FORCE_COLOR", "0"))
524+
try:
525+
return bool(int(env_var))
526+
except ValueError:
527+
return bool(env_var)
524528

525529

526530
class FancyFormatter:

test-data/unit/check-class-namedtuple.test

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,6 @@ class X(typing.NamedTuple):
393393
[out]
394394
main:6: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
395395
main:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
396-
main:7: error: Type cannot be declared in assignment to non-self attribute
397-
main:7: error: "int" has no attribute "x"
398396
main:9: error: Non-default NamedTuple fields cannot follow default fields
399397

400398
[builtins fixtures/list.pyi]

test-data/unit/check-incremental.test

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6334,3 +6334,17 @@ reveal_type(D().meth)
63346334
[out2]
63356335
tmp/m.py:4: note: Revealed type is "def [Self <: lib.C] (self: Self`0, other: Self`0) -> Self`0"
63366336
tmp/m.py:5: note: Revealed type is "def (other: m.D) -> m.D"
6337+
6338+
[case testIncrementalNestedGenericCallableCrash]
6339+
from typing import TypeVar, Callable
6340+
6341+
T = TypeVar("T")
6342+
6343+
class B:
6344+
def foo(self) -> Callable[[T], T]: ...
6345+
6346+
class C(B):
6347+
def __init__(self) -> None:
6348+
self.x = self.foo()
6349+
[out]
6350+
[out2]

0 commit comments

Comments
 (0)