Skip to content

Commit 697a956

Browse files
authored
Fine-grained: Trigger when abstract attributes change (#4977)
Add new kind of trigger `<ClsName.(abstract)>` which gets triggered when the set of abstract attributes in a class change. This is a more efficient alternative to #4922.
1 parent 7b257c8 commit 697a956

File tree

9 files changed

+155
-0
lines changed

9 files changed

+155
-0
lines changed

mypy/server/astdiff.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ def snapshot_definition(node: Optional[SymbolNode],
211211
snapshot_optional_type(node._promote))
212212
prefix = node.fullname()
213213
symbol_table = snapshot_symbol_table(prefix, node.names)
214+
# Special dependency for abstract attribute handling.
215+
symbol_table['(abstract)'] = ('Abstract', tuple(sorted(node.abstract_attributes)))
214216
return ('TypeInfo', common, attrs, symbol_table)
215217
else:
216218
# Other node types are handled elsewhere.

mypy/server/deps.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,18 @@ def process_type_info(self, info: TypeInfo) -> None:
254254
target=make_trigger(info.fullname() + '.__init__'))
255255
self.add_dependency(make_trigger(base_info.fullname() + '.__new__'),
256256
target=make_trigger(info.fullname() + '.__new__'))
257+
# If the set of abstract attributes change, this may invalidate class
258+
# instantiation, or change the generated error message, since Python checks
259+
# class abstract status when creating an instance.
260+
#
261+
# TODO: We should probably add this dependency only from the __init__ of the
262+
# current class, and independent of bases (to trigger changes in message
263+
# wording, as errors may enumerate all abstract attributes).
264+
self.add_dependency(make_trigger(base_info.fullname() + '.(abstract)'),
265+
target=make_trigger(info.fullname() + '.__init__'))
266+
# If the base class abstract attributes change, subclass abstract
267+
# attributes need to be recalculated.
268+
self.add_dependency(make_trigger(base_info.fullname() + '.(abstract)'))
257269

258270
def visit_import(self, o: Import) -> None:
259271
for id, as_id in o.ids:

test-data/unit/deps-classes.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class D:
115115
class S2(C): pass
116116
[out]
117117
-- TODO: Is it okay to have targets like m.S1@4.__init__?
118+
<m.C.(abstract)> -> <m.S1@4.__init__>, <m.S2@8.__init__>, m.D.g, m.f
118119
<m.C.__init__> -> <m.S1@4.__init__>, <m.S2@8.__init__>
119120
<m.C.__new__> -> <m.S1@4.__new__>, <m.S2@8.__new__>
120121
<m.C> -> m.C, m.D.g, m.f
@@ -132,6 +133,7 @@ class D(C):
132133
super().__init__(x)
133134
super().foo()
134135
[out]
136+
<m.C.(abstract)> -> <m.D.__init__>, m
135137
<m.C.__init__> -> <m.D.__init__>, m.D.__init__
136138
<m.C.__new__> -> <m.D.__new__>
137139
<m.C.foo> -> <m.D.foo>
@@ -150,6 +152,7 @@ class D(C):
150152
def foo() -> None:
151153
D(6)
152154
[out]
155+
<m.C.(abstract)> -> <m.D.__init__>, m
153156
<m.C.__init__> -> <m.D.__init__>
154157
<m.C.__new__> -> <m.D.__new__>
155158
<m.C> -> m, m.C, m.D

test-data/unit/deps-generics.test

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class A(Generic[T]): pass
9393
class B(A[C]): pass
9494
class C: pass
9595
[out]
96+
<m.A.(abstract)> -> <m.B.__init__>, m
9697
<m.A.__init__> -> <m.B.__init__>
9798
<m.A.__new__> -> <m.B.__new__>
9899
<m.A> -> m.A, m.B
@@ -108,6 +109,7 @@ T = TypeVar('T')
108109
class A(Generic[T]): pass
109110
class B(A[T]): pass
110111
[out]
112+
<m.A.(abstract)> -> <m.B.__init__>, m
111113
<m.A.__init__> -> <m.B.__init__>
112114
<m.A.__new__> -> <m.B.__new__>
113115
<m.A> -> m.A, m.B

