Skip to content

Commit cba7887

Browse files
authored
Add experimental hidden flag to tweak dependencies (#5367)
This flag will tweak fine grained dependencies to reflect propagation of type (im)precision instead of actual semantic dependencies. The flag should never be used to run actual type check, only to analyze type coverage in a code base. The flag could be useful when annotating legacy code, to check which functions (decorators, base classes, variables) are most important to annotate first.
1 parent c52fabf commit cba7887

File tree

6 files changed

+202
-19
lines changed

6 files changed

+202
-19
lines changed

mypy/build.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2217,7 +2217,8 @@ def compute_fine_grained_deps(self) -> None:
22172217
return
22182218
self.fine_grained_deps = get_dependencies(target=self.tree,
22192219
type_map=self.type_map(),
2220-
python_version=self.options.python_version)
2220+
python_version=self.options.python_version,
2221+
options=self.manager.options)
22212222

22222223
def valid_references(self) -> Set[str]:
22232224
assert self.ancestors is not None
@@ -2570,7 +2571,8 @@ def dispatch(sources: List[BuildSource], manager: BuildManager) -> Graph:
25702571
if manager.options.dump_deps:
25712572
# This speeds up startup a little when not using the daemon mode.
25722573
from mypy.server.deps import dump_all_dependencies
2573-
dump_all_dependencies(manager.modules, manager.all_types, manager.options.python_version)
2574+
dump_all_dependencies(manager.modules, manager.all_types,
2575+
manager.options.python_version, manager.options)
25742576
return graph
25752577

25762578

