Skip to content

Commit 9cf43ff

Browse files
committed
Improve annotations of calls and assignments
1 parent 5212710 commit 9cf43ff

File tree

3 files changed

+80
-10
lines changed

3 files changed

+80
-10
lines changed

mypyc/annotate.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,20 @@
2222
ForStmt,
2323
FuncDef,
2424
GeneratorExpr,
25+
IndexExpr,
2526
LambdaExpr,
2627
MemberExpr,
2728
MypyFile,
29+
NamedTupleExpr,
2830
NameExpr,
31+
NewTypeExpr,
2932
Node,
33+
OpExpr,
3034
RefExpr,
3135
TupleExpr,
36+
TypedDictExpr,
3237
TypeInfo,
38+
TypeVarExpr,
3339
Var,
3440
WithStmt,
3541
)
@@ -204,7 +210,7 @@ def function_annotations(func_ir: FuncIR, tree: MypyFile) -> dict[int, list[Anno
204210
elif name == "PyObject_VectorcallMethod":
205211
method_name = get_str_literal(op.args[0])
206212
if method_name:
207-
ann = f'Call non-native method "{method_name}".'
213+
ann = f'Call non-native method "{method_name}" (it may be defined in a non-native class, or decorated).'
208214
else:
209215
ann = "Dynamic method call."
210216
elif name in op_hints:
@@ -300,6 +306,10 @@ def visit_class_def(self, o: ClassDef, /) -> None:
300306
if isinstance(s, AssignmentStmt):
301307
# Don't complain about attribute initializers
302308
self.ignored_lines.add(s.line)
309+
elif isinstance(s, Decorator):
310+
# Don't complain about decorator definitions that generate some
311+
# dynamic operations. This is a bit heavy-handed.
312+
self.ignored_lines.add(s.func.line)
303313

304314
def visit_with_stmt(self, o: WithStmt, /) -> None:
305315
for expr in o.expr:
@@ -317,10 +327,25 @@ def visit_with_stmt(self, o: WithStmt, /) -> None:
317327
f'"{node.name}" uses @contextmanager, which is slow '
318328
+ "in compiled code. Use a native class with "
319329
+ '"__enter__" and "__exit__" methods instead.',
320-
priority=2,
330+
priority=3,
321331
)
322332
super().visit_with_stmt(o)
323333

334+
def visit_assignment_stmt(self, o: AssignmentStmt, /) -> None:
335+
special_form = False
336+
if self.func_depth == 0:
337+
analyzed = o.rvalue
338+
if isinstance(o.rvalue, (CallExpr, IndexExpr, OpExpr)):
339+
analyzed = o.rvalue.analyzed
340+
if o.is_alias_def or isinstance(
341+
analyzed, (TypeVarExpr, NamedTupleExpr, TypedDictExpr, NewTypeExpr)
342+
):
343+
special_form = True
344+
if special_form:
345+
# TODO: Ignore all lines if multi-line
346+
self.ignored_lines.add(o.line)
347+
super().visit_assignment_stmt(o)
348+
324349
def visit_name_expr(self, o: NameExpr, /) -> None:
325350
if ann := stdlib_hints.get(o.fullname):
326351
self.annotate(o, ann)
@@ -355,8 +380,14 @@ def visit_call_expr(self, o: CallExpr, /) -> None:
355380
+ "constructing an instance is slow.",
356381
2,
357382
)
358-
359-
print(o.callee.node.fullname, info in self.mapper.type_to_ir)
383+
elif isinstance(o.callee, RefExpr) and isinstance(o.callee.node, Decorator):
384+
decorator = o.callee.node
385+
if self.mapper.is_native_ref_expr(o.callee):
386+
self.annotate(
387+
o,
388+
f'Calling a decorated function ("{decorator.name}") is inefficient, even if it\'s native.',
389+
2,
390+
)
360391

361392
def check_isinstance_arg(self, arg: Expression) -> None:
362393
if isinstance(arg, RefExpr):

mypyc/test-data/annotate-basic.test

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,11 @@ def f(x):
109109
from typing import Any
110110

111111
def f1(x):
112-
return x.foo() # A: Call non-native method "foo".
112+
return x.foo() # A: Call non-native method "foo" (it may be defined in a non-native class, or decorated).
113113

114114
def f2(x: Any) -> None:
115-
x.foo(1) # A: Call non-native method "foo".
116-
x.foo(a=1) # A: Call non-native method "foo".
115+
x.foo(1) # A: Call non-native method "foo" (it may be defined in a non-native class, or decorated).
116+
x.foo(a=1) # A: Call non-native method "foo" (it may be defined in a non-native class, or decorated).
117117
t = (1, 'x')
118118
x.foo(*t) # A: Get non-native attribute "foo". Generic call operation.
119119
d = {"a": 1}
@@ -186,7 +186,7 @@ def startswith(x: str) -> bool:
186186
return x.startswith('foo')
187187

188188
def islower(x: str) -> bool:
189-
return x.islower() # A: Call non-native method "islower".
189+
return x.islower() # A: Call non-native method "islower" (it may be defined in a non-native class, or decorated).
190190

191191
[case testAnnotateSpecificStdlibFeatures]
192192
import functools
@@ -351,7 +351,7 @@ from nonnative import C
351351

352352
def f1() -> None:
353353
c = C() # A: Creating an instance of non-native class "C" is slow.
354-
c.foo() # A: Call non-native method "foo".
354+
c.foo() # A: Call non-native method "foo" (it may be defined in a non-native class, or decorated).
355355

356356
class NT(NamedTuple):
357357
x: int
@@ -396,3 +396,40 @@ def f1(x, s: str):
396396

397397
def f2(x, s: str):
398398
setattr(x, s, None) # A: Dynamic attribute set.
399+
400+
[case testAnnotateSpecialAssignments]
401+
from typing import TypeVar, NamedTuple, List, TypedDict, NewType
402+
403+
# Even though these are slow, we don't complain about them since there is generally
404+
# no better way (and at module top level these are very unlikely to be bottlenecks)
405+
A = List[int]
406+
T = TypeVar("T", bound=List[int])
407+
NT = NamedTuple("NT", [("x", List[int])])
408+
TD = TypedDict("TD", {"x": List[int]})
409+
New = NewType("New", List[int])
410+
[typing fixtures/typing-full.pyi]
411+
412+
[case testAnnotateCallDecoratedNativeFunctionOrMethod]
413+
from typing import TypeVar, Callable, Any
414+
415+
F = TypeVar("F", bound=Callable[..., Any])
416+
417+
def mydeco(f: F) -> F:
418+
return f
419+
420+
@mydeco
421+
def d(x: int) -> int:
422+
return x
423+
424+
def f1() -> int:
425+
return d(1) # A: Calling a decorated function ("d") is inefficient, even if it's native.
426+
427+
class C:
428+
@mydeco
429+
def d(self) -> None:
430+
pass
431+
432+
433+
def f2() -> None:
434+
c = C()
435+
c.d() # A: Call non-native method "d" (it may be defined in a non-native class, or decorated).

mypyc/test/test_annotate.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
5050
else:
5151
annotations = generate_annotations("native.py", tree, ir, type_map, mapper)
5252
actual = []
53-
for line_num, line_anns in annotations.annotations.items():
53+
for line_num, line_anns in sorted(
54+
annotations.annotations.items(), key=lambda it: it[0]
55+
):
5456
anns = get_max_prio(line_anns)
5557
str_anns = [a.message for a in anns]
5658
s = " ".join(str_anns)

0 commit comments

Comments
 (0)