Skip to content

Commit 332c215

Browse files
authored
Added special-case logic to detect when a class-like symbol created by calling NewType is used as an actual class for assignability or member accesses. This addresses #10550. (#10562)
1 parent 0f8ded0 commit 332c215

File tree

4 files changed

+61
-12
lines changed

4 files changed

+61
-12
lines changed

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5717,6 +5717,17 @@ export function createTypeEvaluator(
57175717
case TypeCategory.Class: {
57185718
let typeResult: TypeResult | undefined;
57195719

5720+
// If this is a class-like function created via NewType, treat
5721+
// it like a function for purposes of member accesses.
5722+
if (
5723+
ClassType.isNewTypeClass(baseType) &&
5724+
!baseType.priv.includeSubclasses &&
5725+
prefetched?.functionClass &&
5726+
isClass(prefetched.functionClass)
5727+
) {
5728+
baseType = ClassType.cloneAsInstance(prefetched.functionClass);
5729+
}
5730+
57205731
const enumMemberResult = getTypeOfEnumMember(
57215732
evaluatorInterface,
57225733
node,
@@ -10279,6 +10290,19 @@ export function createTypeEvaluator(
1027910290
}
1028010291

1028110292
if (isClass(subtype)) {
10293+
// Specifically handle the case where the subtype is a class-like
10294+
// object created by calling NewType. At runtime, it's actually
10295+
// a FunctionType object.
10296+
if (
10297+
isClassInstance(subtype) &&
10298+
ClassType.isNewTypeClass(subtype) &&
10299+
!subtype.priv.includeSubclasses
10300+
) {
10301+
if (prefetched?.functionClass) {
10302+
return prefetched.functionClass;
10303+
}
10304+
}
10305+
1028210306
return convertToInstantiable(stripLiteralValue(subtype));
1028310307
}
1028410308

@@ -13512,12 +13536,7 @@ export function createTypeEvaluator(
1351213536
const initType = FunctionType.createSynthesizedInstance('__init__');
1351313537
FunctionType.addParam(
1351413538
initType,
13515-
FunctionParam.create(
13516-
ParamCategory.Simple,
13517-
ClassType.cloneAsInstance(classType),
13518-
FunctionParamFlags.TypeDeclared,
13519-
'self'
13520-
)
13539+
FunctionParam.create(ParamCategory.Simple, AnyType.create(), FunctionParamFlags.TypeDeclared, 'self')
1352113540
);
1352213541
FunctionType.addParam(
1352313542
initType,
@@ -13538,7 +13557,7 @@ export function createTypeEvaluator(
1353813557
const newType = FunctionType.createSynthesizedInstance('__new__', FunctionTypeFlags.ConstructorMethod);
1353913558
FunctionType.addParam(
1354013559
newType,
13541-
FunctionParam.create(ParamCategory.Simple, classType, FunctionParamFlags.TypeDeclared, 'cls')
13560+
FunctionParam.create(ParamCategory.Simple, AnyType.create(), FunctionParamFlags.TypeDeclared, 'cls')
1354213561
);
1354313562
FunctionType.addDefaultParams(newType);
1354413563
newType.shared.declaredReturnType = ClassType.cloneAsInstance(classType);
@@ -24439,6 +24458,15 @@ export function createTypeEvaluator(
2443924458
}
2444024459
}
2444124460

24461+
// If the source is a class-like type created by a call to NewType, treat it
24462+
// as a FunctionClass instance rather than an instantiable class for
24463+
// purposes of assignability. This reflects its actual runtime type.
24464+
if (isInstantiableClass(srcType) && ClassType.isNewTypeClass(srcType) && !srcType.priv.includeSubclasses) {
24465+
if (prefetched?.functionClass && isInstantiableClass(prefetched?.functionClass)) {
24466+
srcType = ClassType.cloneAsInstance(prefetched.functionClass);
24467+
}
24468+
}
24469+
2444224470
if (recursionCount > maxTypeRecursionCount) {
2444324471
return true;
2444424472
}

packages/pyright-internal/src/tests/samples/newType1.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ class TD1(TypedDict):
6868

6969

7070
def func2(x: MyString):
71-
# This should generate an error because isinstance can't be used
72-
# with a NewType.
71+
# This should generate two errors because isinstance can't be used
72+
# with a NewType and it violates the isinstance siganture.
7373
if isinstance(x, MyString):
7474
pass
7575

76-
# This should generate an error because issubclass can't be used
77-
# with a NewType.
76+
# This should generate two errors because issubclass can't be used
77+
# with a NewType and it violates the issubclass signature.
7878
if issubclass(type(x), (MyString, int)):
7979
pass
8080

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This sample tests that classes created with NewType are treated
2+
# as though they're functions at runtime.
3+
4+
from typing import NewType
5+
6+
MyStr = NewType("MyStr", str)
7+
8+
# This should generate an error.
9+
v1: type = MyStr
10+
11+
# This should generate an error.
12+
MyStr.capitalize
13+
14+
MyStr.__name__ # OK
15+

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ test('MissingSuper1', () => {
266266
test('NewType1', () => {
267267
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['newType1.py']);
268268

269-
TestUtils.validateResults(analysisResults, 11);
269+
TestUtils.validateResults(analysisResults, 13);
270270
});
271271

272272
test('NewType2', () => {
@@ -299,6 +299,12 @@ test('NewType6', () => {
299299
TestUtils.validateResults(analysisResults, 1);
300300
});
301301

302+
test('NewType7', () => {
303+
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['newType7.py']);
304+
305+
TestUtils.validateResults(analysisResults, 2);
306+
});
307+
302308
test('isInstance1', () => {
303309
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['isinstance1.py']);
304310

0 commit comments

Comments
 (0)