Skip to content

Commit 2f99e09

Browse files
brandtbucherGuido van Rossum
authored and
Guido van Rossum
committed
Fix class/def line reporting and ignoring for 3.8+ (#6753)
Fixes #3871. This change fixes a few things concerning class/function definition line numbers. For users running mypy with Python 3.8+: - Function definitions are now reported on the correct line, rather than the line of the first decorator plus the number of decorators. - Class definitions are now reported on the correct line, rather than the line of the first decorator. - **These changes are fully backward compatible with respect to `# type: ignore` lines**. In other words, existing comments will _not_ need to be moved. This is accomplished by defining an ignore "scope" just like we added to expressions with #6648. See the recent discussion at #3871 for reasoning. For other users (running mypy with Python 3.7 or earlier): - Class definition lines are reported by using the same decorator-offset estimate as functions. This is both more accurate, and necessary to get 15 or so tests to pass for both pre- and post-3.8 versions. This seems fine, since there [doesn't appear](#6539 (comment)) to be a compelling reason why it was different in the first place. - **This change is also backward-compatible with existing ignores, as above.** About 15 tests had to have their error messages / nodes moved down one line, and 4 new regression tests have been added to `check-38.test`.
1 parent ac70b55 commit 2f99e09

14 files changed

+101
-49
lines changed

mypy/errors.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,14 @@ def add_error_info(self, info: ErrorInfo) -> None:
287287
file, line, end_line = info.origin
288288
if not info.blocker: # Blockers cannot be ignored
289289
if file in self.ignored_lines:
290+
# It's okay if end_line is *before* line.
291+
# Function definitions do this, for example, because the correct
292+
# error reporting line is at the *end* of the ignorable range
293+
# (for compatibility reasons). If so, just flip 'em!
294+
if end_line < line:
295+
line, end_line = end_line, line
290296
# Check each line in this context for "type: ignore" comments.
291-
# For anything other than Python 3.8 expressions, line == end_line,
292-
# so we only loop once.
297+
# line == end_line for most nodes, so we only loop once.
293298
for scope_line in range(line, end_line + 1):
294299
if scope_line in self.ignored_lines[file]:
295300
# Annotation requests us to ignore all errors on this line.

mypy/fastparse.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -524,13 +524,18 @@ def do_func_def(self, n: Union[ast3.FunctionDef, ast3.AsyncFunctionDef],
524524
# Before 3.8, [typed_]ast the line number points to the first decorator.
525525
# In 3.8, it points to the 'def' line, where we want it.
526526
lineno += len(n.decorator_list)
527+
end_lineno = None
528+
else:
529+
# Set end_lineno to the old pre-3.8 lineno, in order to keep
530+
# existing "# type: ignore" comments working:
531+
end_lineno = n.decorator_list[0].lineno + len(n.decorator_list)
527532

528533
var = Var(func_def.name())
529534
var.is_ready = False
530535
var.set_line(lineno)
531536

532537
func_def.is_decorated = True
533-
func_def.set_line(lineno, n.col_offset)
538+
func_def.set_line(lineno, n.col_offset, end_lineno)
534539
func_def.body.set_line(lineno) # TODO: Why?
535540

536541
deco = Decorator(func_def, self.translate_expr_list(n.decorator_list), var)
@@ -633,15 +638,15 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
633638
metaclass=dict(keywords).get('metaclass'),
634639
keywords=keywords)
635640
cdef.decorators = self.translate_expr_list(n.decorator_list)
636-
if n.decorator_list and sys.version_info >= (3, 8):
637-
# Before 3.8, n.lineno points to the first decorator; in
638-
# 3.8, it points to the 'class' statement. We always make
639-
# it point to the first decorator. (The node structure
640-
# here is different than for decorated functions.)
641-
cdef.line = n.decorator_list[0].lineno
642-
cdef.column = n.col_offset
641+
# Set end_lineno to the old mypy 0.700 lineno, in order to keep
642+
# existing "# type: ignore" comments working:
643+
if sys.version_info < (3, 8):
644+
cdef.line = n.lineno + len(n.decorator_list)
645+
cdef.end_line = n.lineno
643646
else:
644-
self.set_line(cdef, n)
647+
cdef.line = n.lineno
648+
cdef.end_line = n.decorator_list[0].lineno if n.decorator_list else None
649+
cdef.column = n.col_offset
645650
self.class_and_function_stack.pop()
646651
return cdef
647652

mypy/fastparse2.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,9 @@ def visit_ClassDef(self, n: ast27.ClassDef) -> ClassDef:
529529
self.translate_expr_list(n.bases),
530530
metaclass=None)
531531
cdef.decorators = self.translate_expr_list(n.decorator_list)
532-
self.set_line(cdef, n)
532+
cdef.line = n.lineno + len(n.decorator_list)
533+
cdef.column = n.col_offset
534+
cdef.end_line = n.lineno
533535
self.class_and_function_stack.pop()
534536
return cdef
535537

