Skip to content

Commit 3b39b1b

Browse files
authored
Support None partial types with local partial types (python#20938)
This turned out to be surprisingly easy, hopefully I didn't miss anything. Implementation is a bit ad-hoc, but the feature is equally ad-hoc. I enable both class-level and module-level `None` cases. Note this also naturally fixes a bug when skipping function bodies in third-party packages. I added this as an optimization of the previous stripping bodies optimization, but I forgot about partial types.
1 parent 1952636 commit 3b39b1b

6 files changed

Lines changed: 312 additions & 80 deletions

File tree

mypy/checker.py

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -719,10 +719,25 @@ def accept_loop(
719719
# Definitions
720720
#
721721

722+
@contextmanager
723+
def set_recurse_into_functions(self) -> Iterator[None]:
724+
"""Temporarily set recurse_into_functions to True.
725+
726+
This is used to process top-level functions/methods as a whole.
727+
"""
728+
old_recurse_into_functions = self.recurse_into_functions
729+
self.recurse_into_functions = True
730+
try:
731+
yield
732+
finally:
733+
self.recurse_into_functions = old_recurse_into_functions
734+
722735
def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
723-
if not self.recurse_into_functions:
736+
# If a function/method can infer variable types, it should be processed as part
737+
# of the module top level (i.e. module interface).
738+
if not self.recurse_into_functions and not defn.can_infer_vars:
724739
return
725-
with self.tscope.function_scope(defn):
740+
with self.tscope.function_scope(defn), self.set_recurse_into_functions():
726741
self._visit_overloaded_func_def(defn)
727742

728743
def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
@@ -1196,9 +1211,9 @@ def get_generator_return_type(self, return_type: Type, is_coroutine: bool) -> Ty
11961211
return NoneType()
11971212

11981213
def visit_func_def(self, defn: FuncDef) -> None:
1199-
if not self.recurse_into_functions:
1214+
if not self.recurse_into_functions and not defn.can_infer_vars:
12001215
return
1201-
with self.tscope.function_scope(defn):
1216+
with self.tscope.function_scope(defn), self.set_recurse_into_functions():
12021217
self.check_func_item(defn, name=defn.name)
12031218
if not self.can_skip_diagnostics:
12041219
if defn.info:
@@ -1438,6 +1453,7 @@ def check_func_def(
14381453
or self.options.preserve_asts
14391454
or not isinstance(defn, FuncDef)
14401455
or defn.has_self_attr_def
1456+
or defn.can_infer_vars
14411457
):
14421458
self.accept(item.body)
14431459
unreachable = self.binder.is_unreachable()
@@ -5604,8 +5620,8 @@ def visit_decorator(self, e: Decorator) -> None:
56045620
def visit_decorator_inner(
56055621
self, e: Decorator, allow_empty: bool = False, skip_first_item: bool = False
56065622
) -> None:
5607-
if self.recurse_into_functions:
5608-
with self.tscope.function_scope(e.func):
5623+
if self.recurse_into_functions or e.func.can_infer_vars:
5624+
with self.tscope.function_scope(e.func), self.set_recurse_into_functions():
56095625
self.check_func_item(e.func, name=e.func.name, allow_empty=allow_empty)
56105626

56115627
# Process decorators from the inside out to determine decorated signature, which
@@ -7718,30 +7734,7 @@ def enter_partial_types(
77187734
partial_types, _, _ = self.partial_types.pop()
77197735
if not self.current_node_deferred:
77207736
for var, context in partial_types.items():
7721-
# If we require local partial types, there are a few exceptions where
7722-
# we fall back to inferring just "None" as the type from a None initializer:
7723-
#
7724-
# 1. If all happens within a single function this is acceptable, since only
7725-
# the topmost function is a separate target in fine-grained incremental mode.
7726-
# We primarily want to avoid "splitting" partial types across targets.
7727-
#
7728-
# 2. A None initializer in the class body if the attribute is defined in a base
7729-
# class is fine, since the attribute is already defined and it's currently okay
7730-
# to vary the type of an attribute covariantly. The None type will still be
7731-
# checked for compatibility with base classes elsewhere. Without this exception
7732-
# mypy could require an annotation for an attribute that already has been
7733-
# declared in a base class, which would be bad.
7734-
allow_none = (
7735-
not self.options.local_partial_types
7736-
or is_function
7737-
or (is_class and self.is_defined_in_base_class(var))
7738-
)
7739-
if (
7740-
allow_none
7741-
and isinstance(var.type, PartialType)
7742-
and var.type.type is None
7743-
and not permissive
7744-
):
7737+
if isinstance(var.type, PartialType) and var.type.type is None and not permissive:
77457738
var.type = NoneType()
77467739
else:
77477740
if var not in self.partial_reported and not permissive:
@@ -7812,10 +7805,15 @@ def find_partial_types_in_all_scopes(
78127805
# for fine-grained incremental mode).
78137806
disallow_other_scopes = self.options.local_partial_types
78147807

7808+
# There are two exceptions:
78157809
if isinstance(var.type, PartialType) and var.type.type is not None and var.info:
7816-
# This is an ugly hack to make partial generic self attributes behave
7817-
# as if --local-partial-types is always on (because it used to be like this).
7810+
# We always prohibit non-None partial types at class scope
7811+
# for historical reasons.
78187812
disallow_other_scopes = True
7813+
if isinstance(var.type, PartialType) and var.type.type is None:
7814+
# We always allow None partial types, since this is a common use case.
7815+
# It is special-cased in fine-grained incremental mode.
7816+
disallow_other_scopes = False
78197817

78207818
scope_active = (
78217819
not disallow_other_scopes or scope.is_local == self.partial_types[-1].is_local

mypy/nodes.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,7 @@ class FuncBase(Node):
589589
"is_final", # Uses "@final"
590590
"is_explicit_override", # Uses "@override"
591591
"is_type_check_only", # Uses "@type_check_only"
592+
"can_infer_vars",
592593
"_fullname",
593594
)
594595

@@ -609,6 +610,18 @@ def __init__(self) -> None:
609610
self.is_final = False
610611
self.is_explicit_override = False
611612
self.is_type_check_only = False
613+
# Can this function/method infer types of variables defined outside? Currently,
614+
# we only set this in cases like:
615+
# x = None
616+
# def foo() -> None:
617+
# global x
618+
# x = 1
619+
# and
620+
# class C:
621+
# x = None
622+
# def foo(self) -> None:
623+
# self.x = 1
624+
self.can_infer_vars = False
612625
# Name with module prefix
613626
self._fullname = ""
614627

mypy/semanal.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4589,6 +4589,17 @@ def make_name_lvalue_point_to_existing_def(
45894589
self.name_not_defined(lval.name, lval)
45904590
self.check_lvalue_validity(lval.node, lval)
45914591

4592+
if self.scope.functions and lval.name in self.global_decls[-1]:
4593+
# Technically, we only need to set this if original r.h.s. would be inferred
4594+
# as None, but it is tricky to detect reliably during semantic analysis.
4595+
if (
4596+
original_def
4597+
and isinstance(original_def.node, Var)
4598+
and original_def.node.is_inferred
4599+
):
4600+
for func in self.scope.functions:
4601+
func.can_infer_vars = True
4602+
45924603
def analyze_tuple_or_list_lvalue(self, lval: TupleExpr, explicit_type: bool = False) -> None:
45934604
"""Analyze an lvalue or assignment target that is a list or tuple."""
45944605
items = lval.items
@@ -4674,6 +4685,14 @@ def analyze_member_lvalue(
46744685
for func in self.scope.functions:
46754686
if isinstance(func, FuncDef):
46764687
func.has_self_attr_def = True
4688+
if (
4689+
cur_node
4690+
and isinstance(cur_node.node, Var)
4691+
and cur_node.node.is_inferred
4692+
and cur_node.node.is_initialized_in_class
4693+
):
4694+
for func in self.scope.functions:
4695+
func.can_infer_vars = True
46774696
self.check_lvalue_validity(lval.node, lval)
46784697

46794698
def is_self_member_ref(self, memberexpr: MemberExpr) -> bool:

mypy/server/update.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
from mypy.modulefinder import BuildSource
139139
from mypy.nodes import (
140140
Decorator,
141+
FuncBase,
141142
FuncDef,
142143
ImportFrom,
143144
MypyFile,
@@ -953,6 +954,20 @@ def find_targets_recursive(
953954
deferred, stale_proto = lookup_target(manager, target)
954955
if stale_proto:
955956
stale_protos.add(stale_proto)
957+
958+
# If there are function targets that can infer outer variables, they should
959+
# be re-processed as part of the module top-level instead (for consistency).
960+
regular = []
961+
shared = []
962+
for d in deferred:
963+
if isinstance(d.node, FuncBase) and d.node.can_infer_vars:
964+
shared.append(d)
965+
else:
966+
regular.append(d)
967+
deferred = regular
968+
if shared:
969+
deferred.append(FineGrainedDeferredNode(manager.modules[module_id], None))
970+
956971
result[module_id].update(deferred)
957972

958973
return result, unloaded_files, stale_protos

0 commit comments

Comments
 (0)