Added detection of errors when a namedtuple definition includes a language keyword. Also added minimal support for the rename parameter to the namedtuple function. This addresses #5423. (#5482)

Co-authored-by: Eric Traut <erictr@microsoft.com>
This commit is contained in:
Eric Traut 2023-07-12 10:24:46 +02:00 committed by GitHub
parent 70ecdc2974
commit e63f229d59
6 changed files with 117 additions and 15 deletions

View File

@ -18,26 +18,13 @@ import {
ParseNodeType,
StringListNode,
} from '../parser/parseNodes';
import { Tokenizer } from '../parser/tokenizer';
import { getFileInfo } from './analyzerNodeInfo';
import { DeclarationType, VariableDeclaration } from './declaration';
import * as ParseTreeUtils from './parseTreeUtils';
import { evaluateStaticBoolExpression } from './staticExpressions';
import { Symbol, SymbolFlags } from './symbol';
import { FunctionArgument, TypeEvaluator } from './typeEvaluatorTypes';
import {
AnyType,
ClassType,
ClassTypeFlags,
combineTypes,
FunctionParameter,
FunctionType,
FunctionTypeFlags,
isClassInstance,
isInstantiableClass,
NoneType,
TupleTypeArgument,
Type,
UnknownType,
} from './types';
import {
computeMroLinearization,
convertToInstance,
@ -46,6 +33,21 @@ import {
specializeTupleClass,
synthesizeTypeVarForSelfCls,
} from './typeUtils';
import {
AnyType,
ClassType,
ClassTypeFlags,
FunctionParameter,
FunctionType,
FunctionTypeFlags,
NoneType,
TupleTypeArgument,
Type,
UnknownType,
combineTypes,
isClassInstance,
isInstantiableClass,
} from './types';
// Creates a new custom tuple factory class with named values.
// Supports both typed and untyped variants.
@ -59,6 +61,25 @@ export function createNamedTupleType(
const fileInfo = getFileInfo(errorNode);
let className = 'namedtuple';
// The "rename" parameter is supported only in the untyped version.
let allowRename = false;
if (!includesTypes) {
const renameArg = argList.find(
(arg) => arg.argumentCategory === ArgumentCategory.Simple && arg.name?.value === 'rename'
);
if (renameArg?.valueExpression) {
const renameValue = evaluateStaticBoolExpression(
renameArg.valueExpression,
fileInfo.executionEnvironment,
fileInfo.definedConstants
);
if (renameValue === true) {
allowRename = true;
}
}
}
if (argList.length === 0) {
evaluator.addError(Localizer.Diagnostic.namedTupleFirstArg(), errorNode);
} else {
@ -156,6 +177,14 @@ export function createNamedTupleType(
entries.forEach((entryName, index) => {
entryName = entryName.trim();
if (entryName) {
entryName = renameKeyword(
evaluator,
entryName,
allowRename,
entriesArg.valueExpression!,
index
);
const entryType = UnknownType.create();
const paramInfo: FunctionParameter = {
category: ParameterCategory.Simple,
@ -232,6 +261,8 @@ export function createNamedTupleType(
entryName = entryNameNode.strings.map((s) => s.value).join('');
if (!entryName) {
evaluator.addError(Localizer.Diagnostic.namedTupleEmptyName(), entryNameNode);
} else {
entryName = renameKeyword(evaluator, entryName, allowRename, entryNameNode, index);
}
} else {
addGenericGetAttribute = true;
@ -412,3 +443,27 @@ export function updateNamedTupleBaseClass(
return isUpdateNeeded;
}
function renameKeyword(
evaluator: TypeEvaluator,
name: string,
allowRename: boolean,
errorNode: ExpressionNode,
index: number
): string {
// Determine whether the name is a keyword in python.
const isKeyword = Tokenizer.isKeyword(name);
if (!isKeyword) {
// No rename necessary.
return name;
}
if (allowRename) {
// Rename based on index.
return `_${index}`;
}
evaluator.addError(Localizer.Diagnostic.namedTupleNameKeyword(), errorNode);
return name;
}

View File

@ -583,6 +583,7 @@ export namespace Localizer {
export const namedTupleEmptyName = () => getRawString('Diagnostic.namedTupleEmptyName');
export const namedTupleFirstArg = () => getRawString('Diagnostic.namedTupleFirstArg');
export const namedTupleMultipleInheritance = () => getRawString('Diagnostic.namedTupleMultipleInheritance');
export const namedTupleNameKeyword = () => getRawString('Diagnostic.namedTupleNameKeyword');
export const namedTupleNameType = () => getRawString('Diagnostic.namedTupleNameType');
export const namedTupleNameUnique = () => getRawString('Diagnostic.namedTupleNameUnique');
export const namedTupleNoTypes = () => getRawString('Diagnostic.namedTupleNoTypes');

View File

@ -268,6 +268,7 @@
"namedTupleEmptyName": "Names within a named tuple cannot be empty",
"namedTupleMultipleInheritance": "Multiple inheritance with NamedTuple is not supported",
"namedTupleFirstArg": "Expected named tuple class name as first argument",
"namedTupleNameKeyword": "Field names cannot be a keyword",
"namedTupleNameType": "Expected two-entry tuple specifying entry name and type",
"namedTupleNameUnique": "Names within a named tuple must be unique",
"namedTupleNoTypes": "\"namedtuple\" provides no types for tuple entries; use \"NamedTuple\" instead",

View File

@ -91,6 +91,8 @@ const _keywords: Map<string, KeywordType> = new Map([
['True', KeywordType.True],
]);
const _softKeywords = new Set(['match', 'case', 'type']);
const _operatorInfo: { [key: number]: OperatorFlags } = {
[OperatorType.Add]: OperatorFlags.Unary | OperatorFlags.Binary,
[OperatorType.AddEqual]: OperatorFlags.Assignment,
@ -364,6 +366,19 @@ export class Tokenizer {
return _operatorInfo[operatorType];
}
static isKeyword(name: string, includeSoftKeywords = false): boolean {
const keyword = _keywords.get(name);
if (!keyword) {
return false;
}
if (includeSoftKeywords) {
return true;
}
return !_softKeywords.has(name);
}
static isOperatorAssignment(operatorType?: OperatorType): boolean {
if (operatorType === undefined || _operatorInfo[operatorType] === undefined) {
return false;

View File

@ -0,0 +1,24 @@
# This sample tests the detection of keywords in a named tuple
# definition and support for the "rename" parameter.
from collections import namedtuple
from typing import NamedTuple
# This should generate an error because "def" is a keyword.
NT1 = namedtuple("NT1", ["abc", "def"])
# This should generate an error because "class" is a keyword.
NT2 = namedtuple("NT2", ["abc", "class"], rename=False)
NT3 = namedtuple("NT3", ["abc", "def"], rename=True)
v3 = NT3(abc=0, _1=0)
# This should generate an error because "def" is a keyword.
NT4 = NamedTuple("NT4", [("abc", int), ("def", int)])
# These are soft keywords, so they shouldn't generate an error.
NT5 = namedtuple("NT5", ["type", "match"])

View File

@ -1374,6 +1374,12 @@ test('NamedTuple8', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('NamedTuple9', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['namedTuple9.py']);
TestUtils.validateResults(analysisResults, 3);
});
test('Slots1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['slots1.py']);