mypy/nodes.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ def __init__(self, line: int = -1, column: int = -1) -> None:
2929
self.column = column
3030
self.end_line = None # type: Optional[int]
3131

32-
def set_line(self, target: Union['Context', int], column: Optional[int] = None) -> None:
32+
def set_line(self,
33+
target: Union['Context', int],
34+
column: Optional[int] = None,
35+
end_line: Optional[int] = None) -> None:
3336
"""If target is a node, pull line (and column) information
3437
into this node. If column is specified, this will override any column
3538
information coming from a node.
@@ -44,6 +47,9 @@ def set_line(self, target: Union['Context', int], column: Optional[int] = None)
4447
if column is not None:
4548
self.column = column
4649

50+
if end_line is not None:
51+
self.end_line = end_line
52+
4753
def get_line(self) -> int:
4854
"""Don't use. Use x.line."""
4955
return self.line
@@ -534,13 +540,16 @@ def __init__(self,
534540
self.initializer = initializer
535541
self.kind = kind # must be an ARG_* constant
536542

537-
def set_line(self, target: Union[Context, int], column: Optional[int] = None) -> None:
538-
super().set_line(target, column)
543+
def set_line(self,
544+
target: Union[Context, int],
545+
column: Optional[int] = None,
546+
end_line: Optional[int] = None) -> None:
547+
super().set_line(target, column, end_line)
539548

540549
if self.initializer:
541-
self.initializer.set_line(self.line, self.column)
550+
self.initializer.set_line(self.line, self.column, self.end_line)
542551

543-
self.variable.set_line(self.line, self.column)
552+
self.variable.set_line(self.line, self.column, self.end_line)
544553

545554

546555
FUNCITEM_FLAGS = FUNCBASE_FLAGS + [
@@ -595,10 +604,13 @@ def __init__(self,
595604
def max_fixed_argc(self) -> int:
596605
return self.max_pos
597606

598-
def set_line(self, target: Union[Context, int], column: Optional[int] = None) -> None:
599-
super().set_line(target, column)
607+
def set_line(self,
608+
target: Union[Context, int],
609+
column: Optional[int] = None,
610+
end_line: Optional[int] = None) -> None:
611+
super().set_line(target, column, end_line)
600612
for arg in self.arguments:
601-
arg.set_line(self.line, self.column)
613+
arg.set_line(self.line, self.column, self.end_line)
602614

603615
def is_dynamic(self) -> bool:
604616
return self.type is None

test-data/unit/check-38.test

+28
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
[case testDecoratedClassLine]
2+
def d(c): ...
3+
@d
4+
5+
class C: ...
6+
class C: ... # E: Name 'C' already defined on line 4
7+
8+
[case testDecoratedFunctionLine]
9+
# flags: --disallow-untyped-defs
10+
def d(f): ... # type: ignore
11+
@d
12+
13+
def f(): ... # E: Function is missing a type annotation
14+
15+
[case testIgnoreDecoratedFunction1]
16+
# flags: --disallow-untyped-defs --warn-unused-ignores
17+
def d(f): ... # type: ignore
18+
@d
19+
# type: ignore
20+
def f(): ... # type: ignore # E: unused 'type: ignore' comment
21+
22+
[case testIgnoreDecoratedFunction2]
23+
# flags: --disallow-untyped-defs
24+
def d(f): ... # type: ignore
25+
@d
26+
27+
def f(): ... # type: ignore
28+
129
[case testIgnoreScopeIssue1032]
230
def f(a: int): ...
331
f(

test-data/unit/check-attr.test

+6-6
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ A(1, [2], '3', 4, 5) # E: Too many arguments for "A"
7474
[case testAttrsUntypedNoUntypedDefs]
7575
# flags: --disallow-untyped-defs
7676
import attr
77-
@attr.s # E: Function is missing a type annotation for one or more arguments
78-
class A:
77+
@attr.s
78+
class A: # E: Function is missing a type annotation for one or more arguments
7979
a = attr.ib() # E: Need type annotation for 'a'
8080
_b = attr.ib() # E: Need type annotation for '_b'
8181
c = attr.ib(18) # E: Need type annotation for 'c'
@@ -785,8 +785,8 @@ import attr
785785
@attr.s
786786
class Good(object):
787787
pass
788-
@attr.s # E: attrs only works with new-style classes
789-
class Bad:
788+
@attr.s
789+
class Bad: # E: attrs only works with new-style classes
790790
pass
791791
[builtins_py2 fixtures/bool.pyi]
792792

@@ -1043,8 +1043,8 @@ reveal_type(B) # E: Revealed type is 'def (x: __main__.C) -> __main__.B'
10431043
# flags: --disallow-untyped-defs
10441044
import attr
10451045

1046-
@attr.s # E: Function is missing a type annotation for one or more arguments
1047-
class B:
1046+
@attr.s
1047+
class B: # E: Function is missing a type annotation for one or more arguments
10481048
x = attr.ib() # E: Need type annotation for 'x'
10491049

10501050
reveal_type(B) # E: Revealed type is 'def (x: Any) -> __main__.B'

test-data/unit/check-classes.test

+12-12
Original file line numberDiff line numberDiff line change
@@ -4850,19 +4850,19 @@ class C3(six.with_metaclass(A)): pass # E: Metaclasses not inheriting from 'typ
48504850
# E: Metaclasses not inheriting from 'type' are not supported
48514851
class D3(A): pass
48524852
class C4(six.with_metaclass(M), metaclass=M): pass # E: Multiple metaclass definitions
4853-
@six.add_metaclass(M) # E: Multiple metaclass definitions
4854-
class D4(metaclass=M): pass
4853+
@six.add_metaclass(M)
4854+
class D4(metaclass=M): pass # E: Multiple metaclass definitions
48554855
class C5(six.with_metaclass(f())): pass # E: Dynamic metaclass not supported for 'C5'
48564856
@six.add_metaclass(f()) # E: Dynamic metaclass not supported for 'D5'
48574857
class D5: pass
48584858

4859-
@six.add_metaclass(M) # E: Multiple metaclass definitions
4860-
class CD(six.with_metaclass(M)): pass
4859+
@six.add_metaclass(M)
4860+
class CD(six.with_metaclass(M)): pass # E: Multiple metaclass definitions
48614861

48624862
class M1(type): pass
48634863
class Q1(metaclass=M1): pass
4864-
@six.add_metaclass(M) # E: Inconsistent metaclass structure for 'CQA'
4865-
class CQA(Q1): pass
4864+
@six.add_metaclass(M)
4865+
class CQA(Q1): pass # E: Inconsistent metaclass structure for 'CQA'
48664866
class CQW(six.with_metaclass(M, Q1)): pass # E: Inconsistent metaclass structure for 'CQW'
48674867

48684868
[case testSixMetaclassErrors_python2]
@@ -4972,20 +4972,20 @@ def decorate_forward_ref() -> Callable[[Type[A]], Type[A]]:
49724972
@decorate(11)
49734973
class A: pass
49744974

4975-
@decorate # E: Argument 1 to "decorate" has incompatible type "Type[A2]"; expected "int"
4976-
class A2: pass
4975+
@decorate
4976+
class A2: pass # E: Argument 1 to "decorate" has incompatible type "Type[A2]"; expected "int"
49774977

49784978
[case testClassDecoratorIncorrect]
49794979
def not_a_class_decorator(x: int) -> int: ...
4980-
@not_a_class_decorator(7) # E: "int" not callable
4981-
class A3: pass
4980+
@not_a_class_decorator(7)
4981+
class A3: pass # E: "int" not callable
49824982

49834983
not_a_function = 17
49844984
@not_a_function() # E: "int" not callable
49854985
class B: pass
49864986

4987-
@not_a_function # E: "int" not callable
4988-
class B2: pass
4987+
@not_a_function
4988+
class B2: pass # E: "int" not callable
49894989

49904990
b = object()
49914991
@b.nothing # E: "object" has no attribute "nothing"

test-data/unit/check-dataclasses.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,8 @@ app1 >= app3
387387
# flags: --python-version 3.6
388388
from dataclasses import dataclass
389389

390-
@dataclass(eq=False, order=True) # E: eq must be True if order is True
391-
class Application:
390+
@dataclass(eq=False, order=True)
391+
class Application: # E: eq must be True if order is True
392392
...
393393

394394
[builtins fixtures/list.pyi]

test-data/unit/check-incremental.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -3267,9 +3267,9 @@ def foo() -> None:
32673267
reveal_type(A)
32683268
[builtins fixtures/list.pyi]
32693269
[out1]
3270-
main:8: error: Revealed type is 'def (x: builtins.str) -> __main__.A@5'
3270+
main:8: error: Revealed type is 'def (x: builtins.str) -> __main__.A@6'
32713271
[out2]
3272-
main:8: error: Revealed type is 'def (x: builtins.str) -> __main__.A@5'
3272+
main:8: error: Revealed type is 'def (x: builtins.str) -> __main__.A@6'
32733273

32743274
[case testAttrsIncrementalConverterInSubmoduleForwardRef]
32753275
from a.a import A

test-data/unit/check-protocols.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -1486,8 +1486,8 @@ class C(Protocol):
14861486
[case testSimpleRuntimeProtocolCheck]
14871487
from typing import Protocol, runtime
14881488

1489-
@runtime # E: @runtime can only be used with protocol classes
1490-
class C:
1489+
@runtime
1490+
class C: # E: @runtime can only be used with protocol classes
14911491
pass
14921492

14931493
class P(Protocol):

test-data/unit/parse-python2.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@ class C:
754754
pass
755755
[out]
756756
MypyFile:1(
757-
ClassDef:1(
757+
ClassDef:2(
758758
C
759759
Decorators(
760760
CallExpr:1(

test-data/unit/parse.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -2778,12 +2778,12 @@ class X: pass
27782778
class Z: pass
27792779
[out]
27802780
MypyFile:1(
2781-
ClassDef:1(
2781+
ClassDef:2(
27822782
X
27832783
Decorators(
27842784
NameExpr(foo))
27852785
PassStmt:2())
2786-
ClassDef:3(
2786+
ClassDef:5(
27872787
Z
27882788
Decorators(
27892789
CallExpr:3(

test-data/unit/semanal-classes.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ class A: pass
524524
[out]
525525
MypyFile:1(
526526
Import:1(typing)
527-
ClassDef:2(
527+
ClassDef:3(
528528
A
529529
Decorators(
530530
NameExpr(object [builtins.object]))

test-data/unit/semanal-types.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -1375,7 +1375,7 @@ class S: pass
13751375
[out]
13761376
MypyFile:1(
13771377
ImportFrom:1(typing, [_promote])
1378-
ClassDef:2(
1378+
ClassDef:3(
13791379
S
13801380
Promote(builtins.str)
13811381
Decorators(

0 commit comments

Comments
 (0)