Added a new configuration option deprecateTypingAliases that enables deprecation detection and reporting for symbols imported from the typing module that are deprecated according to PEP 585 and 604. The option is currently disabled by default (even in strict mode) but can be enabled manually. This addresses #3598. (#5812)

Co-authored-by: Eric Traut <erictr@microsoft.com>
This commit is contained in:
Eric Traut 2023-08-25 11:17:37 -07:00 committed by GitHub
parent 6cc5e3c255
commit 4474b7d36b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 437 additions and 85 deletions

View File

@ -56,6 +56,8 @@ The following settings control pyrights diagnostic output (warnings or errors
<a name="enableTypeIgnoreComments"></a> **enableTypeIgnoreComments** [boolean]: PEP 484 defines support for "# type: ignore" comments. This switch enables or disables support for these comments. The default value for this setting is `true`. This does not affect "# pyright: ignore" comments.
<a name="deprecateTypingAliases"></a> **deprecateTypingAliases** [boolean]: PEP 585 indicates that aliases to types in standard collections that were introduced solely to support generics are deprecated as of Python 3.9. This switch controls whether these are treated as deprecated. This applies only when pythonVersion is 3.9 or newer. The default value for this setting is `false` but may be switched to `true` in the future.
<a name="enableExperimentalFeatures"></a> **enableExperimentalFeatures** [boolean]: Enables a set of experimental (mostly undocumented) features that correspond to proposed or exploratory changes to the Python typing standard. These features will likely change or be removed, so they should not be used except for experimentation purposes.
<a name="reportGeneralTypeIssues"></a> **reportGeneralTypeIssues** [boolean or string, optional]: Generate or suppress diagnostics for general type inconsistencies, unsupported operations, argument/parameter mismatches, etc. This covers all of the basic type-checking rules not covered by other rules. It does not include syntax errors. The default value for this setting is `"error"`.
@ -304,6 +306,7 @@ The following table lists the default severity levels for each diagnostic rule w
| strictListInference | false | false | true |
| strictDictionaryInference | false | false | true |
| strictSetInference | false | false | true |
| deprecateTypingAliases | false | false | false |
| enableExperimentalFeatures | false | false | false |
| reportMissingModuleSource | "warning" | "warning" | "warning" |
| reportMissingImports | "warning" | "error" | "error" |

View File

@ -95,6 +95,7 @@ import { AnalyzerFileInfo } from './analyzerFileInfo';
import * as AnalyzerNodeInfo from './analyzerNodeInfo';
import { Declaration, DeclarationType, isAliasDeclaration } from './declaration';
import { getNameNodeForDeclaration } from './declarationUtils';
import { deprecatedAliases, deprecatedSpecialForms } from './deprecatedSymbols';
import { ImportResolver, ImportedModuleDescriptor, createImportedModuleDescriptor } from './importResolver';
import { ImportResult, ImportType } from './importResult';
import { getRelativeModuleName, getTopLevelImports } from './importStatementUtils';
@ -189,51 +190,6 @@ interface TypeVarUsageInfo {
nodes: NameNode[];
}
interface DeprecatedForm {
version: PythonVersion;
fullName: string;
replacementText: string;
}
const deprecatedAliases = new Map<string, DeprecatedForm>([
['Tuple', { version: PythonVersion.V3_9, fullName: 'builtins.tuple', replacementText: 'tuple' }],
['List', { version: PythonVersion.V3_9, fullName: 'builtins.list', replacementText: 'list' }],
['Dict', { version: PythonVersion.V3_9, fullName: 'builtins.dict', replacementText: 'dict' }],
['Set', { version: PythonVersion.V3_9, fullName: 'builtins.set', replacementText: 'set' }],
['FrozenSet', { version: PythonVersion.V3_9, fullName: 'builtins.frozenset', replacementText: 'frozenset' }],
['Type', { version: PythonVersion.V3_9, fullName: 'builtins.type', replacementText: 'type' }],
['Deque', { version: PythonVersion.V3_9, fullName: 'collections.deque', replacementText: 'collections.deque' }],
[
'DefaultDict',
{
version: PythonVersion.V3_9,
fullName: 'collections.defaultdict',
replacementText: 'collections.defaultdict',
},
],
[
'OrderedDict',
{
version: PythonVersion.V3_9,
fullName: 'collections.OrderedDict',
replacementText: 'collections.OrderedDict',
},
],
[
'Counter',
{ version: PythonVersion.V3_9, fullName: 'collections.Counter', replacementText: 'collections.Counter' },
],
[
'ChainMap',
{ version: PythonVersion.V3_9, fullName: 'collections.ChainMap', replacementText: 'collections.ChainMap' },
],
]);
const deprecatedSpecialForms = new Map<string, DeprecatedForm>([
['Optional', { version: PythonVersion.V3_10, fullName: 'typing.Optional', replacementText: '| None' }],
['Union', { version: PythonVersion.V3_10, fullName: 'typing.Union', replacementText: '|' }],
]);
// When enabled, this debug flag causes the code complexity of
// functions to be emitted.
const isPrintCodeComplexityEnabled = false;
@ -1471,7 +1427,13 @@ export class Checker extends ParseTreeWalker {
override visitMemberAccess(node: MemberAccessNode) {
const type = this._evaluator.getType(node);
this._reportDeprecatedUse(node.memberName, type);
const leftExprType = this._evaluator.getType(node.leftExpression);
this._reportDeprecatedUse(
node.memberName,
type,
leftExprType && isModule(leftExprType) && leftExprType.moduleName === 'typing'
);
this._conditionallyReportPrivateUsage(node.memberName);
@ -1558,8 +1520,17 @@ export class Checker extends ParseTreeWalker {
break;
}
let isImportFromTyping = false;
if (node.parent?.nodeType === ParseNodeType.ImportFrom) {
if (node.parent.module.leadingDots === 0 && node.parent.module.nameParts.length === 1) {
if (node.parent.module.nameParts[0].value === 'typing') {
isImportFromTyping = true;
}
}
}
const type = this._evaluator.getType(node.alias ?? node.name);
this._reportDeprecatedUse(node.name, type);
this._reportDeprecatedUse(node.name, type, isImportFromTyping);
return false;
}
@ -3765,7 +3736,7 @@ export class Checker extends ParseTreeWalker {
return false;
}
private _reportDeprecatedUse(node: NameNode, type: Type | undefined) {
private _reportDeprecatedUse(node: NameNode, type: Type | undefined, isImportFromTyping = false) {
if (!type) {
return;
}
@ -3854,32 +3825,35 @@ export class Checker extends ParseTreeWalker {
}
}
// We'll leave this disabled for now because this would be too noisy for most
// code bases. We may want to add it at some future date.
if (0) {
if (this._fileInfo.diagnosticRuleSet.deprecateTypingAliases) {
const deprecatedForm = deprecatedAliases.get(node.value) ?? deprecatedSpecialForms.get(node.value);
if (deprecatedForm) {
if (isInstantiableClass(type) && type.details.fullName === deprecatedForm.fullName) {
if (
(isInstantiableClass(type) && type.details.fullName === deprecatedForm.fullName) ||
type.typeAliasInfo?.fullName === deprecatedForm.fullName
) {
if (this._fileInfo.executionEnvironment.pythonVersion >= deprecatedForm.version) {
if (this._fileInfo.diagnosticRuleSet.reportDeprecated === 'none') {
this._evaluator.addDeprecated(
Localizer.Diagnostic.deprecatedType().format({
version: versionToString(deprecatedForm.version),
replacement: deprecatedForm.replacementText,
}),
node
);
} else {
this._evaluator.addDiagnostic(
this._fileInfo.diagnosticRuleSet.reportDeprecated,
DiagnosticRule.reportDeprecated,
Localizer.Diagnostic.deprecatedType().format({
version: versionToString(deprecatedForm.version),
replacement: deprecatedForm.replacementText,
}),
node
);
if (!deprecatedForm.typingImportOnly || isImportFromTyping) {
if (this._fileInfo.diagnosticRuleSet.reportDeprecated === 'none') {
this._evaluator.addDeprecated(
Localizer.Diagnostic.deprecatedType().format({
version: versionToString(deprecatedForm.version),
replacement: deprecatedForm.replacementText,
}),
node
);
} else {
this._evaluator.addDiagnostic(
this._fileInfo.diagnosticRuleSet.reportDeprecated,
DiagnosticRule.reportDeprecated,
Localizer.Diagnostic.deprecatedType().format({
version: versionToString(deprecatedForm.version),
replacement: deprecatedForm.replacementText,
}),
node
);
}
}
}
}

View File

@ -0,0 +1,305 @@
/*
* deprecatedSymbols.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
* Author: Eric Traut
*
* A list of implicitly-deprecated symbols as defined in PEP 585, etc.
*/
import { PythonVersion } from '../common/pythonVersion';
export interface DeprecatedForm {
// The version of Python where this symbol becomes deprecated
version: PythonVersion;
// The full name of the deprecated type
fullName: string;
// The replacement form
replacementText: string;
// Indicates that the symbol is deprecated only if imported from `typing`
typingImportOnly?: boolean;
}
export const deprecatedAliases = new Map<string, DeprecatedForm>([
['Tuple', { version: PythonVersion.V3_9, fullName: 'builtins.tuple', replacementText: 'tuple' }],
['List', { version: PythonVersion.V3_9, fullName: 'builtins.list', replacementText: 'list' }],
['Dict', { version: PythonVersion.V3_9, fullName: 'builtins.dict', replacementText: 'dict' }],
['Set', { version: PythonVersion.V3_9, fullName: 'builtins.set', replacementText: 'set' }],
['FrozenSet', { version: PythonVersion.V3_9, fullName: 'builtins.frozenset', replacementText: 'frozenset' }],
['Type', { version: PythonVersion.V3_9, fullName: 'builtins.type', replacementText: 'type' }],
['Deque', { version: PythonVersion.V3_9, fullName: 'collections.deque', replacementText: 'collections.deque' }],
[
'DefaultDict',
{
version: PythonVersion.V3_9,
fullName: 'collections.defaultdict',
replacementText: 'collections.defaultdict',
},
],
[
'OrderedDict',
{
version: PythonVersion.V3_9,
fullName: 'collections.OrderedDict',
replacementText: 'collections.OrderedDict',
},
],
[
'Counter',
{ version: PythonVersion.V3_9, fullName: 'collections.Counter', replacementText: 'collections.Counter' },
],
[
'ChainMap',
{ version: PythonVersion.V3_9, fullName: 'collections.ChainMap', replacementText: 'collections.ChainMap' },
],
[
'Awaitable',
{
version: PythonVersion.V3_9,
fullName: 'typing.Awaitable',
replacementText: 'collections.abc.Awaitable',
typingImportOnly: true,
},
],
[
'Coroutine',
{
version: PythonVersion.V3_9,
fullName: 'typing.Coroutine',
replacementText: 'collections.abc.Coroutine',
typingImportOnly: true,
},
],
[
'AsyncIterable',
{
version: PythonVersion.V3_9,
fullName: 'typing.AsyncIterable',
replacementText: 'collections.abc.AsyncIterable',
typingImportOnly: true,
},
],
[
'AsyncIterator',
{
version: PythonVersion.V3_9,
fullName: 'typing.AsyncIterator',
replacementText: 'collections.abc.AsyncIterator',
typingImportOnly: true,
},
],
[
'AsyncGenerator',
{
version: PythonVersion.V3_9,
fullName: 'typing.AsyncGenerator',
replacementText: 'collections.abc.AsyncGenerator',
typingImportOnly: true,
},
],
[
'Iterable',
{
version: PythonVersion.V3_9,
fullName: 'typing.Iterable',
replacementText: 'collections.abc.Iterable',
typingImportOnly: true,
},
],
[
'Iterator',
{
version: PythonVersion.V3_9,
fullName: 'typing.Iterator',
replacementText: 'collections.abc.Iterator',
typingImportOnly: true,
},
],
[
'Generator',
{
version: PythonVersion.V3_9,
fullName: 'typing.Generator',
replacementText: 'collections.abc.Generator',
typingImportOnly: true,
},
],
[
'Reversible',
{
version: PythonVersion.V3_9,
fullName: 'typing.Reversible',
replacementText: 'collections.abc.Reversible',
typingImportOnly: true,
},
],
[
'Container',
{
version: PythonVersion.V3_9,
fullName: 'typing.Container',
replacementText: 'collections.abc.Container',
typingImportOnly: true,
},
],
[
'Collection',
{
version: PythonVersion.V3_9,
fullName: 'typing.Collection',
replacementText: 'collections.abc.Collection',
typingImportOnly: true,
},
],
[
'AbstractSet',
{
version: PythonVersion.V3_9,
fullName: 'typing.AbstractSet',
replacementText: 'collections.abc.Set',
},
],
[
'MutableSet',
{
version: PythonVersion.V3_9,
fullName: 'typing.MutableSet',
replacementText: 'collections.abc.MutableSet',
typingImportOnly: true,
},
],
[
'Mapping',
{
version: PythonVersion.V3_9,
fullName: 'typing.Mapping',
replacementText: 'collections.abc.Mapping',
typingImportOnly: true,
},
],
[
'MutableMapping',
{
version: PythonVersion.V3_9,
fullName: 'typing.MutableMapping',
replacementText: 'collections.abc.MutableMapping',
typingImportOnly: true,
},
],
[
'Sequence',
{
version: PythonVersion.V3_9,
fullName: 'typing.Sequence',
replacementText: 'collections.abc.Sequence',
typingImportOnly: true,
},
],
[
'MutableSequence',
{
version: PythonVersion.V3_9,
fullName: 'typing.MutableSequence',
replacementText: 'collections.abc.MutableSequence',
typingImportOnly: true,
},
],
[
'ByteString',
{
version: PythonVersion.V3_9,
fullName: 'typing.ByteString',
replacementText: 'collections.abc.ByteString',
typingImportOnly: true,
},
],
[
'MappingView',
{
version: PythonVersion.V3_9,
fullName: 'typing.MappingView',
replacementText: 'collections.abc.MappingView',
typingImportOnly: true,
},
],
[
'KeysView',
{
version: PythonVersion.V3_9,
fullName: 'typing.KeysView',
replacementText: 'collections.abc.KeysView',
typingImportOnly: true,
},
],
[
'ItemsView',
{
version: PythonVersion.V3_9,
fullName: 'typing.ItemsView',
replacementText: 'collections.abc.ItemsView',
typingImportOnly: true,
},
],
[
'ValuesView',
{
version: PythonVersion.V3_9,
fullName: 'typing.ValuesView',
replacementText: 'collections.abc.ValuesView',
typingImportOnly: true,
},
],
[
'ContextManager',
{
version: PythonVersion.V3_9,
fullName: 'contextlib.AbstractContextManager',
replacementText: 'contextlib.AbstractContextManager',
typingImportOnly: true,
},
],
[
'AsyncContextManager',
{
version: PythonVersion.V3_9,
fullName: 'contextlib.AbstractAsyncContextManager',
replacementText: 'contextlib.AbstractAsyncContextManager',
typingImportOnly: true,
},
],
[
'Pattern',
{
version: PythonVersion.V3_9,
fullName: 're.Pattern',
replacementText: 're.Pattern',
typingImportOnly: true,
},
],
[
'Match',
{
version: PythonVersion.V3_9,
fullName: 're.Match',
replacementText: 're.Match',
typingImportOnly: true,
},
],
]);
export const deprecatedSpecialForms = new Map<string, DeprecatedForm>([
['Optional', { version: PythonVersion.V3_10, fullName: 'typing.Optional', replacementText: '| None' }],
['Union', { version: PythonVersion.V3_10, fullName: 'typing.Union', replacementText: '|' }],
[
'Callable',
{
version: PythonVersion.V3_9,
fullName: 'typing.Callable',
replacementText: 'collections.abc.Callable',
typingImportOnly: true,
},
],
]);

View File

@ -111,6 +111,9 @@ export interface DiagnosticRuleSet {
// Enable support for type: ignore comments?
enableTypeIgnoreComments: boolean;
// Treat old typing aliases as deprecated if pythonVersion >= 3.9?
deprecateTypingAliases: boolean;
// Report general type issues?
reportGeneralTypeIssues: DiagnosticLevel;
@ -333,6 +336,7 @@ export function getBooleanDiagnosticRules(includeNonOverridable = false) {
DiagnosticRule.strictDictionaryInference,
DiagnosticRule.analyzeUnannotatedFunctions,
DiagnosticRule.strictParameterNoneValue,
DiagnosticRule.deprecateTypingAliases,
];
if (includeNonOverridable) {
@ -340,7 +344,6 @@ export function getBooleanDiagnosticRules(includeNonOverridable = false) {
// want to override it in strict mode or support
// it within pyright comments.
boolRules.push(DiagnosticRule.enableTypeIgnoreComments);
boolRules.push(DiagnosticRule.enableExperimentalFeatures);
}
return boolRules;
@ -437,6 +440,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet {
strictParameterNoneValue: true,
enableExperimentalFeatures: false,
enableTypeIgnoreComments: true,
deprecateTypingAliases: false,
reportGeneralTypeIssues: 'none',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'none',
@ -520,6 +524,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet {
strictParameterNoneValue: true,
enableExperimentalFeatures: false,
enableTypeIgnoreComments: true,
deprecateTypingAliases: false,
reportGeneralTypeIssues: 'error',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'none',
@ -601,8 +606,9 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet {
strictDictionaryInference: true,
analyzeUnannotatedFunctions: true,
strictParameterNoneValue: true,
enableExperimentalFeatures: false, // Not overridden by strict mode
enableExperimentalFeatures: false,
enableTypeIgnoreComments: true, // Not overridden by strict mode
deprecateTypingAliases: false,
reportGeneralTypeIssues: 'error',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'error',

View File

@ -18,6 +18,7 @@ export enum DiagnosticRule {
strictParameterNoneValue = 'strictParameterNoneValue',
enableExperimentalFeatures = 'enableExperimentalFeatures',
enableTypeIgnoreComments = 'enableTypeIgnoreComments',
deprecateTypingAliases = 'deprecateTypingAliases',
reportGeneralTypeIssues = 'reportGeneralTypeIssues',
reportPropertyTypeMismatch = 'reportPropertyTypeMismatch',

View File

@ -501,23 +501,51 @@ test('RegionComments1', () => {
TestUtils.validateResults(analysisResults, 2);
});
// For now, this functionality is disabled.
test('Deprecated1', () => {
const configOptions = new ConfigOptions('.');
// test('Deprecated1', () => {
// const configOptions = new ConfigOptions('.');
configOptions.defaultPythonVersion = PythonVersion.V3_8;
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, 31, 0, 0);
// configOptions.defaultPythonVersion = PythonVersion.V3_8;
// const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
// TestUtils.validateResults(analysisResults1, 0, 0, 0, 0, 0);
configOptions.defaultPythonVersion = PythonVersion.V3_9;
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
TestUtils.validateResults(analysisResults2, 0, 0, 0, 31, 0, 0);
// configOptions.defaultPythonVersion = PythonVersion.V3_9;
// const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
// TestUtils.validateResults(analysisResults2, 0, 0, 0, 0, 11);
configOptions.defaultPythonVersion = PythonVersion.V3_10;
const analysisResults3 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
TestUtils.validateResults(analysisResults3, 0, 0, 0, 31, 0, 0);
// configOptions.defaultPythonVersion = PythonVersion.V3_10;
// const analysisResults3 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
// TestUtils.validateResults(analysisResults3, 0, 0, 0, 0, 13);
// });
// Now enable the deprecateTypingAliases setting.
configOptions.diagnosticRuleSet.deprecateTypingAliases = true;
configOptions.defaultPythonVersion = PythonVersion.V3_8;
const analysisResults4 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
TestUtils.validateResults(analysisResults4, 0, 0, 0, 31, 0, 0);
configOptions.defaultPythonVersion = PythonVersion.V3_9;
const analysisResults5 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
TestUtils.validateResults(analysisResults5, 0, 0, 0, 31, 0, 48);
configOptions.defaultPythonVersion = PythonVersion.V3_10;
const analysisResults6 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
TestUtils.validateResults(analysisResults6, 0, 0, 0, 31, 0, 52);
// Now change reportDeprecated to emit an error.
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
configOptions.defaultPythonVersion = PythonVersion.V3_8;
const analysisResults7 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
TestUtils.validateResults(analysisResults7, 0, 0, 0, 31, 0, 0);
configOptions.defaultPythonVersion = PythonVersion.V3_9;
const analysisResults8 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
TestUtils.validateResults(analysisResults8, 48, 0, 0, 31, 0, 0);
configOptions.defaultPythonVersion = PythonVersion.V3_10;
const analysisResults9 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions);
TestUtils.validateResults(analysisResults9, 52, 0, 0, 31, 0, 0);
});
test('Deprecated2', () => {
const configOptions = new ConfigOptions('.');

View File

@ -16,8 +16,37 @@ from typing import (
Tuple,
Type,
Union,
Awaitable,
Coroutine,
AsyncIterable,
AsyncGenerator,
Iterable,
Iterator,
Generator,
Reversible,
Container,
Collection as C1,
Callable,
AbstractSet,
MutableSet,
Mapping,
MutableMapping,
Sequence,
MutableSequence,
ByteString as BS1,
MappingView,
KeysView,
ItemsView,
ValuesView,
ContextManager as CM1,
AsyncContextManager,
Pattern as P1,
Match as M1,
)
from collections.abc import Collection, ByteString
from contextlib import AbstractContextManager
from re import Pattern, Match
# These should be marked deprecated for Python >= 3.9
v1: List[int] = [1, 2, 3]

View File

@ -154,6 +154,12 @@
"title": "Allow \"# type: ignore\" comments",
"default": true
},
"deprecateTypingAliases": {
"$id": "#/properties/deprecateTypingAliases",
"type": "boolean",
"title": "Treat typing-specific aliases to standard types as deprecated",
"default": true
},
"reportGeneralTypeIssues": {
"$id": "#/properties/reportGeneralTypeIssues",
"$ref": "#/definitions/diagnostic",