From 63e0876cff97c0f689d94569081f9698c00685eb Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 12 Jun 2024 10:48:37 -0700 Subject: [PATCH] Added support for `deprecated` objects that are instantiated prior to being used as a decorator. This allows for a factory usage pattern. This addresses #5953. --- .../src/analyzer/decorators.ts | 74 ++++++++----------- .../src/analyzer/typeEvaluator.ts | 12 +++ .../src/analyzer/typeUtils.ts | 19 ----- .../pyright-internal/src/analyzer/types.ts | 17 ++++- .../src/tests/checker.test.ts | 11 +++ .../src/tests/samples/deprecated7.py | 30 ++++++++ 6 files changed, 100 insertions(+), 63 deletions(-) create mode 100644 packages/pyright-internal/src/tests/samples/deprecated7.py diff --git a/packages/pyright-internal/src/analyzer/decorators.ts b/packages/pyright-internal/src/analyzer/decorators.ts index 456ff2486..bc30588ef 100644 --- a/packages/pyright-internal/src/analyzer/decorators.ts +++ b/packages/pyright-internal/src/analyzer/decorators.ts @@ -34,7 +34,7 @@ import { validatePropertyMethod, } from './properties'; import { EvaluatorFlags, FunctionArgument, TypeEvaluator } from './typeEvaluatorTypes'; -import { isBuiltInDeprecatedType, isPartlyUnknown, isProperty } from './typeUtils'; +import { isPartlyUnknown, isProperty } from './typeUtils'; import { ClassType, ClassTypeFlags, @@ -43,7 +43,9 @@ import { FunctionTypeFlags, OverloadedFunctionType, Type, + TypeBase, UnknownType, + isClass, isClassInstance, isFunction, isInstantiableClass, @@ -86,17 +88,6 @@ export function getFunctionInfoFromDecorators( let evaluatorFlags = fileInfo.isStubFile ? EvaluatorFlags.AllowForwardReferences : EvaluatorFlags.None; if (decoratorNode.expression.nodeType !== ParseNodeType.Call) { evaluatorFlags |= EvaluatorFlags.CallBaseDefaults; - } else { - if (decoratorNode.expression.nodeType === ParseNodeType.Call) { - const decoratorCallType = evaluator.getTypeOfExpression( - decoratorNode.expression.leftExpression, - evaluatorFlags | EvaluatorFlags.CallBaseDefaults - ).type; - - if (isBuiltInDeprecatedType(decoratorCallType)) { - deprecationMessage = getCustomDeprecationMessage(decoratorNode); - } - } } const decoratorTypeResult = evaluator.getTypeOfExpression(decoratorNode.expression, evaluatorFlags); @@ -118,21 +109,23 @@ export function getFunctionInfoFromDecorators( } else if (decoratorType.details.builtInName === 'overload') { flags |= FunctionTypeFlags.Overloaded; } - } else if (isInstantiableClass(decoratorType)) { - if (ClassType.isBuiltIn(decoratorType, 'staticmethod')) { - if (isInClass) { - flags |= FunctionTypeFlags.StaticMethod; + } else if (isClass(decoratorType)) { + if (TypeBase.isInstantiable(decoratorType)) { + if (ClassType.isBuiltIn(decoratorType, 'staticmethod')) { + if (isInClass) { + flags |= FunctionTypeFlags.StaticMethod; + } + } else if (ClassType.isBuiltIn(decoratorType, 'classmethod')) { + if (isInClass) { + flags |= FunctionTypeFlags.ClassMethod; + } } - } else if (ClassType.isBuiltIn(decoratorType, 'classmethod')) { - if (isInClass) { - flags |= FunctionTypeFlags.ClassMethod; + } else { + if (ClassType.isBuiltIn(decoratorType, 'deprecated')) { + deprecationMessage = decoratorType.deprecatedInstanceMessage; } } } - - if (isBuiltInDeprecatedType(decoratorType)) { - deprecationMessage = getCustomDeprecationMessage(decoratorNode); - } } return { flags, deprecationMessage }; @@ -189,10 +182,6 @@ export function applyFunctionDecorator( return inputFunctionType; } } - - if (isBuiltInDeprecatedType(decoratorCallType)) { - return inputFunctionType; - } } let returnType = getTypeOfDecorator(evaluator, decoratorNode, inputFunctionType); @@ -260,11 +249,11 @@ export function applyFunctionDecorator( return inputFunctionType; } - } - } - if (isBuiltInDeprecatedType(decoratorType)) { - return inputFunctionType; + case 'decorator': { + return inputFunctionType; + } + } } // Handle properties and subclasses of properties specially. @@ -332,11 +321,6 @@ export function applyClassDecorator( ); } } - - if (isBuiltInDeprecatedType(decoratorCallType)) { - originalClassType.details.deprecatedMessage = getCustomDeprecationMessage(decoratorNode); - return inputClassType; - } } if (isOverloadedFunction(decoratorType)) { @@ -395,6 +379,11 @@ export function applyClassDecorator( applyDataClassDecorator(evaluator, decoratorNode, originalClassType, dataclassBehaviors, callNode); return inputClassType; } + } else if (isClassInstance(decoratorType)) { + if (ClassType.isBuiltIn(decoratorType, 'deprecated')) { + originalClassType.details.deprecatedMessage = decoratorType.deprecatedInstanceMessage; + return inputClassType; + } } return getTypeOfDecorator(evaluator, decoratorNode, inputClassType); @@ -588,16 +577,15 @@ export function addOverloadsToFunctionType(evaluator: TypeEvaluator, node: Funct return type; } -// Given a @typing.deprecated decorator node, returns either '' or a custom +// Given a @typing.deprecated call node, returns either '' or a custom // deprecation message if one is provided. -function getCustomDeprecationMessage(decorator: DecoratorNode): string { +export function getDeprecatedMessageFromCall(node: CallNode): string { if ( - decorator.expression.nodeType === ParseNodeType.Call && - decorator.expression.arguments.length > 0 && - decorator.expression.arguments[0].argumentCategory === ArgumentCategory.Simple && - decorator.expression.arguments[0].valueExpression.nodeType === ParseNodeType.StringList + node.arguments.length > 0 && + node.arguments[0].argumentCategory === ArgumentCategory.Simple && + node.arguments[0].valueExpression.nodeType === ParseNodeType.StringList ) { - const stringListNode = decorator.expression.arguments[0].valueExpression; + const stringListNode = node.arguments[0].valueExpression; const message = stringListNode.strings.map((s) => s.value).join(''); return convertDocStringToPlainText(message); } diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 44823ee69..ad670dd6e 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -130,6 +130,7 @@ import { addOverloadsToFunctionType, applyClassDecorator, applyFunctionDecorator, + getDeprecatedMessageFromCall, getFunctionInfoFromDecorators, } from './decorators'; import { @@ -9980,6 +9981,17 @@ export function createTypeEvaluator( returnType = convertToInstance(unexpandedCallType); } + // If we instantiated the "deprecated" class, attach the deprecation + // message to the instance. + if ( + errorNode.nodeType === ParseNodeType.Call && + returnType && + isClassInstance(returnType) && + ClassType.isBuiltIn(returnType, 'deprecated') + ) { + returnType = ClassType.cloneForDeprecatedInstance(returnType, getDeprecatedMessageFromCall(errorNode)); + } + // If we instantiated a type, transform it into a class. // This can happen if someone directly instantiates a metaclass // deriving from type. diff --git a/packages/pyright-internal/src/analyzer/typeUtils.ts b/packages/pyright-internal/src/analyzer/typeUtils.ts index 7dd108b71..c324766d7 100644 --- a/packages/pyright-internal/src/analyzer/typeUtils.ts +++ b/packages/pyright-internal/src/analyzer/typeUtils.ts @@ -388,25 +388,6 @@ export function isTypeVarSame(type1: TypeVarType, type2: Type) { return isCompatible; } -// The `deprecated` type has been defined as a function, an overloaded function, -// and a class in various versions of typeshed. This function checks for all of -// these variants to determine whether the type is the built-in `deprecated` type. -export function isBuiltInDeprecatedType(type: Type) { - if (isFunction(type)) { - return type.details.builtInName === 'deprecated'; - } - - if (isOverloadedFunction(type)) { - return type.overloads.length > 0 && type.overloads[0].details.builtInName === 'deprecated'; - } - - if (isInstantiableClass(type)) { - return ClassType.isBuiltIn(type, 'deprecated'); - } - - return false; -} - export function makeInferenceContext(expectedType: undefined, isTypeIncomplete?: boolean): undefined; export function makeInferenceContext(expectedType: Type, isTypeIncomplete?: boolean): InferenceContext; export function makeInferenceContext(expectedType?: Type, isTypeIncomplete?: boolean): InferenceContext | undefined; diff --git a/packages/pyright-internal/src/analyzer/types.ts b/packages/pyright-internal/src/analyzer/types.ts index ba115f928..edcdfec70 100644 --- a/packages/pyright-internal/src/analyzer/types.ts +++ b/packages/pyright-internal/src/analyzer/types.ts @@ -614,12 +614,16 @@ interface ClassDetails { typeParameters: TypeVarType[]; typeVarScopeId?: TypeVarScopeId | undefined; docString?: string | undefined; - deprecatedMessage?: string | undefined; dataClassEntries?: DataClassEntry[] | undefined; dataClassBehaviors?: DataClassBehaviors | undefined; typedDictEntries?: TypedDictEntries | undefined; localSlotsNames?: string[]; + // If the class is decorated with a @deprecated decorator, this + // string provides the message to be displayed when the class + // is used. + deprecatedMessage?: string | undefined; + // A cache of protocol classes (indexed by the class full name) // that have been determined to be compatible or incompatible // with this class. We use "object" here to avoid a circular dependency. @@ -750,6 +754,11 @@ export interface ClassType extends TypeBase { fgetInfo?: PropertyMethodInfo | undefined; fsetInfo?: PropertyMethodInfo | undefined; fdelInfo?: PropertyMethodInfo | undefined; + + // Provides the deprecated message specifically for instances of + // the "deprecated" class. This allows these instances to be used + // as decorators for other classes or functions. + deprecatedInstanceMessage?: string | undefined; } export namespace ClassType { @@ -867,6 +876,12 @@ export namespace ClassType { return newClassType; } + export function cloneForDeprecatedInstance(type: ClassType, deprecatedMessage?: string): ClassType { + const newClassType = TypeBase.cloneType(type); + newClassType.deprecatedInstanceMessage = deprecatedMessage; + return newClassType; + } + export function cloneForTypingAlias(classType: ClassType, aliasName: string): ClassType { const newClassType = TypeBase.cloneType(classType); newClassType.aliasName = aliasName; diff --git a/packages/pyright-internal/src/tests/checker.test.ts b/packages/pyright-internal/src/tests/checker.test.ts index f807780ed..3f00f3abc 100644 --- a/packages/pyright-internal/src/tests/checker.test.ts +++ b/packages/pyright-internal/src/tests/checker.test.ts @@ -596,3 +596,14 @@ test('Deprecated6', () => { const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated6.py'], configOptions); TestUtils.validateResults(analysisResults2, 3); }); + +test('Deprecated7', () => { + const configOptions = new ConfigOptions(Uri.empty()); + + const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated7.py'], configOptions); + TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 2); + + configOptions.diagnosticRuleSet.reportDeprecated = 'error'; + const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated7.py'], configOptions); + TestUtils.validateResults(analysisResults2, 2); +}); diff --git a/packages/pyright-internal/src/tests/samples/deprecated7.py b/packages/pyright-internal/src/tests/samples/deprecated7.py new file mode 100644 index 000000000..a42599454 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/deprecated7.py @@ -0,0 +1,30 @@ +# This sample tests the case where a "deprecated" instance is instantiated +# prior to being used as a decorator. + +# pyright: reportMissingModuleSource=false + +from typing_extensions import deprecated + + +todo = deprecated("This needs to be implemented!!") + + +@todo +class ClassA: ... + + +# This should generate an error if reportDeprecated is enabled. +ClassA() + + +@todo +def func1() -> None: + pass + + +# This should generate an error if reportDeprecated is enabled. +func1() + + +def func2() -> None: + pass