Skip to content

Commit

Permalink
Added support for type narrowing of a class pattern when the specifie…
Browse files Browse the repository at this point in the history
…d class is `type()` or a subtype thereof and the subject contains a `type[X]` whose metaclass potentially matches the pattern. This addresses #5573. (#5576)

Co-authored-by: Eric Traut <[email protected]>
  • Loading branch information
erictraut and msfterictraut authored Jul 25, 2023
1 parent 12e9c31 commit 1da8c18
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 0 deletions.
31 changes: 31 additions & 0 deletions packages/pyright-internal/src/analyzer/patternMatching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
getTypeCondition,
getTypeVarScopeId,
isLiteralType,
isMetaclassInstance,
isPartlyUnknown,
isTupleClass,
isUnboundedTupleClass,
Expand Down Expand Up @@ -672,10 +673,23 @@ function narrowTypeBasedOnClassPattern(
}

const classInstance = convertToInstance(classType);
const isPatternMetaclass = isMetaclassInstance(classInstance);

return evaluator.mapSubtypesExpandTypeVars(
type,
/* conditionFilter */ undefined,
(subjectSubtypeExpanded, subjectSubtypeUnexpanded) => {
// Handle the case where the class pattern references type() or a subtype thereof
// and the subject type is an instantiable class itself.
if (isPatternMetaclass && isInstantiableClass(subjectSubtypeExpanded)) {
const metaclass = subjectSubtypeExpanded.details.effectiveMetaclass ?? UnknownType.create();
if (isInstantiableClass(classType) && evaluator.assignType(classType, metaclass)) {
return undefined;
}

return subjectSubtypeExpanded;
}

if (!isNoneInstance(subjectSubtypeExpanded) && !isClassInstance(subjectSubtypeExpanded)) {
return subjectSubtypeUnexpanded;
}
Expand Down Expand Up @@ -764,6 +778,9 @@ function narrowTypeBasedOnClassPattern(
}

if (isInstantiableClass(expandedSubtype)) {
const expandedSubtypeInstance = convertToInstance(expandedSubtype);
const isPatternMetaclass = isMetaclassInstance(expandedSubtypeInstance);

return evaluator.mapSubtypesExpandTypeVars(
type,
/* conditionFilter */ undefined,
Expand All @@ -772,6 +789,20 @@ function narrowTypeBasedOnClassPattern(
return convertToInstance(unexpandedSubtype);
}

// Handle the case where the class pattern references type() or a subtype thereof
// and the subject type is a class itself.
if (isPatternMetaclass && isInstantiableClass(subjectSubtypeExpanded)) {
const metaclass = subjectSubtypeExpanded.details.effectiveMetaclass ?? UnknownType.create();
if (
evaluator.assignType(expandedSubtype, metaclass) ||
evaluator.assignType(metaclass, expandedSubtype)
) {
return subjectSubtypeExpanded;
}

return undefined;
}

if (
isNoneInstance(subjectSubtypeExpanded) &&
isInstantiableClass(expandedSubtype) &&
Expand Down
47 changes: 47 additions & 0 deletions packages/pyright-internal/src/tests/samples/matchClass4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# This sample tests the case where a subject is narrowed against a
# class pattern that includes a type() or subclass thereof and
# the subject contains a type[T].


class MyMeta(type):
pass


class A:
pass


class B(A, metaclass=MyMeta):
pass


def func1(subj: type[A]):
match subj:
case type():
reveal_type(subj, expected_text="type[A]")
case _:
reveal_type(subj, expected_text="Never")


def func2(subj: type[A]):
match subj:
case MyMeta():
reveal_type(subj, expected_text="type[A]")
case _:
reveal_type(subj, expected_text="type[A]")


def func3(subj: type[B]):
match subj:
case MyMeta():
reveal_type(subj, expected_text="type[B]")
case _:
reveal_type(subj, expected_text="Never")


def func4(subj: type[B] | type[int]):
match subj:
case MyMeta():
reveal_type(subj, expected_text="type[B] | type[int]")
case _:
reveal_type(subj, expected_text="type[int]")
8 changes: 8 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,14 @@ test('MatchClass3', () => {
TestUtils.validateResults(analysisResults, 0);
});

test('MatchClass4', () => {
const configOptions = new ConfigOptions('.');

configOptions.defaultPythonVersion = PythonVersion.V3_10;
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchClass4.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
});

test('MatchValue1', () => {
const configOptions = new ConfigOptions('.');

Expand Down

0 comments on commit 1da8c18

Please sign in to comment.