Skip to content

Commit 661599d

Browse files
committed
stub mode for TYPE_CHECKING and introducing BASEDMYPY_TYPE_CHECKING
1 parent 49af712 commit 661599d

File tree

10 files changed

+130
-35
lines changed

10 files changed

+130
-35
lines changed

.mypy/baseline.json

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2241,7 +2241,7 @@
22412241
"code": "explicit-override",
22422242
"column": 4,
22432243
"message": "Method \"visit_while_stmt\" is not using @override but is overriding a method in class \"mypy.visitor.NodeVisitor\"",
2244-
"offset": 36,
2244+
"offset": 43,
22452245
"src": "def visit_while_stmt(self, s: WhileStmt) -> None:",
22462246
"target": "mypy.checker.TypeChecker.visit_while_stmt"
22472247
},
@@ -5901,7 +5901,7 @@
59015901
"code": "no-any-explicit",
59025902
"column": 20,
59035903
"message": "Explicit \"Any\" is not allowed",
5904-
"offset": 4,
5904+
"offset": 5,
59055905
"src": "resp: dict[str, Any] = {}",
59065906
"target": "mypy.dmypy_server.Server.serve"
59075907
},
@@ -6345,19 +6345,11 @@
63456345
"src": "res[\"memory_vms_mib\"] = meminfo.vms / MiB",
63466346
"target": "mypy.dmypy_server.get_meminfo"
63476347
},
6348-
{
6349-
"code": "no-any-expr",
6350-
"column": 12,
6351-
"message": "Expression type contains \"Any\" (has type \"dict[str, Any]\")",
6352-
"offset": 12,
6353-
"src": "res[\"memory_maxrss_mib\"] = rusage.ru_maxrss * factor / MiB",
6354-
"target": "mypy.dmypy_server.get_meminfo"
6355-
},
63566348
{
63576349
"code": "no-any-expr",
63586350
"column": 11,
63596351
"message": "Expression type contains \"Any\" (has type \"dict[str, Any]\")",
6360-
"offset": 1,
6352+
"offset": 13,
63616353
"src": "return res",
63626354
"target": "mypy.dmypy_server.get_meminfo"
63636355
}
@@ -13395,7 +13387,7 @@
1339513387
"code": "explicit-override",
1339613388
"column": 4,
1339713389
"message": "Method \"name\" is not using @override but is overriding a method in class \"mypy.nodes.FuncBase\"",
13398-
"offset": 91,
13390+
"offset": 93,
1339913391
"src": "def name(self) -> str:",
1340013392
"target": "mypy.nodes"
1340113393
},
@@ -13835,7 +13827,7 @@
1383513827
"code": "explicit-override",
1383613828
"column": 4,
1383713829
"message": "Method \"accept\" is not using @override but is overriding a method in class \"mypy.nodes.Statement\"",
13838-
"offset": 19,
13830+
"offset": 27,
1383913831
"src": "def accept(self, visitor: StatementVisitor[T]) -> T:",
1384013832
"target": "mypy.nodes.IfStmt.accept"
1384113833
},
@@ -14845,7 +14837,7 @@
1484514837
"code": "no-any-expr",
1484614838
"column": 18,
1484714839
"message": "Expression has type \"Any\"",
14848-
"offset": 128,
14840+
"offset": 129,
1484914841
"src": "MACHDEP = sysconfig.get_config_var(\"MACHDEP\")",
1485014842
"target": "mypy.options.Options.__init__"
1485114843
},
@@ -15895,7 +15887,7 @@
1589515887
"code": "no-any-expr",
1589615888
"column": 49,
1589715889
"message": "Expression has type \"Any\"",
15898-
"offset": 146,
15890+
"offset": 150,
1589915891
"src": "result = consider_sys_platform(expr, options.platform)",
1590015892
"target": "mypy.reachability.infer_condition_value"
1590115893
},
@@ -15954,6 +15946,14 @@
1595415946
"offset": 3,
1595515947
"src": "def visit_func_def(self, node: FuncDef) -> None:",
1595615948
"target": "mypy.reachability.MarkImportsMypyOnlyVisitor.visit_func_def"
15949+
},
15950+
{
15951+
"code": "explicit-override",
15952+
"column": 4,
15953+
"message": "Method \"visit_overloaded_func_def\" is not using @override but is overriding a method in class \"mypy.traverser.TraverserVisitor\"",
15954+
"offset": 3,
15955+
"src": "def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:",
15956+
"target": "mypy.reachability.MarkImportsMypyOnlyVisitor.visit_overloaded_func_def"
1595715957
}
1595815958
],
1595915959
"mypy/refinfo.py": [
@@ -17145,7 +17145,7 @@
1714517145
"code": "possibly-undefined",
1714617146
"column": 41,
1714717147
"message": "Name \"original_target\" may be undefined",
17148-
"offset": 68,
17148+
"offset": 71,
1714917149
"src": "if defn.info and original_target == defn.arguments[0].variable.name:",
1715017150
"target": "mypy.semanal"
1715117151
},
@@ -17177,15 +17177,15 @@
1717717177
"code": "redundant-expr",
1717817178
"column": 17,
1717917179
"message": "Condition is always true",
17180-
"offset": 137,
17180+
"offset": 138,
1718117181
"src": "elif isinstance(item, FuncDef):",
1718217182
"target": "mypy.semanal.SemanticAnalyzer.analyze_overload_sigs_and_impl"
1718317183
},
1718417184
{
1718517185
"code": "redundant-expr",
1718617186
"column": 17,
1718717187
"message": "Condition is always true",
17188-
"offset": 88,
17188+
"offset": 89,
1718917189
"src": "elif isinstance(item, FuncDef):",
1719017190
"target": "mypy.semanal.SemanticAnalyzer.process_static_or_class_method_in_overload"
1719117191
},
@@ -17281,7 +17281,7 @@
1728117281
"code": "no-any-expr",
1728217282
"column": 67,
1728317283
"message": "Expression type contains \"Any\" (has type \"set[Any]\")",
17284-
"offset": 162,
17284+
"offset": 164,
1728517285
"src": "alternatives = set(module.names.keys()).difference({source_id})",
1728617286
"target": "mypy.semanal.SemanticAnalyzer.report_missing_module_attribute"
1728717287
},
@@ -17569,7 +17569,7 @@
1756917569
"code": "explicit-override",
1757017570
"column": 4,
1757117571
"message": "Method \"visit_try_stmt\" is not using @override but is overriding a method in class \"mypy.visitor.NodeVisitor\"",
17572-
"offset": 8,
17572+
"offset": 16,
1757317573
"src": "def visit_try_stmt(self, s: TryStmt) -> None:",
1757417574
"target": "mypy.semanal.SemanticAnalyzer.visit_try_stmt"
1757517575
},
@@ -18249,7 +18249,7 @@
1824918249
"code": "explicit-override",
1825018250
"column": 4,
1825118251
"message": "Method \"class_type\" is not using @override but is overriding a method in class \"mypy.plugin.SemanticAnalyzerPluginInterface\"",
18252-
"offset": 66,
18252+
"offset": 68,
1825318253
"src": "def class_type(self, self_type: Type) -> Type:",
1825418254
"target": "mypy.semanal.SemanticAnalyzer.class_type"
1825518255
},
@@ -31721,7 +31721,7 @@
3172131721
"code": "explicit-override",
3172231722
"column": 4,
3172331723
"message": "Method \"visit_any\" is not using @override but is overriding a method in class \"mypy.type_visitor.TypeVisitor\"",
31724-
"offset": 692,
31724+
"offset": 693,
3172531725
"src": "def visit_any(self, t: AnyType) -> Type:",
3172631726
"target": "mypy.typeanal.TypeAnalyser.visit_any"
3172731727
},

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
### Added
55
- Any parameter `_` will be inferred as `object` (#687)
66
- `work_not_properly_function_names` made available to per module configuration (#699)
7+
- Support `BASEDMYPY_TYPE_CHECKING` (#702)
8+
- Enable stub mode within `TYPE_CHECKING` branches (#702)
79

810
## [2.5.0]
911
### Added

mypy/checker.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4918,6 +4918,9 @@ def visit_if_stmt(self, s: IfStmt) -> None:
49184918
# This frame records the knowledge from previous if/elif clauses not being taken.
49194919
# Fall-through to the original frame is handled explicitly in each block.
49204920
with self.binder.frame_context(can_skip=False, conditional_frame=True, fall_through=0):
4921+
is_stub = self.is_stub
4922+
if s.is_mypy_only:
4923+
self.is_stub = True
49214924
for e, b in zip(s.expr, s.body):
49224925
t = get_proper_type(self.expr_checker.accept(e))
49234926
allow_redundant_expr = self.allow_redundant_expr
@@ -4944,10 +4947,14 @@ def visit_if_stmt(self, s: IfStmt) -> None:
49444947
):
49454948
self.msg.fail("Condition is always true", e, code=codes.REDUNDANT_EXPR)
49464949
self.push_type_map(else_map)
4947-
4950+
if s.is_mypy_only is False:
4951+
self.is_stub = True
4952+
else:
4953+
self.is_stub = is_stub
49484954
with self.binder.frame_context(can_skip=False, fall_through=2):
49494955
if s.else_body:
49504956
self.accept(s.else_body)
4957+
self.is_stub = is_stub
49514958

49524959
def visit_while_stmt(self, s: WhileStmt) -> None:
49534960
"""Type check a while statement."""

mypy/dmypy_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1022,7 +1022,7 @@ def get_meminfo() -> dict[str, Any]:
10221022
factor = 1
10231023
else:
10241024
factor = 1024 # Linux
1025-
res["memory_maxrss_mib"] = rusage.ru_maxrss * factor / MiB
1025+
res["memory_maxrss_mib"] = rusage.ru_maxrss * factor / MiB # type: ignore[no-any-expr, unused-ignore]
10261026
return res
10271027

10281028

mypy/nodes.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -558,11 +558,12 @@ class OverloadedFuncDef(FuncBase, SymbolNode, Statement):
558558
Overloaded variants must be consecutive in the source file.
559559
"""
560560

561-
__slots__ = ("items", "unanalyzed_items", "impl")
561+
__slots__ = ("items", "unanalyzed_items", "impl", "is_mypy_only")
562562

563563
items: list[OverloadPart]
564564
unanalyzed_items: list[OverloadPart]
565565
impl: OverloadPart | None
566+
is_mypy_only: bool
566567

567568
def __init__(self, items: list[OverloadPart]) -> None:
568569
super().__init__()
@@ -572,6 +573,7 @@ def __init__(self, items: list[OverloadPart]) -> None:
572573
if items:
573574
# TODO: figure out how to reliably set end position (we don't know the impl here).
574575
self.set_line(items[0].line, items[0].column)
576+
self.is_mypy_only = False
575577

576578
@property
577579
def name(self) -> str:
@@ -1475,19 +1477,27 @@ def accept(self, visitor: StatementVisitor[T]) -> T:
14751477

14761478

14771479
class IfStmt(Statement):
1478-
__slots__ = ("expr", "body", "else_body")
1480+
__slots__ = ("expr", "body", "else_body", "is_mypy_only")
14791481

1480-
__match_args__ = ("expr", "body", "else_body")
1482+
__match_args__ = ("expr", "body", "else_body", "is_mypy_only")
14811483

14821484
expr: list[Expression]
14831485
body: list[Block]
14841486
else_body: Block | None
1487+
is_mypy_only: bool | None
14851488

1486-
def __init__(self, expr: list[Expression], body: list[Block], else_body: Block | None) -> None:
1489+
def __init__(
1490+
self,
1491+
expr: list[Expression],
1492+
body: list[Block],
1493+
else_body: Block | None,
1494+
is_mypy_only: bool | None = None,
1495+
) -> None:
14871496
super().__init__()
14881497
self.expr = expr
14891498
self.body = body
14901499
self.else_body = else_body
1500+
self.is_mypy_only = is_mypy_only
14911501

14921502
def accept(self, visitor: StatementVisitor[T]) -> T:
14931503
return visitor.visit_if_stmt(self)

mypy/reachability.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
MemberExpr,
2424
NameExpr,
2525
OpExpr,
26+
OverloadedFuncDef,
2627
SliceExpr,
2728
StrExpr,
2829
TupleExpr,
@@ -54,12 +55,15 @@ def infer_reachability_of_if_statement(s: IfStmt, options: Options) -> None:
5455
for i in range(len(s.expr)):
5556
result = infer_condition_value(s.expr[i], options)
5657
if result in (ALWAYS_FALSE, MYPY_FALSE):
58+
if result == MYPY_FALSE:
59+
s.is_mypy_only = False
5760
# The condition is considered always false, so we skip the if/elif body.
5861
mark_block_unreachable(s.body[i])
5962
elif result in (ALWAYS_TRUE, MYPY_TRUE):
6063
# This condition is considered always true, so all of the remaining
6164
# elif/else bodies should not be checked.
6265
if result == MYPY_TRUE:
66+
s.is_mypy_only = True
6367
# This condition is false at runtime; this will affect
6468
# import priorities.
6569
mark_block_mypy_only(s.body[i])
@@ -149,7 +153,7 @@ def infer_condition_value(expr: Expression, options: Options) -> int:
149153
result = ALWAYS_FALSE
150154
elif name == "PY3":
151155
result = ALWAYS_TRUE
152-
elif name == "MYPY" or name == "TYPE_CHECKING":
156+
elif name in {"BASEDMYPY_TYPE_CHECKING", "MYPY", "TYPE_CHECKING"}:
153157
result = MYPY_TRUE
154158
elif name in options.always_true:
155159
result = ALWAYS_TRUE
@@ -360,3 +364,6 @@ def visit_import_all(self, node: ImportAll) -> None:
360364

361365
def visit_func_def(self, node: FuncDef) -> None:
362366
node.is_mypy_only = True
367+
368+
def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:
369+
o.is_mypy_only = True

mypy/semanal.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,9 @@ def analyze_func_def(self, defn: FuncDef) -> None:
907907
# Signature must be analyzed in the surrounding scope so that
908908
# class-level imported names and type variables are in scope.
909909
analyzer = self.type_analyzer()
910+
analyzer.always_allow_new_syntax = (
911+
analyzer.always_allow_new_syntax or defn.is_mypy_only
912+
)
910913
tag = self.track_incomplete_refs()
911914
result = analyzer.visit_callable_type(defn.type, nested=False)
912915
# Don't store not ready types (including placeholders).
@@ -1189,7 +1192,8 @@ def setup_self_type(self) -> None:
11891192
def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
11901193
self.statement = defn
11911194
self.add_function_to_symbol_table(defn)
1192-
1195+
if self.is_stub_file:
1196+
defn.is_mypy_only = True
11931197
if not self.recurse_into_functions:
11941198
return
11951199

@@ -1369,7 +1373,7 @@ def handle_missing_overload_decorators(
13691373

13701374
def handle_missing_overload_implementation(self, defn: OverloadedFuncDef) -> None:
13711375
"""Generate error about missing overload implementation (only if needed)."""
1372-
if not self.is_stub_file:
1376+
if not self.is_stub_file and not defn.is_mypy_only:
13731377
if self.type and self.type.is_protocol and not self.is_func_scope():
13741378
# An overloaded protocol method doesn't need an implementation,
13751379
# but if it doesn't have one, then it is considered abstract.
@@ -1379,6 +1383,7 @@ def handle_missing_overload_implementation(self, defn: OverloadedFuncDef) -> Non
13791383
else:
13801384
item.abstract_status = IS_ABSTRACT
13811385
else:
1386+
13821387
# TODO: also allow omitting an implementation for abstract methods in ABCs?
13831388
self.fail(
13841389
"An overloaded function outside a stub file must have an implementation",
@@ -2701,7 +2706,9 @@ def visit_import_from(self, imp: ImportFrom) -> None:
27012706
# Modules imported in a stub file without using 'from Y import X as X' will
27022707
# not get exported.
27032708
# When implicit re-exporting is disabled, we have the same behavior as stubs.
2704-
use_implicit_reexport = not self.is_stub_file and self.options.implicit_reexport
2709+
# we recheck if it's a stub manually to avoid TYPE_CHECKING blocks
2710+
is_stub_file = self.cur_mod_node.path.lower().endswith(".pyi")
2711+
use_implicit_reexport = not is_stub_file and self.options.implicit_reexport
27052712
module_public = use_implicit_reexport or (as_id is not None and id == as_id)
27062713

27072714
# If the module does not contain a symbol with the name 'id',
@@ -5128,10 +5135,18 @@ def visit_continue_stmt(self, s: ContinueStmt) -> None:
51285135
def visit_if_stmt(self, s: IfStmt) -> None:
51295136
self.statement = s
51305137
infer_reachability_of_if_statement(s, self.options)
5138+
is_stub_file = self.is_stub_file
5139+
if s.is_mypy_only:
5140+
self._is_stub_file = True
51315141
for i in range(len(s.expr)):
51325142
s.expr[i].accept(self)
51335143
self.visit_block(s.body[i])
5144+
if s.is_mypy_only is False:
5145+
self._is_stub_file = True
5146+
else:
5147+
self._is_stub_file = is_stub_file
51345148
self.visit_block_maybe(s.else_body)
5149+
self._is_stub_file = is_stub_file
51355150

51365151
def visit_try_stmt(self, s: TryStmt) -> None:
51375152
self.statement = s
@@ -7017,6 +7032,8 @@ def anal_type(
70177032
a.always_allow_new_syntax = False
70187033
if runtime is False:
70197034
a.always_allow_new_syntax = True
7035+
if self.is_stub_file:
7036+
a.always_allow_new_syntax = True
70207037
tag = self.track_incomplete_refs()
70217038
typ = typ.accept(a)
70227039
if self.found_incomplete_ref(tag):

0 commit comments

Comments
 (0)