test-data/unit/deps-statements.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ def f(x: A) -> A:
581581
def g() -> None: pass
582582
[builtins fixtures/isinstancelist.pyi]
583583
[out]
584+
<m.A.(abstract)> -> <m.B.__init__>, m
584585
<m.A.__init__> -> <m.B.__init__>
585586
<m.A.__new__> -> <m.B.__new__>
586587
<m.A> -> <m.f>, m, m.A, m.B, m.f
@@ -606,6 +607,7 @@ class C:
606607
def g(self) -> None: pass
607608
[builtins fixtures/isinstancelist.pyi]
608609
[out]
610+
<m.A.(abstract)> -> <m.B.__init__>, m
609611
<m.A.__init__> -> <m.B.__init__>
610612
<m.A.__new__> -> <m.B.__new__>
611613
<m.A> -> <m.C.f>, m, m.A, m.B, m.C.f
@@ -674,6 +676,7 @@ class C:
674676
x: int
675677
[out]
676678
<m.N> -> <m.f>, m, m.f
679+
<m.C.(abstract)> -> <m.N.__init__>, m
677680
<m.C.__init__> -> <m.N.__init__>
678681
<m.C.__new__> -> <m.N.__new__>
679682
<m.C.x> -> <m.N.x>

test-data/unit/deps-types.test

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ class I: pass
383383
[out]
384384
<m.C> -> m.C
385385
<a.A> -> m
386+
<mod.I.(abstract)> -> <m.C.__init__>, m
386387
<mod.I.__init__> -> <m.C.__init__>
387388
<mod.I.__new__> -> <m.C.__new__>
388389
<mod.I> -> m, m.C
@@ -571,6 +572,7 @@ class I: pass
571572
<m.A> -> m
572573
<m.B> -> m
573574
<m.C> -> m.C
575+
<mod.I.(abstract)> -> <m.C.__init__>, m
574576
<mod.I.__init__> -> <m.C.__init__>, m
575577
<mod.I.__new__> -> <m.C.__new__>, m
576578
<mod.I> -> m, m.C

test-data/unit/deps.test

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ class A:
110110
class B(A):
111111
pass
112112
[out]
113+
<m.A.(abstract)> -> <m.B.__init__>, m
113114
<m.A.__init__> -> <m.B.__init__>
114115
<m.A.__new__> -> <m.B.__new__>
115116
<m.A> -> m, m.A, m.B
@@ -122,6 +123,7 @@ class B(A):
122123
def f(self) -> None:
123124
self.x = 1
124125
[out]
126+
<m.A.(abstract)> -> <m.B.__init__>, m
125127
<m.A.__init__> -> <m.B.__init__>
126128
<m.A.__new__> -> <m.B.__new__>
127129
<m.A.f> -> m.B.f
@@ -139,11 +141,13 @@ class C(B):
139141
def f(self) -> None:
140142
self.x = 1
141143
[out]
144+
<m.A.(abstract)> -> <m.B.__init__>, <m.C.__init__>, m
142145
<m.A.__init__> -> <m.B.__init__>, <m.C.__init__>
143146
<m.A.__new__> -> <m.B.__new__>, <m.C.__new__>
144147
<m.A.f> -> m.C.f
145148
<m.A.x> -> <m.C.x>
146149
<m.A> -> m, m.A, m.B
150+
<m.B.(abstract)> -> <m.C.__init__>, m
147151
<m.B.__init__> -> <m.C.__init__>
148152
<m.B.__new__> -> <m.C.__new__>
149153
<m.B.f> -> m.C.f
@@ -165,6 +169,7 @@ class A:
165169
[out]
166170
<m.B.x> -> m.B.f
167171
<m.B> -> m.B
172+
<n.A.(abstract)> -> <m.B.__init__>, m
168173
<n.A.__init__> -> <m.B.__init__>
169174
<n.A.__new__> -> <m.B.__new__>
170175
<n.A.f> -> m.B.f
@@ -180,6 +185,7 @@ class B(A):
180185
def f(self) -> None:
181186
self.g()
182187
[out]
188+
<m.A.(abstract)> -> <m.B.__init__>, m
183189
<m.A.__init__> -> <m.B.__init__>
184190
<m.A.__new__> -> <m.B.__new__>
185191
<m.A.f> -> m.B.f
@@ -1083,6 +1089,7 @@ class A(B):
10831089
<m.D.x> -> m
10841090
<m.D> -> m.D
10851091
<mod.A> -> <m.D.x>, m
1092+
<mod.C.(abstract)> -> <m.D.__init__>, m
10861093
<mod.C.__init__> -> <m.D.__init__>
10871094
<mod.C.__new__> -> <m.D.__new__>
10881095
<mod.C.x> -> <m.D.x>
@@ -1105,6 +1112,7 @@ class A(B):
11051112
<m.D.x> -> m.D.__init__
11061113
<m.D> -> m.D
11071114
<mod.A> -> <m.D.x>, m, m.D.__init__
1115+
<mod.C.(abstract)> -> <m.D.__init__>, m
11081116
<mod.C.__init__> -> <m.D.__init__>, m.D.__init__
11091117
<mod.C.__new__> -> <m.D.__new__>
11101118
<mod.C.x> -> <m.D.x>

