Skip to content

Commit a75256a

Browse files
committed
variance for parameter specification and type variable tuples
1 parent df077f6 commit a75256a

7 files changed

Lines changed: 197 additions & 22 deletions

File tree

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

Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13503,7 +13503,52 @@ export function createTypeEvaluator(
1350313503
const paramName = paramNameNode ? paramNameNode.d.value : undefined;
1350413504

1350513505
if (paramName) {
13506-
if (paramName === 'default') {
13506+
if (paramName === 'covariant') {
13507+
if (argList[i].valueExpression && getBooleanValue(argList[i].valueExpression!)) {
13508+
if (
13509+
typeVar.shared.declaredVariance === Variance.Contravariant ||
13510+
typeVar.shared.declaredVariance === Variance.Auto
13511+
) {
13512+
addDiagnostic(
13513+
DiagnosticRule.reportGeneralTypeIssues,
13514+
LocMessage.typeVarVariance(),
13515+
argList[i].valueExpression!
13516+
);
13517+
} else {
13518+
typeVar.shared.declaredVariance = Variance.Covariant;
13519+
}
13520+
}
13521+
} else if (paramName === 'contravariant') {
13522+
if (argList[i].valueExpression && getBooleanValue(argList[i].valueExpression!)) {
13523+
if (
13524+
typeVar.shared.declaredVariance === Variance.Covariant ||
13525+
typeVar.shared.declaredVariance === Variance.Auto
13526+
) {
13527+
addDiagnostic(
13528+
DiagnosticRule.reportGeneralTypeIssues,
13529+
LocMessage.typeVarVariance(),
13530+
argList[i].valueExpression!
13531+
);
13532+
} else {
13533+
typeVar.shared.declaredVariance = Variance.Contravariant;
13534+
}
13535+
}
13536+
} else if (paramName === 'infer_variance') {
13537+
if (argList[i].valueExpression && getBooleanValue(argList[i].valueExpression!)) {
13538+
if (
13539+
typeVar.shared.declaredVariance === Variance.Covariant ||
13540+
typeVar.shared.declaredVariance === Variance.Contravariant
13541+
) {
13542+
addDiagnostic(
13543+
DiagnosticRule.reportGeneralTypeIssues,
13544+
LocMessage.typeVarVariance(),
13545+
argList[i].valueExpression!
13546+
);
13547+
} else {
13548+
typeVar.shared.declaredVariance = Variance.Auto;
13549+
}
13550+
}
13551+
} else if (paramName === 'default') {
1350713552
const expr = argList[i].valueExpression;
1350813553
if (expr) {
1350913554
const defaultType = getTypeVarTupleDefaultType(expr, /* isPep695Syntax */ false);
@@ -13593,7 +13638,52 @@ export function createTypeEvaluator(
1359313638
const paramName = paramNameNode ? paramNameNode.d.value : undefined;
1359413639

1359513640
if (paramName) {
13596-
if (paramName === 'default') {
13641+
if (paramName === 'covariant') {
13642+
if (argList[i].valueExpression && getBooleanValue(argList[i].valueExpression!)) {
13643+
if (
13644+
paramSpec.shared.declaredVariance === Variance.Contravariant ||
13645+
paramSpec.shared.declaredVariance === Variance.Auto
13646+
) {
13647+
addDiagnostic(
13648+
DiagnosticRule.reportGeneralTypeIssues,
13649+
LocMessage.typeVarVariance(),
13650+
argList[i].valueExpression!
13651+
);
13652+
} else {
13653+
paramSpec.shared.declaredVariance = Variance.Covariant;
13654+
}
13655+
}
13656+
} else if (paramName === 'contravariant') {
13657+
if (argList[i].valueExpression && getBooleanValue(argList[i].valueExpression!)) {
13658+
if (
13659+
paramSpec.shared.declaredVariance === Variance.Covariant ||
13660+
paramSpec.shared.declaredVariance === Variance.Auto
13661+
) {
13662+
addDiagnostic(
13663+
DiagnosticRule.reportGeneralTypeIssues,
13664+
LocMessage.typeVarVariance(),
13665+
argList[i].valueExpression!
13666+
);
13667+
} else {
13668+
paramSpec.shared.declaredVariance = Variance.Contravariant;
13669+
}
13670+
}
13671+
} else if (paramName === 'infer_variance') {
13672+
if (argList[i].valueExpression && getBooleanValue(argList[i].valueExpression!)) {
13673+
if (
13674+
paramSpec.shared.declaredVariance === Variance.Covariant ||
13675+
paramSpec.shared.declaredVariance === Variance.Contravariant
13676+
) {
13677+
addDiagnostic(
13678+
DiagnosticRule.reportGeneralTypeIssues,
13679+
LocMessage.typeVarVariance(),
13680+
argList[i].valueExpression!
13681+
);
13682+
} else {
13683+
paramSpec.shared.declaredVariance = Variance.Auto;
13684+
}
13685+
}
13686+
} else if (paramName === 'default') {
1359713687
const expr = argList[i].valueExpression;
1359813688
if (expr) {
1359913689
const defaultType = getParamSpecDefaultType(expr, /* isPep695Syntax */ false);
@@ -18690,24 +18780,33 @@ export function createTypeEvaluator(
1869018780
undefined
1869118781
);
1869218782

18693-
classType.shared.typeParams.forEach((param, paramIndex) => {
18694-
// Skip TypeVarTuples and ParamSpecs.
18695-
if (isTypeVarTuple(param) || isParamSpec(param)) {
18696-
return;
18697-
}
18783+
// A scopeless ParamSpec used as the "top type" substitute for ParamSpec parameters
18784+
// during variance inference. It has no scope ID so makeTypeVarsBound leaves it free,
18785+
// giving us the needed asymmetry:
18786+
// - as src in assignBoundTypeVar: fails (not the gradual `...` form) → covariant check fails
18787+
// - as dest in assignTypeVar: returns true immediately (no scopeId) → contravariant check passes
18788+
const dummyParamSpec = TypeVarType.createInstantiable('__varianceDummyP', TypeVarKind.ParamSpec);
18789+
const dummyTypeVarTuple = TypeVarType.createInstantiable('__varianceDummyTs', TypeVarKind.TypeVarTuple);
1869818790

18791+
classType.shared.typeParams.forEach((param, paramIndex) => {
1869918792
// Skip type variables without auto-variance.
1870018793
if (param.shared.declaredVariance !== Variance.Auto) {
1870118794
return;
1870218795
}
1870318796

1870418797
// Replace all type arguments with a dummy type except for the
18705-
// TypeVar of interest, which is replaced with an object instance.
18798+
// TypeVar of interest. For regular TypeVars use an object instance;
18799+
// for ParamSpec/TypeVarTuple use a scopeless dummy (object would become
18800+
// the gradual `...` form, making both checks pass; TypeVarTuple at paramIndex
18801+
// was previously returned as itself, making srcTypeArgs === destTypeArgs).
1870618802
const srcTypeArgs = classType.shared.typeParams.map((p, i) => {
18707-
if (isTypeVarTuple(p)) {
18708-
return p;
18803+
if (i === paramIndex) {
18804+
if (isParamSpec(p)) return dummyParamSpec;
18805+
if (isTypeVarTuple(p)) return dummyTypeVarTuple;
18806+
return getObjectType();
1870918807
}
18710-
return i === paramIndex ? getObjectType() : dummyTypeObject;
18808+
if (isTypeVarTuple(p)) return p;
18809+
return dummyTypeObject;
1871118810
});
1871218811

1871318812
// Replace all type arguments with a dummy type except for the
@@ -23062,16 +23161,14 @@ export function createTypeEvaluator(
2306223161
if (scopeNode.nodeType === ParseNodeType.Class) {
2306323162
scopeType = TypeVarScopeType.Class;
2306423163

23065-
// Set the variance to "auto" for class-scoped TypeVars.
23066-
typeVar.shared.declaredVariance =
23067-
isParamSpec(typeVar) || isTypeVarTuple(typeVar) ? Variance.Invariant : Variance.Auto;
23164+
// Set the variance to "auto" for class-scoped type variables
23165+
typeVar.shared.declaredVariance = Variance.Auto;
2306823166
} else if (scopeNode.nodeType === ParseNodeType.Function) {
2306923167
scopeType = TypeVarScopeType.Function;
2307023168
} else {
2307123169
assert(scopeNode.nodeType === ParseNodeType.TypeAlias);
2307223170
scopeType = TypeVarScopeType.TypeAlias;
23073-
typeVar.shared.declaredVariance =
23074-
isParamSpec(typeVar) || isTypeVarTuple(typeVar) ? Variance.Invariant : Variance.Auto;
23171+
typeVar.shared.declaredVariance = Variance.Auto;
2307523172
}
2307623173

2307723174
typeVar = TypeVarType.cloneForScopeId(

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3212,7 +3212,7 @@ export function isVarianceOfTypeArgCompatible(type: Type, typeParamVariance: Var
32123212
return true;
32133213
}
32143214

3215-
if (isTypeVar(type) && !isParamSpec(type) && !isTypeVarTuple(type)) {
3215+
if (isTypeVar(type)) {
32163216
const typeArgVariance = type.shared.declaredVariance;
32173217

32183218
if (typeArgVariance === Variance.Contravariant || typeArgVariance === Variance.Covariant) {
@@ -3223,10 +3223,6 @@ export function isVarianceOfTypeArgCompatible(type: Type, typeParamVariance: Var
32233223
return type.shared.typeParams.every((typeParam, index) => {
32243224
let typeArgType: Type | undefined;
32253225

3226-
if (isParamSpec(typeParam) || isTypeVarTuple(typeParam)) {
3227-
return true;
3228-
}
3229-
32303226
if (type.priv.typeArgs && index < type.priv.typeArgs.length) {
32313227
typeArgType = type.priv.typeArgs[index];
32323228
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# This sample tests variance inference for TypeVarTuple type parameters.
2+
3+
from typing import Callable, ParamSpec
4+
5+
P_co = ParamSpec("P_co", covariant=True)
6+
P_contra = ParamSpec("P_contra", contravariant=True)
7+
P_infer = ParamSpec("P_infer", infer_variance=True)
8+
9+
10+
class ShouldBeContravariant1[**OutP]:
11+
def f(self) -> Callable[OutP, None]: ...
12+
13+
14+
vcontra1_1: ShouldBeContravariant1[object] = ShouldBeContravariant1[int]() # pyright: ignore[reportAssignmentType]
15+
vcontra1_2: ShouldBeContravariant1[int] = ShouldBeContravariant1[object]() # OK
16+
17+
18+
class ShouldBeCovariant1[**OutP]:
19+
def f(self, fn: Callable[OutP, None]) -> None: ...
20+
21+
22+
vco1_1: ShouldBeCovariant1[int] = ShouldBeCovariant1[object]() # pyright: ignore[reportAssignmentType]
23+
vco1_2: ShouldBeCovariant1[object] = ShouldBeCovariant1[int]() # OK
24+
25+
26+
class ShouldBeInvariant1[**OutP]:
27+
def f(self, fn: Callable[OutP, None]) -> Callable[OutP, None]: ...
28+
29+
30+
vinv1_1: ShouldBeInvariant1[object] = ShouldBeInvariant1[int]() # pyright: ignore[reportAssignmentType]
31+
vinv1_2: ShouldBeInvariant1[int] = ShouldBeInvariant1[object]() # pyright: ignore[reportAssignmentType]

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ class ClassB(Generic[_Xs]): ...
5353
Ts2 = TypeVarTuple("Ts2", int, str)
5454

5555
# This should generate TypeVarTuple cannot be covariant.
56+
# based no error
5657
Ts3 = TypeVarTuple("Ts3", covariant=True)
5758

5859
# This should generate TypeVarTuple cannot be contravariant.
60+
# based no error
5961
Ts4 = TypeVarTuple("Ts4", contravariant=True)
6062

6163
# This should generate TypeVarTuple does not accept other keyword arguments.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# This sample tests variance inference for ParamSpec type parameters.
2+
3+
from typing import TypeVarTuple
4+
5+
Ts_co = TypeVarTuple("Ts_co", covariant=True)
6+
Ts_contra = TypeVarTuple("Ts_contra", contravariant=True)
7+
Ts_infer = TypeVarTuple("Ts_infer", infer_variance=True)
8+
9+
10+
class ShouldBeContravariant1[*OutTs]:
11+
def f(self, t: tuple[*OutTs]): ...
12+
13+
14+
vcontra1_1: ShouldBeContravariant1[object] = ShouldBeContravariant1[int]() # pyright: ignore[reportAssignmentType]
15+
vcontra1_2: ShouldBeContravariant1[int] = ShouldBeContravariant1[object]() # OK
16+
17+
18+
class ShouldBeCovariant1[*OutTs]:
19+
def f(self) -> tuple[*OutTs]: ...
20+
21+
22+
vco1_1: ShouldBeCovariant1[int] = ShouldBeCovariant1[object]() # pyright: ignore[reportAssignmentType]
23+
vco1_2: ShouldBeCovariant1[object] = ShouldBeCovariant1[int]() # OK
24+
25+
26+
class ShouldBeInvariant1[*OutTs]:
27+
def f(self, t: tuple[*OutTs]) -> tuple[*OutTs]: ...
28+
29+
30+
vinv1_1: ShouldBeInvariant1[object] = ShouldBeInvariant1[int]() # pyright: ignore[reportAssignmentType]
31+
vinv1_2: ShouldBeInvariant1[int] = ShouldBeInvariant1[object]() # pyright: ignore[reportAssignmentType]

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ test('AutoVariance5', () => {
118118
TestUtils.validateResults(analysisResults, 0);
119119
});
120120

121+
test('ParamSpecVariance1', () => {
122+
const configOptions = new ConfigOptions(Uri.empty());
123+
configOptions.diagnosticRuleSet.reportUnnecessaryTypeIgnoreComment = 'error';
124+
configOptions.diagnosticRuleSet.reportUnusedParameter = 'none';
125+
126+
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['paramSpecVariance1.py'], configOptions);
127+
TestUtils.validateResultsButBased(analysisResults, {});
128+
});
129+
130+
test('TypeVarTupleVariance1', () => {
131+
const configOptions = new ConfigOptions(Uri.empty());
132+
configOptions.diagnosticRuleSet.reportUnnecessaryTypeIgnoreComment = 'error';
133+
configOptions.diagnosticRuleSet.reportUnusedParameter = 'none';
134+
135+
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarTupleVariance1.py'], configOptions);
136+
TestUtils.validateResultsButBased(analysisResults, {});
137+
});
138+
121139
test('TypeAliasStatement1', () => {
122140
const configOptions = new ConfigOptions(Uri.empty());
123141
configOptions.defaultPythonVersion = pythonVersion3_12;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ test('TypeVarTuple1', () => {
196196

197197
configOptions.defaultPythonVersion = pythonVersion3_11;
198198
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarTuple1.py'], configOptions);
199-
TestUtils.validateResults(analysisResults, 18);
199+
TestUtils.validateResults(analysisResults, 16);
200200
});
201201

202202
test('TypeVarTuple2', () => {

0 commit comments

Comments
 (0)