diff --git a/docs/type-concepts.md b/docs/type-concepts.md index 6cf6b0cc1..127c500e7 100644 --- a/docs/type-concepts.md +++ b/docs/type-concepts.md @@ -297,3 +297,29 @@ def add_one(value: _StrOrFloat) -> _StrOrFloat: ``` Notice that the type of variable `sum` is reported with asterisks (`*`). This indicates that internally the type checker is tracking the type as conditional. In this particular example, it indicates that `sum` is a `str` type if the parameter `value` is a `str` but is a `float` if `value` is a `float`. By tracking these conditional types, the type checker can verify that the return type is consistent with the return type `_StrOrFloat`. + + +### Inferred type of self and cls parameters + +When a type annotation for a method’s `self` or `cls` parameter is omitted, pyright will infer its type based on the class that contains the method. The inferred type is internally represented as a type variable that is bound to the class. + +Within the function, the type of `self` is printed with a tilde preceding the class name. This indicates that the type is a TypeVar bound to the class rather than the class itself. Outside of the function, this TypeVar is resolved based on the usage. + +```python +class Parent: + def method1(self): + reveal_type(self) # ~Parent + return self + + @classmethod + def method2(cls): + reveal_type(cls) # Type[~Parent] + return cls + +class Child(Parent): + ... + +reveal_type(Child().method1()) # Child +reveal_type(Child.method2()) # Type[Child] +``` + diff --git a/packages/pyright-internal/src/analyzer/typePrinter.ts b/packages/pyright-internal/src/analyzer/typePrinter.ts index 1cfc543d2..9da3860d7 100644 --- a/packages/pyright-internal/src/analyzer/typePrinter.ts +++ b/packages/pyright-internal/src/analyzer/typePrinter.ts @@ -53,9 +53,13 @@ export const enum PrintTypeFlags { // Expand type aliases to display their individual parts? ExpandTypeAlias = 1 << 5, - // Add "*" for types that are conditionally constrained when + // Omit "*" for types that are conditionally constrained when // used with constrained TypeVars. OmitConditionalConstraint = 1 << 6, + + // Omit "~" for synthesized type variables that represent the + // type of an unannotated self or cls parameter. + OmitSelfClsTypeIndicator = 1 << 7, } export type FunctionReturnTypeCallback = (type: FunctionType) => Type; @@ -317,14 +321,24 @@ export function printType( return type.details.recursiveTypeAliasName; } + // If it's a synthesized type var used to implement `self` or `cls` types, + // print the type with a special character that indicates that the type + // is internally represented as a TypeVar. if (type.details.boundType) { - const boundTypeString = printType( + let boundTypeString = printType( type.details.boundType, printTypeFlags & ~PrintTypeFlags.ExpandTypeAlias, returnTypeCallback, recursionTypes ); + if ( + (printTypeFlags & PrintTypeFlags.OmitSelfClsTypeIndicator) === 0 && + !isAnyOrUnknown(type.details.boundType) + ) { + boundTypeString = `~${boundTypeString}`; + } + if (TypeBase.isInstantiable(type)) { return `Type[${boundTypeString}]`; } @@ -549,7 +563,12 @@ export function printFunctionParts( const paramType = FunctionType.getEffectiveParameterType(type, index); const paramTypeString = recursionTypes.length < maxTypeRecursionCount - ? printType(paramType, printTypeFlags, returnTypeCallback, recursionTypes) + ? printType( + paramType, + printTypeFlags | PrintTypeFlags.OmitSelfClsTypeIndicator, + returnTypeCallback, + recursionTypes + ) : ''; paramString += ': ' + paramTypeString; diff --git a/packages/pyright-internal/src/tests/samples/isinstance6.py b/packages/pyright-internal/src/tests/samples/isinstance6.py index 6049cd51c..2db8de087 100644 --- a/packages/pyright-internal/src/tests/samples/isinstance6.py +++ b/packages/pyright-internal/src/tests/samples/isinstance6.py @@ -8,14 +8,14 @@ class Foo: @classmethod def bar(cls, other: type): if issubclass(other, cls): - t1: Literal["Type[Foo]"] = reveal_type(other) + t1: Literal["Type[~Foo]"] = reveal_type(other) if issubclass(other, (int, cls)): - t2: Literal["Type[Foo] | Type[int]"] = reveal_type(other) + t2: Literal["Type[~Foo] | Type[int]"] = reveal_type(other) def baz(self, other: object): if isinstance(other, self.__class__): - t1: Literal["Foo"] = reveal_type(other) + t1: Literal["~Foo"] = reveal_type(other) if isinstance(other, (int, self.__class__)): - t2: Literal["Foo | int"] = reveal_type(other) + t2: Literal["~Foo | int"] = reveal_type(other) diff --git a/packages/pyright-internal/src/tests/samples/memberAccess8.py b/packages/pyright-internal/src/tests/samples/memberAccess8.py index 65c932585..0380d9af2 100644 --- a/packages/pyright-internal/src/tests/samples/memberAccess8.py +++ b/packages/pyright-internal/src/tests/samples/memberAccess8.py @@ -1,4 +1,4 @@ -# This sample tests the use of a generic property class. +# This sample tests the use of a generic descriptor class. from typing import Any, Callable, Generic, Literal, Optional, Type, TypeVar, overload @@ -72,4 +72,4 @@ class B: b = B() t_b1: Literal["str"] = reveal_type(b.foo) -t_b2: Literal["Minimal[B, str]"] = reveal_type(B.foo) +t_b2: Literal["Minimal[~B, str]"] = reveal_type(B.foo)