test-data/unit/diff.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,3 +879,19 @@ def outer() -> None:
879879
def inner(y: B) -> A:
880880
pass
881881
[out]
882+
883+
[case testAddAbstractMethod]
884+
from abc import abstractmethod
885+
class A:
886+
@abstractmethod
887+
def f(self) -> None: pass
888+
[file next.py]
889+
from abc import abstractmethod
890+
class A:
891+
@abstractmethod
892+
def f(self) -> None: pass
893+
@abstractmethod
894+
def g(self) -> None: pass
895+
[out]
896+
__main__.A.(abstract)
897+
__main__.A.g

test-data/unit/fine-grained.test

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6163,3 +6163,110 @@ def deco(f: F) -> F:
61636163
[out]
61646164
==
61656165
main:5: error: Return type of "m" incompatible with supertype "B"
6166+
6167+
[case testAddAbstractMethod]
6168+
from b import D
6169+
D()
6170+
[file b.py]
6171+
from a import C
6172+
class D(C):
6173+
def f(self) -> None: pass
6174+
[file a.py]
6175+
from abc import abstractmethod
6176+
class C:
6177+
@abstractmethod
6178+
def f(self) -> None: pass
6179+
[file a.py.2]
6180+
from abc import abstractmethod
6181+
class C:
6182+
@abstractmethod
6183+
def f(self) -> None: pass
6184+
@abstractmethod
6185+
def g(self) -> None: pass
6186+
[file a.py.3]
6187+
from abc import abstractmethod
6188+
class C:
6189+
@abstractmethod
6190+
def f(self) -> None: pass
6191+
def g(self) -> None: pass
6192+
[out]
6193+
==
6194+
main:2: error: Cannot instantiate abstract class 'D' with abstract attribute 'g'
6195+
==
6196+
6197+
[case testMakeClassAbstract]
6198+
from a import C
6199+
c = C()
6200+
[file a.py]
6201+
from abc import abstractmethod
6202+
class C: pass
6203+
[file a.py.2]
6204+
from abc import abstractmethod
6205+
class C:
6206+
@abstractmethod
6207+
def f(self) -> None: pass
6208+
[out]
6209+
==
6210+
main:2: error: Cannot instantiate abstract class 'C' with abstract attribute 'f'
6211+
6212+
[case testMakeMethodNoLongerAbstract1]
6213+
[file z.py]
6214+
from abc import abstractmethod, ABCMeta
6215+
class I(metaclass=ABCMeta):
6216+
@abstractmethod
6217+
def f(self) -> None: pass
6218+
@abstractmethod
6219+
def g(self) -> None: pass
6220+
[file b.py]
6221+
import z
6222+
def x() -> Foo: return Foo()
6223+
class Foo(z.I):
6224+
def f(self) -> None: pass
6225+
def g(self) -> None: pass
6226+
6227+
[file z.py.2]
6228+
from abc import abstractmethod, ABCMeta
6229+
class I(metaclass=ABCMeta):
6230+
def f(self) -> None: pass
6231+
@abstractmethod
6232+
def g(self) -> None: pass
6233+
6234+
[file b.py.2]
6235+
import z
6236+
def x() -> Foo: return Foo()
6237+
class Foo(z.I):
6238+
def g(self) -> None: pass
6239+
[out]
6240+
==
6241+
6242+
[case testMakeMethodNoLongerAbstract2]
6243+
-- this version never failed, but it is just a file-renaming
6244+
-- away from the above test that did
6245+
[file a.py]
6246+
from abc import abstractmethod, ABCMeta
6247+
class I(metaclass=ABCMeta):
6248+
@abstractmethod
6249+
def f(self) -> None: pass
6250+
@abstractmethod
6251+
def g(self) -> None: pass
6252+
[file b.py]
6253+
import a
6254+
def x() -> Foo: return Foo()
6255+
class Foo(a.I):
6256+
def f(self) -> None: pass
6257+
def g(self) -> None: pass
6258+
6259+
[file a.py.2]
6260+
from abc import abstractmethod, ABCMeta
6261+
class I(metaclass=ABCMeta):
6262+
def f(self) -> None: pass
6263+
@abstractmethod
6264+
def g(self) -> None: pass
6265+
6266+
[file b.py.2]
6267+
import a
6268+
def x() -> Foo: return Foo()
6269+
class Foo(a.I):
6270+
def g(self) -> None: pass
6271+
[out]
6272+
==

0 commit comments

Comments
 (0)