Skip to content

Commit fd1aeab

Browse files
committed
Merge remote-tracking branch 'upstream/master' into TypeVar-01-foundation
2 parents bca0afc + 541639e commit fd1aeab

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1228
-529
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ repos:
44
hooks:
55
- id: black
66
- repo: https://github.com/pycqa/isort
7-
rev: 5.11.5 # must match test-requirements.txt
7+
rev: 5.12.0 # must match test-requirements.txt
88
hooks:
99
- id: isort
1010
- repo: https://github.com/pycqa/flake8
11-
rev: 5.0.4 # must match test-requirements.txt
11+
rev: 6.0.0 # must match test-requirements.txt
1212
hooks:
1313
- id: flake8
1414
additional_dependencies:

docs/source/error_code_list2.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,47 @@ silence the error:
347347
348348
async def g() -> None:
349349
_ = asyncio.create_task(f()) # No error
350+
351+
Check that ``# type: ignore`` comment is used [unused-ignore]
352+
-------------------------------------------------------------
353+
354+
If you use :option:`--enable-error-code unused-ignore <mypy --enable-error-code>`,
355+
or :option:`--warn-unused-ignores <mypy --warn-unused-ignores>`
356+
mypy generates an error if you don't use a ``# type: ignore`` comment, i.e. if
357+
there is a comment, but there would be no error generated by mypy on this line
358+
anyway.
359+
360+
Example:
361+
362+
.. code-block:: python
363+
364+
# Use "mypy --warn-unused-ignores ..."
365+
366+
def add(a: int, b: int) -> int:
367+
# Error: unused "type: ignore" comment
368+
return a + b # type: ignore
369+
370+
Note that due to a specific nature of this comment, the only way to selectively
371+
silence it, is to include the error code explicitly. Also note that this error is
372+
not shown if the ``# type: ignore`` is not used due to code being statically
373+
unreachable (e.g. due to platform or version checks).
374+
375+
Example:
376+
377+
.. code-block:: python
378+
379+
# Use "mypy --warn-unused-ignores ..."
380+
381+
import sys
382+
383+
try:
384+
# The "[unused-ignore]" is needed to get a clean mypy run
385+
# on both Python 3.8, and 3.9 where this module was added
386+
import graphlib # type: ignore[import,unused-ignore]
387+
except ImportError:
388+
pass
389+
390+
if sys.version_info >= (3, 9):
391+
# The following will not generate an error on either
392+
# Python 3.8, or Python 3.9
393+
42 + "testing..." # type: ignore