mypy/main.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,12 @@ def add_invertible_flag(flag: str,
612612
# --local-partial-types disallows partial types spanning module top level and a function
613613
# (implicitly defined in fine-grained incremental mode)
614614
parser.add_argument('--local-partial-types', action='store_true', help=argparse.SUPPRESS)
615+
# --logical-deps adds some more dependencies that are not semantically needed, but
616+
# may be helpful to determine relative importance of classes and functions for overall
617+
# type precision in a code base. It also _removes_ some deps, so this flag should be never
618+
# used except for generating code stats. This also automatically enables --cache-fine-grained.
619+
# NOTE: This is an experimental option that may be modified or removed at any time.
620+
parser.add_argument('--logical-deps', action='store_true', help=argparse.SUPPRESS)
615621
# --bazel changes some behaviors for use with Bazel (https://bazel.build).
616622
parser.add_argument('--bazel', action='store_true', help=argparse.SUPPRESS)
617623
# --package-root adds a directory below which directories are considered
@@ -776,6 +782,10 @@ def add_invertible_flag(flag: str,
776782
if options.quick_and_dirty:
777783
options.incremental = True
778784

785+
# Let logical_deps imply cache_fine_grained (otherwise the former is useless).
786+
if options.logical_deps:
787+
options.cache_fine_grained = True
788+
779789
# Set target.
780790
if special_opts.modules + special_opts.packages:
781791
options.build_type = BuildType.MODULE

mypy/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ def __init__(self) -> None:
193193
self.show_column_numbers = False # type: bool
194194
self.dump_graph = False
195195
self.dump_deps = False
196+
self.logical_deps = False
196197
# If True, partial types can't span a module top level and a function
197198
self.local_partial_types = False
198199
# Some behaviors are changed when using Bazel (https://bazel.build).

mypy/server/deps.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,15 @@ class 'mod.Cls'. This can also refer to an attribute inherited from a
105105
from mypy.util import correct_relative_import
106106
from mypy.scope import Scope
107107
from mypy.typestate import TypeState
108+
from mypy.options import Options
108109

109110

110111
def get_dependencies(target: MypyFile,
111112
type_map: Dict[Expression, Type],
112-
python_version: Tuple[int, int]) -> Dict[str, Set[str]]:
113+
python_version: Tuple[int, int],
114+
options: Options) -> Dict[str, Set[str]]:
113115
"""Get all dependencies of a node, recursively."""
114-
visitor = DependencyVisitor(type_map, python_version, target.alias_deps)
116+
visitor = DependencyVisitor(type_map, python_version, target.alias_deps, options)
115117
target.accept(visitor)
116118
return visitor.map
117119

@@ -148,7 +150,8 @@ class DependencyVisitor(TraverserVisitor):
148150
def __init__(self,
149151
type_map: Dict[Expression, Type],
150152
python_version: Tuple[int, int],
151-
alias_deps: 'DefaultDict[str, Set[str]]') -> None:
153+
alias_deps: 'DefaultDict[str, Set[str]]',
154+
options: Optional[Options] = None) -> None:
152155
self.scope = Scope()
153156
self.type_map = type_map
154157
self.python2 = python_version[0] == 2
@@ -165,6 +168,7 @@ def __init__(self,
165168
self.map = {} # type: Dict[str, Set[str]]
166169
self.is_class = False
167170
self.is_package_init_file = False
171+
self.options = options
168172

169173
def visit_mypy_file(self, o: MypyFile) -> None:
170174
self.scope.enter_file(o.fullname())
@@ -201,6 +205,22 @@ def visit_decorator(self, o: Decorator) -> None:
201205
# generate dependency.
202206
if not o.func.is_overload and self.scope.current_function_name() is None:
203207
self.add_dependency(make_trigger(o.func.fullname()))
208+
if self.options is not None and self.options.logical_deps:
209+
# Add logical dependencies from decorators to the function. For example,
210+
# if we have
211+
# @dec
212+
# def func(): ...
213+
# then if `dec` is unannotated, then it will "spoil" `func` and consequently
214+
# all call sites, making them all `Any`.
215+
for d in o.decorators:
216+
tname = None # type: Optional[str]
217+
if isinstance(d, RefExpr) and d.fullname is not None:
218+
tname = d.fullname
219+
if (isinstance(d, CallExpr) and isinstance(d.callee, RefExpr) and
220+
d.callee.fullname is not None):
221+
tname = d.callee.fullname
222+
if tname is not None:
223+
self.add_dependency(make_trigger(tname), make_trigger(o.func.fullname()))
204224
super().visit_decorator(o)
205225

206226
def visit_class_def(self, o: ClassDef) -> None:
@@ -263,6 +283,18 @@ def process_type_info(self, info: TypeInfo) -> None:
263283
target=make_trigger(info.fullname() + '.' + name))
264284
for base_info in non_trivial_bases(info):
265285
for name, node in base_info.names.items():
286+
if self.options and self.options.logical_deps:
287+
# Skip logical dependency if an attribute is not overridden. For example,
288+
# in case of:
289+
# class Base:
290+
# x = 1
291+
# y = 2
292+
# class Sub(Base):
293+
# x = 3
294+
# we skip <Base.y> -> <Child.y>, because even if `y` is unannotated it
295+
# doesn't affect precision of Liskov checking.
296+
if name not in info.names:
297+
continue
266298
self.add_dependency(make_trigger(base_info.fullname() + '.' + name),
267299
target=make_trigger(info.fullname() + '.' + name))
268300
self.add_dependency(make_trigger(base_info.fullname() + '.__init__'),
@@ -368,6 +400,28 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None:
368400
if o.type:
369401
for trigger in get_type_triggers(o.type):
370402
self.add_dependency(trigger)
403+
if self.options and self.options.logical_deps and o.unanalyzed_type is None:
404+
# Special case: for definitions without an explicit type like this:
405+
# x = func(...)
406+
# we add a logical dependency <func> -> <x>, because if `func` is not annotated,
407+
# then it will make all points of use of `x` unchecked.
408+
if (isinstance(rvalue, CallExpr) and isinstance(rvalue.callee, RefExpr)
409+
and rvalue.callee.fullname is not None):
410+
fname = None # type: Optional[str]
411+
if isinstance(rvalue.callee.node, TypeInfo):
412+
# use actual __init__ as a dependency source
413+
init = rvalue.callee.node.get('__init__')
414+
if init and isinstance(init.node, FuncBase):
415+
fname = init.node.fullname()
416+
else:
417+
fname = rvalue.callee.fullname
418+
if fname is None:
419+
return
420+
for lv in o.lvalues:
421+
if isinstance(lv, RefExpr) and lv.fullname and lv.is_new_def:
422+
if lv.kind == LDEF:
423+
return # local definitions don't generate logical deps
424+
self.add_dependency(make_trigger(fname), make_trigger(lv.fullname))
371425

372426
def process_lvalue(self, lvalue: Expression) -> None:
373427
"""Generate additional dependencies for an lvalue."""
@@ -815,7 +869,8 @@ def has_user_bases(info: TypeInfo) -> bool:
815869

816870
def dump_all_dependencies(modules: Dict[str, MypyFile],
817871
type_map: Dict[Expression, Type],
818-
python_version: Tuple[int, int]) -> None:
872+
python_version: Tuple[int, int],
873+
options: Options) -> None:
819874
"""Generate dependencies for all interesting modules and print them to stdout."""
820875
all_deps = {} # type: Dict[str, Set[str]]
821876
for id, node in modules.items():
@@ -824,7 +879,7 @@ def dump_all_dependencies(modules: Dict[str, MypyFile],
824879
if id in ('builtins', 'typing') or '/typeshed/' in node.path:
825880
continue
826881
assert id == node.fullname()
827-
deps = get_dependencies(node, type_map, python_version)
882+
deps = get_dependencies(node, type_map, python_version, options)
828883
for trigger, targets in deps.items():
829884
all_deps.setdefault(trigger, set()).update(targets)
830885
TypeState.add_all_protocol_deps(all_deps)

mypy/test/testdeps.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from mypy.server.deps import get_dependencies
1616
from mypy.test.config import test_temp_dir
1717
from mypy.test.data import DataDrivenTestCase, DataSuite
18-
from mypy.test.helpers import assert_string_arrays_equal
18+
from mypy.test.helpers import assert_string_arrays_equal, parse_options
1919
from mypy.types import Type
2020
from mypy.typestate import TypeState
2121

@@ -41,7 +41,13 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
4141
python_version = defaults.PYTHON2_VERSION
4242
else:
4343
python_version = defaults.PYTHON3_VERSION
44-
messages, files, type_map = self.build(src, python_version)
44+
options = parse_options(src, testcase, incremental_step=1)
45+
options.use_builtins_fixtures = True
46+
options.show_traceback = True
47+
options.cache_dir = os.devnull
48+
options.python_version = python_version
49+
options.export_types = True
50+
messages, files, type_map = self.build(src, options)
4551
a = messages
4652
if files is None or type_map is None:
4753
if not a:
@@ -53,7 +59,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
5359
'typing',
5460
'mypy_extensions',
5561
'enum'):
56-
new_deps = get_dependencies(files[module], type_map, python_version)
62+
new_deps = get_dependencies(files[module], type_map, python_version, options)
5763
for source in new_deps:
5864
deps[source].update(new_deps[source])
5965

