Skip to content

Commit 7e96fd7

Browse files
committed
flag abstract base classes with no abstract methods
1 parent 264b247 commit 7e96fd7

3 files changed

Lines changed: 91 additions & 1 deletion

File tree

packages/pyright-internal/src/analyzer/typeEvaluator.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10786,9 +10786,18 @@ export function createTypeEvaluator(
1078610786

1078710787
if (ClassType.supportsAbstractMethods(expandedCallType)) {
1078810788
const abstractSymbols = getAbstractSymbols(expandedCallType);
10789+
// Check if ABC is in the direct base classes (not just anywhere in MRO)
10790+
const derivesDirectlyFromABC = expandedCallType.shared.baseClasses.some(
10791+
(baseClass) => isInstantiableClass(baseClass) && baseClass.shared.fullName == 'abc.ABC'
10792+
);
10793+
// Check if the class uses ABCMeta as its metaclass
10794+
const hasABCMetaMetaclass =
10795+
expandedCallType.shared.declaredMetaclass &&
10796+
isInstantiableClass(expandedCallType.shared.declaredMetaclass) &&
10797+
expandedCallType.shared.declaredMetaclass.shared.fullName == 'abc.ABCMeta';
1078910798

1079010799
if (
10791-
abstractSymbols.length > 0 &&
10800+
(abstractSymbols.length > 0 || derivesDirectlyFromABC || hasABCMetaMetaclass) &&
1079210801
!expandedCallType.priv.includeSubclasses &&
1079310802
!isTypeVar(unexpandedCallType)
1079410803
) {

packages/pyright-internal/src/tests/checker.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ test('AbstractClass11', () => {
128128
TestUtils.validateResults(analysisResults, 2);
129129
});
130130

131+
test('AbstractClass12', () => {
132+
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass12.py']);
133+
134+
TestUtils.validateResultsButBased(analysisResults, {});
135+
});
136+
131137
test('Constants1', () => {
132138
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['constants1.py']);
133139

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# this sample tests the type analyzer's ability to flag attempts
2+
# to instantiate abstract base classes that have no abstract methods
3+
4+
from abc import ABC, ABCMeta
5+
6+
7+
class AbstractFoo(ABC):
8+
"""an abstract class with no abstract methods"""
9+
10+
class Foo(AbstractFoo):
11+
"""a concrete subclass"""
12+
13+
# this should generate an error because `AbstractFoo`
14+
# is an abstract class even though it has no abstract methods
15+
a = AbstractFoo() # pyright: ignore[reportAbstractUsage]
16+
17+
# this should not generate an error because Foo is concrete
18+
b = Foo()
19+
20+
21+
class NotAbstract:
22+
"""a regular class that doesn't derive from ABC"""
23+
24+
# This should not generate an error because NotAbstract is not abstract
25+
e = NotAbstract()
26+
27+
28+
class IndirectlyAbstract(AbstractFoo):
29+
"""inherits from an abstract class but doesn't override anything"""
30+
31+
32+
# this should not generate an error because IndirectlyAbstract
33+
# doesn't directly inherit from ABC (only through AbstractFoo)
34+
f = IndirectlyAbstract()
35+
36+
37+
class ConcreteClass(AbstractFoo):
38+
"""a concrete class that properly inherits from AbstractFoo"""
39+
40+
41+
# This should not generate an error
42+
g = ConcreteClass()
43+
44+
45+
class AbstractWithMethods(ABC):
46+
"""abstract class with no abstract methods but regular methods"""
47+
48+
def regular_method(self):
49+
return 1
50+
51+
@classmethod
52+
def class_method(cls):
53+
return 2
54+
55+
@staticmethod
56+
def static_method():
57+
return 3
58+
59+
60+
# This should generate an error even though all methods are concrete
61+
h = AbstractWithMethods() # pyright: ignore[reportAbstractUsage]
62+
63+
64+
class AbstractWithMetaclass(metaclass=ABCMeta):
65+
"""abstract class using ABCMeta metaclass with no abstract methods"""
66+
67+
class ConcreteWithMetaclass(AbstractWithMetaclass):
68+
"""concrete subclass of AbstractWithMetaclass"""
69+
70+
71+
# this should generate an error because AbstractWithMetaclass uses ABCMeta
72+
i = AbstractWithMetaclass() # pyright: ignore[reportAbstractUsage]
73+
74+
# this should not generate an error
75+
j = ConcreteWithMetaclass()

0 commit comments

Comments
 (0)