mypy/applytype.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from mypy.types import (
99
AnyType,
1010
CallableType,
11+
Instance,
1112
Parameters,
1213
ParamSpecType,
1314
PartialType,
@@ -75,7 +76,6 @@ def apply_generic_arguments(
7576
report_incompatible_typevar_value: Callable[[CallableType, Type, str, Context], None],
7677
context: Context,
7778
skip_unsatisfied: bool = False,
78-
allow_erased_callables: bool = False,
7979
) -> CallableType:
8080
"""Apply generic type arguments to a callable type.
8181
@@ -119,15 +119,9 @@ def apply_generic_arguments(
119119
star_index = callable.arg_kinds.index(ARG_STAR)
120120
callable = callable.copy_modified(
121121
arg_types=(
122-
[
123-
expand_type(at, id_to_type, allow_erased_callables)
124-
for at in callable.arg_types[:star_index]
125-
]
122+
[expand_type(at, id_to_type) for at in callable.arg_types[:star_index]]
126123
+ [callable.arg_types[star_index]]
127-
+ [
128-
expand_type(at, id_to_type, allow_erased_callables)
129-
for at in callable.arg_types[star_index + 1 :]
130-
]
124+
+ [expand_type(at, id_to_type) for at in callable.arg_types[star_index + 1 :]]
131125
)
132126
)
133127

@@ -155,30 +149,38 @@ def apply_generic_arguments(
155149
assert False, f"mypy bug: unimplemented case, {expanded_tuple}"
156150
elif isinstance(unpacked_type, TypeVarTupleType):
157151
expanded_tvt = expand_unpack_with_variables(var_arg.typ, id_to_type)
158-
assert isinstance(expanded_tvt, list)
159-
for t in expanded_tvt:
160-
assert not isinstance(t, UnpackType)
161-
callable = replace_starargs(callable, expanded_tvt)
152+
if isinstance(expanded_tvt, list):
153+
for t in expanded_tvt:
154+
assert not isinstance(t, UnpackType)
155+
callable = replace_starargs(callable, expanded_tvt)
156+
else:
157+
assert isinstance(expanded_tvt, Instance)
158+
assert expanded_tvt.type.fullname == "builtins.tuple"
159+
callable = callable.copy_modified(
160+
arg_types=(
161+
callable.arg_types[:star_index]
162+
+ [expanded_tvt.args[0]]
163+
+ callable.arg_types[star_index + 1 :]
164+
)
165+
)
162166
else:
163167
assert False, "mypy bug: unhandled case applying unpack"
164168
else:
165169
callable = callable.copy_modified(
166-
arg_types=[
167-
expand_type(at, id_to_type, allow_erased_callables) for at in callable.arg_types
168-
]
170+
arg_types=[expand_type(at, id_to_type) for at in callable.arg_types]
169171
)
170172

171173
# Apply arguments to TypeGuard if any.
172174
if callable.type_guard is not None:
173-
type_guard = expand_type(callable.type_guard, id_to_type, allow_erased_callables)
175+
type_guard = expand_type(callable.type_guard, id_to_type)
174176
else:
175177
type_guard = None
176178

177179
# The callable may retain some type vars if only some were applied.
178180
remaining_tvars = [tv for tv in tvars if tv.id not in id_to_type]
179181

180182
return callable.copy_modified(
181-
ret_type=expand_type(callable.ret_type, id_to_type, allow_erased_callables),
183+
ret_type=expand_type(callable.ret_type, id_to_type),
182184
variables=remaining_tvars,
183185
type_guard=type_guard,
184186
)

mypy/binder.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def __init__(self, id: int, conditional_frame: bool = False) -> None:
5151
# need this field.
5252
self.suppress_unreachable_warnings = False
5353

54+
def __repr__(self) -> str:
55+
return f"Frame({self.id}, {self.types}, {self.unreachable}, {self.conditional_frame})"
56+
5457

5558
Assigns = DefaultDict[Expression, List[Tuple[Type, Optional[Type]]]]
5659

@@ -63,7 +66,7 @@ class ConditionalTypeBinder:
6366
6467
```
6568
class A:
66-
a = None # type: Union[int, str]
69+
a: Union[int, str] = None
6770
x = A()
6871
lst = [x]
6972
reveal_type(x.a) # Union[int, str]
@@ -446,6 +449,7 @@ def top_frame_context(self) -> Iterator[Frame]:
446449
assert len(self.frames) == 1
447450
yield self.push_frame()
448451
self.pop_frame(True, 0)
452+
assert len(self.frames) == 1
449453

450454

451455
def get_declaration(expr: BindableExpression) -> Type | None:

mypy/build.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2238,6 +2238,7 @@ def semantic_analysis_pass1(self) -> None:
22382238
analyzer = SemanticAnalyzerPreAnalysis()
22392239
with self.wrap_context():
22402240
analyzer.visit_file(self.tree, self.xpath, self.id, options)
2241+
self.manager.errors.set_unreachable_lines(self.xpath, self.tree.unreachable_lines)
22412242
# TODO: Do this while constructing the AST?
22422243
self.tree.names = SymbolTable()
22432244
if not self.tree.is_stub:
@@ -2572,7 +2573,10 @@ def dependency_lines(self) -> list[int]:
25722573
return [self.dep_line_map.get(dep, 1) for dep in self.dependencies + self.suppressed]
25732574

25742575
def generate_unused_ignore_notes(self) -> None:
2575-
if self.options.warn_unused_ignores:
2576+
if (
2577+
self.options.warn_unused_ignores
2578+
or codes.UNUSED_IGNORE in self.options.enabled_error_codes
2579+
) and codes.UNUSED_IGNORE not in self.options.disabled_error_codes:
25762580
# If this file was initially loaded from the cache, it may have suppressed
25772581
# dependencies due to imports with ignores on them. We need to generate
25782582
# those errors to avoid spuriously flagging them as unused ignores.

mypy/checker.py

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
import mypy.checkexpr
2828
from mypy import errorcodes as codes, message_registry, nodes, operators
29-
from mypy.binder import ConditionalTypeBinder, get_declaration
29+
from mypy.binder import ConditionalTypeBinder, Frame, get_declaration
3030
from mypy.checkmember import (
3131
MemberContext,
3232
analyze_decorator_or_funcbase_access,
@@ -41,7 +41,7 @@
4141
from mypy.errors import Errors, ErrorWatcher, report_internal_error
4242
from mypy.expandtype import expand_self_type, expand_type, expand_type_by_instance
4343
from mypy.join import join_types
44-
from mypy.literals import Key, literal, literal_hash
44+
from mypy.literals import Key, extract_var_from_literal_hash, literal, literal_hash
4545
from mypy.maptype import map_instance_to_supertype
4646
from mypy.meet import is_overlapping_erased_types, is_overlapping_types
4747
from mypy.message_registry import ErrorMessage
@@ -134,6 +134,7 @@
134134
is_final_node,
135135
)
136136
from mypy.options import Options
137+
from mypy.patterns import AsPattern, StarredPattern
137138
from mypy.plugin import CheckerPluginInterface, Plugin
138139
from mypy.scope import Scope
139140
from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name
@@ -151,7 +152,7 @@
151152
restrict_subtype_away,
152153
unify_generic_callable,
153154
)
154-
from mypy.traverser import all_return_statements, has_return_statement
155+
from mypy.traverser import TraverserVisitor, all_return_statements, has_return_statement
155156
from mypy.treetransform import TransformVisitor
156157
from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type, make_optional_type
157158
from mypy.typeops import (
@@ -1207,6 +1208,21 @@ def check_func_def(
12071208

12081209
# Type check body in a new scope.
12091210
with self.binder.top_frame_context():
1211+
# Copy some type narrowings from an outer function when it seems safe enough
1212+
# (i.e. we can't find an assignment that might change the type of the
1213+
# variable afterwards).
1214+
new_frame: Frame | None = None
1215+
for frame in old_binder.frames:
1216+
for key, narrowed_type in frame.types.items():
1217+
key_var = extract_var_from_literal_hash(key)
1218+
if key_var is not None and not self.is_var_redefined_in_outer_context(
1219+
key_var, defn.line
1220+
):
1221+
# It seems safe to propagate the type narrowing to a nested scope.
1222+
if new_frame is None:
1223+
new_frame = self.binder.push_frame()
1224+
new_frame.types[key] = narrowed_type
1225+
self.binder.declarations[key] = old_binder.declarations[key]
12101226
with self.scope.push_function(defn):
12111227
# We suppress reachability warnings when we use TypeVars with value
12121228
# restrictions: we only want to report a warning if a certain statement is
@@ -1218,6 +1234,8 @@ def check_func_def(
12181234
self.binder.suppress_unreachable_warnings()
12191235
self.accept(item.body)
12201236
unreachable = self.binder.is_unreachable()
1237+
if new_frame is not None:
1238+
self.binder.pop_frame(True, 0)
12211239

12221240
if not unreachable:
12231241
if defn.is_generator or is_named_instance(
@@ -1310,6 +1328,23 @@ def check_func_def(
13101328

13111329
self.binder = old_binder
13121330

1331+
def is_var_redefined_in_outer_context(self, v: Var, after_line: int) -> bool:
1332+
"""Can the variable be assigned to at module top level or outer function?
1333+
1334+
Note that this doesn't do a full CFG analysis but uses a line number based
1335+
heuristic that isn't correct in some (rare) cases.
1336+
"""
1337+
outers = self.tscope.outer_functions()
1338+
if not outers:
1339+
# Top-level function -- outer context is top level, and we can't reason about
1340+
# globals
1341+
return True
1342+
for outer in outers:
1343+
if isinstance(outer, FuncDef):
1344+
if find_last_var_assignment_line(outer.body, v) >= after_line:
1345+
return True
1346+
return False
1347+
13131348
def check_unbound_return_typevar(self, typ: CallableType) -> None:
13141349
"""Fails when the return typevar is not defined in arguments."""
13151350
if isinstance(typ.ret_type, TypeVarType) and typ.ret_type in typ.variables:
@@ -7630,3 +7665,80 @@ def collapse_walrus(e: Expression) -> Expression:
76307665
if isinstance(e, AssignmentExpr):
76317666
return e.target
76327667
return e
7668+
7669+
7670+
def find_last_var_assignment_line(n: Node, v: Var) -> int:
7671+
"""Find the highest line number of a potential assignment to variable within node.
7672+
7673+
This supports local and global variables.
7674+
7675+
Return -1 if no assignment was found.
7676+
"""
7677+
visitor = VarAssignVisitor(v)
7678+
n.accept(visitor)
7679+
return visitor.last_line
7680+
7681+
7682+
class VarAssignVisitor(TraverserVisitor):
7683+
def __init__(self, v: Var) -> None:
7684+
self.last_line = -1
7685+
self.lvalue = False
7686+
self.var_node = v
7687+
7688+
def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
7689+
self.lvalue = True
7690+
for lv in s.lvalues:
7691+
lv.accept(self)
7692+
self.lvalue = False
7693+
7694+
def visit_name_expr(self, e: NameExpr) -> None:
7695+
if self.lvalue and e.node is self.var_node:
7696+
self.last_line = max(self.last_line, e.line)
7697+
7698+
def visit_member_expr(self, e: MemberExpr) -> None:
7699+
old_lvalue = self.lvalue
7700+
self.lvalue = False
7701+
super().visit_member_expr(e)
7702+
self.lvalue = old_lvalue
7703+
7704+
def visit_index_expr(self, e: IndexExpr) -> None:
7705+
old_lvalue = self.lvalue
7706+
self.lvalue = False
7707+
super().visit_index_expr(e)
7708+
self.lvalue = old_lvalue
7709+
7710+
def visit_with_stmt(self, s: WithStmt) -> None:
7711+
self.lvalue = True
7712+
for lv in s.target:
7713+
if lv is not None:
7714+
lv.accept(self)
7715+
self.lvalue = False
7716+
s.body.accept(self)
7717+
7718+
def visit_for_stmt(self, s: ForStmt) -> None:
7719+
self.lvalue = True
7720+
s.index.accept(self)
7721+
self.lvalue = False
7722+
s.body.accept(self)
7723+
if s.else_body:
7724+
s.else_body.accept(self)
7725+
7726+
def visit_assignment_expr(self, e: AssignmentExpr) -> None:
7727+
self.lvalue = True
7728+
e.target.accept(self)
7729+
self.lvalue = False
7730+
e.value.accept(self)
7731+
7732+
def visit_as_pattern(self, p: AsPattern) -> None:
7733+
if p.pattern is not None:
7734+
p.pattern.accept(self)
7735+
if p.name is not None:
7736+
self.lvalue = True
7737+
p.name.accept(self)
7738+
self.lvalue = False
7739+
7740+
def visit_starred_pattern(self, p: StarredPattern) -> None:
7741+
if p.capture is not None:
7742+
self.lvalue = True
7743+
p.capture.accept(self)
7744+
self.lvalue = False

0 commit comments

Comments
 (0)