@@ -75,15 +81,9 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
7581

7682
def build(self,
7783
source: str,
78-
python_version: Tuple[int, int]) -> Tuple[List[str],
79-
Optional[Dict[str, MypyFile]],
80-
Optional[Dict[Expression, Type]]]:
81-
options = Options()
82-
options.use_builtins_fixtures = True
83-
options.show_traceback = True
84-
options.cache_dir = os.devnull
85-
options.python_version = python_version
86-
options.export_types = True
84+
options: Options) -> Tuple[List[str],
85+
Optional[Dict[str, MypyFile]],
86+
Optional[Dict[Expression, Type]]]:
8787
try:
8888
result = build.build(sources=[BuildSource('main', None, source)],
8989
options=options,

test-data/unit/deps.test

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,3 +1134,118 @@ def f(b: B) -> None:
11341134
<m.B.__setitem__> -> m.f
11351135
<m.B> -> <m.f>, m.B, m.f
11361136
<m.g> -> m.f
1137+
1138+
[case testLogicalDecorator]
1139+
# flags: --logical-deps
1140+
from mod import dec
1141+
@dec
1142+
def f() -> None:
1143+
pass
1144+
[file mod.py]
1145+
from typing import Callable
1146+
def dec(f: Callable[[], None]) -> Callable[[], None]:
1147+
pass
1148+
[out]
1149+
<m.f> -> m
1150+
<mod.dec> -> <m.f>, m
1151+
1152+
[case testLogicalDecoratorWithArgs]
1153+
# flags: --logical-deps
1154+
from mod import dec
1155+
@dec(str())
1156+
def f() -> None:
1157+
pass
1158+
[file mod.py]
1159+
from typing import Callable
1160+
def dec(arg: str) -> Callable[[Callable[[], None]], Callable[[], None]]:
1161+
pass
1162+
[out]
1163+
<m.f> -> m
1164+
<mod.dec> -> <m.f>, m
1165+
1166+
[case testLogicalDecoratorMember]
1167+
# flags: --logical-deps
1168+
import mod
1169+
@mod.dec
1170+
def f() -> None:
1171+
pass
1172+
[file mod.py]
1173+
from typing import Callable
1174+
def dec(f: Callable[[], None]) -> Callable[[], None]:
1175+
pass
1176+
[out]
1177+
<m.f> -> m
1178+
<mod.dec> -> <m.f>, m
1179+
<mod> -> m
1180+
1181+
[case testLogicalDefinition]
1182+
# flags: --logical-deps
1183+
from mod import func
1184+
b = func()
1185+
[file mod.py]
1186+
def func() -> int:
1187+
pass
1188+
[out]
1189+
<m.b> -> m
1190+
<mod.func> -> <m.b>, m
1191+
1192+
[case testLogicalDefinitionIrrelevant]
1193+
# flags: --logical-deps
1194+
from mod import func
1195+
def outer() -> None:
1196+
a = func()
1197+
b: int = func()
1198+
c = int()
1199+
c = func()
1200+
[file mod.py]
1201+
def func() -> int:
1202+
pass
1203+
[out]
1204+
<m.b> -> m
1205+
<m.c> -> m
1206+
<mod.func> -> m, m.outer
1207+
1208+
[case testLogicalDefinitionMember]
1209+
# flags: --logical-deps
1210+
import mod
1211+
b = mod.func()
1212+
[file mod.py]
1213+
def func() -> int:
1214+
pass
1215+
[out]
1216+
<m.b> -> m
1217+
<mod.func> -> <m.b>, m
1218+
<mod> -> m
1219+
1220+
[case testLogicalDefinitionClass]
1221+
# flags: --logical-deps
1222+
from mod import Cls
1223+
b = Cls()
1224+
[file mod.py]
1225+
class Base:
1226+
def __init__(self) -> None: pass
1227+
class Cls(Base): pass
1228+
[out]
1229+
<m.b> -> m
1230+
<mod.Base.__init__> -> <m.b>
1231+
<mod.Cls.__init__> -> m
1232+
<mod.Cls.__new__> -> m
1233+
<mod.Cls> -> <m.b>, m
1234+
1235+
[case testLogicalBaseAttribute]
1236+
# flags: --logical-deps
1237+
from mod import C
1238+
class D(C):
1239+
x: int
1240+
[file mod.py]
1241+
class C:
1242+
x: int
1243+
y: int
1244+
[out]
1245+
<m.D.x> -> m
1246+
<m.D> -> m.D
1247+
<mod.C.(abstract)> -> <m.D.__init__>, m
1248+
<mod.C.__init__> -> <m.D.__init__>
1249+
<mod.C.__new__> -> <m.D.__new__>
1250+
<mod.C.x> -> <m.D.x>
1251+
<mod.C> -> m, m.D

0 commit comments

Comments
 (0)