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.

This commit is contained in:
Eric Traut 2024-06-12 10:48:37 -07:00
parent 6baa5b1368
commit 63e0876cff
6 changed files with 100 additions and 63 deletions

View File

@ -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);
}

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -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);
});

View File

@ -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