Fixed bug where callable class members were not being bound to instance as part of member access operator. As part of this fix, removed the FunctionTypeFlags.InstanceMethod flag, which is now implicit if SStaticMethod, ClassMethod or ConstructorMethod are not set.

This commit is contained in:
Eric Traut 2019-12-23 11:08:24 -07:00
parent 87a07022a6
commit cf24dd690c
5 changed files with 114 additions and 61 deletions

View File

@ -1033,9 +1033,8 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
assert(ClassType.isDataClass(classType));
const newType = FunctionType.create(
FunctionTypeFlags.StaticMethod | FunctionTypeFlags.SynthesizedMethod);
const initType = FunctionType.create(
FunctionTypeFlags.InstanceMethod | FunctionTypeFlags.SynthesizedMethod);
FunctionTypeFlags.ConstructorMethod | FunctionTypeFlags.SynthesizedMethod);
const initType = FunctionType.create(FunctionTypeFlags.SynthesizedMethod);
FunctionType.addParameter(newType, {
category: ParameterCategory.Simple,
@ -1150,7 +1149,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
// Synthesize a __new__ method.
const newType = FunctionType.create(
FunctionTypeFlags.StaticMethod | FunctionTypeFlags.SynthesizedMethod);
FunctionTypeFlags.ConstructorMethod | FunctionTypeFlags.SynthesizedMethod);
FunctionType.addParameter(newType, {
category: ParameterCategory.Simple,
name: 'cls',
@ -1160,8 +1159,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
newType.details.declaredReturnType = ObjectType.create(classType);
// Synthesize an __init__ method.
const initType = FunctionType.create(
FunctionTypeFlags.InstanceMethod | FunctionTypeFlags.SynthesizedMethod);
const initType = FunctionType.create(FunctionTypeFlags.SynthesizedMethod);
FunctionType.addParameter(initType, {
category: ParameterCategory.Simple,
name: 'self',
@ -1197,7 +1195,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
entries.forEach((entry, name) => {
const getOverload = FunctionType.create(
FunctionTypeFlags.InstanceMethod | FunctionTypeFlags.SynthesizedMethod | FunctionTypeFlags.Overloaded);
FunctionTypeFlags.SynthesizedMethod | FunctionTypeFlags.Overloaded);
FunctionType.addParameter(getOverload, {
category: ParameterCategory.Simple,
name: 'self',
@ -3955,8 +3953,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
const builtInTupleType = getBuiltInType(errorNode, 'Tuple');
if (builtInTupleType.category === TypeCategory.Class) {
const constructorType = FunctionType.create(
FunctionTypeFlags.StaticMethod | FunctionTypeFlags.ConstructorMethod |
const constructorType = FunctionType.create(FunctionTypeFlags.ConstructorMethod |
FunctionTypeFlags.SynthesizedMethod);
constructorType.details.declaredReturnType = ObjectType.create(classType);
if (ParseTreeUtils.isAssignmentToDefaultsFollowingNamedTuple(errorNode)) {
@ -4112,8 +4109,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
// will handle property type checking. We may need to disable default
// parameter processing for __new__ (see setDefaultParameterCheckDisabled),
// and we don't want to do it for __init__ as well.
const initType = FunctionType.create(
FunctionTypeFlags.InstanceMethod | FunctionTypeFlags.SynthesizedMethod);
const initType = FunctionType.create(FunctionTypeFlags.SynthesizedMethod);
FunctionType.addParameter(initType, selfParameter);
addDefaultFunctionParameters(initType);
initType.details.declaredReturnType = NoneType.create();
@ -4127,15 +4123,13 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
classFields.set('keys', Symbol.createWithType(SymbolFlags.InstanceMember, keysItemType));
classFields.set('items', Symbol.createWithType(SymbolFlags.InstanceMember, keysItemType));
const lenType = FunctionType.create(
FunctionTypeFlags.InstanceMethod | FunctionTypeFlags.SynthesizedMethod);
const lenType = FunctionType.create(FunctionTypeFlags.SynthesizedMethod);
lenType.details.declaredReturnType = getBuiltInObject(errorNode, 'int');
FunctionType.addParameter(lenType, selfParameter);
classFields.set('__len__', Symbol.createWithType(SymbolFlags.ClassMember, lenType));
if (addGenericGetAttribute) {
const getAttribType = FunctionType.create(
FunctionTypeFlags.InstanceMethod | FunctionTypeFlags.SynthesizedMethod);
const getAttribType = FunctionType.create(FunctionTypeFlags.SynthesizedMethod);
getAttribType.details.declaredReturnType = AnyType.create();
FunctionType.addParameter(getAttribType, selfParameter);
FunctionType.addParameter(getAttribType, {
@ -5943,7 +5937,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
}
function inferFirstParamType(flags: FunctionTypeFlags, containingClassType: ClassType): Type | undefined {
if (flags & (FunctionTypeFlags.InstanceMethod | FunctionTypeFlags.ClassMethod | FunctionTypeFlags.ConstructorMethod)) {
if ((flags & FunctionTypeFlags.StaticMethod) === 0) {
if (containingClassType) {
if (ClassType.isProtocol(containingClassType)) {
// Don't specialize the "self" for protocol classes because type
@ -5953,14 +5947,14 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
return AnyType.create();
}
if (flags & FunctionTypeFlags.InstanceMethod) {
const specializedClassType = selfSpecializeClassType(containingClassType);
return ObjectType.create(specializedClassType);
} else if (flags & (FunctionTypeFlags.ClassMethod | FunctionTypeFlags.ConstructorMethod)) {
if (flags & (FunctionTypeFlags.ClassMethod | FunctionTypeFlags.ConstructorMethod)) {
// For class methods, the cls parameter is allowed to skip the
// abstract class test because the caller is possibly passing
// in a non-abstract subclass.
return selfSpecializeClassType(containingClassType, true);
} else if ((flags & FunctionTypeFlags.StaticMethod) === 0) {
const specializedClassType = selfSpecializeClassType(containingClassType);
return ObjectType.create(specializedClassType);
}
}
}
@ -6007,8 +6001,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
// The "__new__" magic method is not an instance method.
// It acts as a static method instead.
if (node.name.value === '__new__') {
flags |= FunctionTypeFlags.StaticMethod;
if (node.name.value === '__new__' && isInClass) {
flags |= FunctionTypeFlags.ConstructorMethod;
}
@ -6017,28 +6010,25 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
undefined, EvaluatorFlags.DoNotSpecialize).type;
if (decoratorType.category === TypeCategory.Function) {
if (decoratorType.details.builtInName === 'abstractmethod') {
flags |= FunctionTypeFlags.AbstractMethod;
if (isInClass) {
flags |= FunctionTypeFlags.AbstractMethod;
}
} else if (decoratorType.details.builtInName === 'final') {
flags |= FunctionTypeFlags.Final;
}
} else if (decoratorType.category === TypeCategory.Class) {
if (ClassType.isBuiltIn(decoratorType, 'staticmethod')) {
flags |= FunctionTypeFlags.StaticMethod;
if (isInClass) {
flags |= FunctionTypeFlags.StaticMethod;
}
} else if (ClassType.isBuiltIn(decoratorType, 'classmethod')) {
flags |= FunctionTypeFlags.ClassMethod;
if (isInClass) {
flags |= FunctionTypeFlags.ClassMethod;
}
}
}
}
// If the function is contained with a class but is not a class
// method or a static method, it's assumed to be an instance method.
if (isInClass) {
if ((flags & (FunctionTypeFlags.ClassMethod | FunctionTypeFlags.StaticMethod |
FunctionTypeFlags.ConstructorMethod)) === 0) {
flags |= FunctionTypeFlags.InstanceMethod;
}
}
return flags;
}
@ -6112,7 +6102,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
fields.set('fget', fgetSymbol);
// Fill in the __get__ method.
const getFunction = FunctionType.create(FunctionTypeFlags.InstanceMethod);
const getFunction = FunctionType.create(FunctionTypeFlags.SynthesizedMethod);
getFunction.details.parameters.push({
category: ParameterCategory.Simple,
name: 'self',
@ -6129,7 +6119,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
fields.set('__get__', getSymbol);
// Fill in the getter, setter and deleter methods.
const accessorFunction = FunctionType.create(FunctionTypeFlags.InstanceMethod);
const accessorFunction = FunctionType.create(FunctionTypeFlags.SynthesizedMethod);
accessorFunction.details.parameters.push({
category: ParameterCategory.Simple,
name: 'self',
@ -6172,7 +6162,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
fields.set('fset', fsetSymbol);
// Fill in the __set__ method.
const setFunction = FunctionType.create(FunctionTypeFlags.InstanceMethod);
const setFunction = FunctionType.create(FunctionTypeFlags.SynthesizedMethod);
setFunction.details.parameters.push({
category: ParameterCategory.Simple,
name: 'self',
@ -6225,7 +6215,7 @@ export function createTypeEvaluator(importLookup: ImportLookup): TypeEvaluator {
fields.set('fdel', fdelSymbol);
// Fill in the __delete__ method.
const delFunction = FunctionType.create(FunctionTypeFlags.InstanceMethod);
const delFunction = FunctionType.create(FunctionTypeFlags.SynthesizedMethod);
delFunction.details.parameters.push({
category: ParameterCategory.Simple,
name: 'self',

View File

@ -7,15 +7,15 @@
* Collection of functions that operate on Type objects.
*/
import { types } from '@babel/core';
import { ParameterCategory } from '../parser/parseNodes';
import { ImportLookup } from './analyzerFileInfo';
import { Symbol, SymbolFlags, SymbolTable } from './symbol';
import { isTypedDictMemberAccessedThroughIndex } from './symbolUtils';
import { AnyType, ClassType, combineTypes, FunctionType, isAnyOrUnknown, isNoneOrNever,
isTypeSame, maxTypeRecursionCount, ModuleType, NeverType, ObjectType,
OverloadedFunctionType, removeNoneFromUnion, SpecializedFunctionTypes, Type, TypeCategory,
OverloadedFunctionType, SpecializedFunctionTypes, Type, TypeCategory,
TypeVarMap, TypeVarType, UnknownType } from './types';
import { DeclarationType } from './declaration';
export interface ClassMember {
// Symbol
@ -486,9 +486,23 @@ export function lookUpClassMember(classType: Type, memberName: string, importLoo
const symbol = memberFields.get(memberName);
if (symbol && symbol.isClassMember()) {
if (!declaredTypesOnly || symbol.hasTypedDeclarations()) {
let isInstanceMember = false;
// For data classes and typed dicts, variables that are declared
// within the class are treated as instance variables. This distinction
// is important in cases where a variable is a callable type because
// we don't want to bind it to the instance like we would for a
// class member.
if (ClassType.isDataClass(classType) || ClassType.isTypedDictClass(classType)) {
const decls = symbol.getDeclarations();
if (decls.length > 0 && decls[0].type === DeclarationType.Variable) {
isInstanceMember = true;
}
}
return {
symbol,
isInstanceMember: false,
isInstanceMember,
classType
};
}
@ -669,7 +683,10 @@ export function selfSpecializeClassType(type: ClassType, setSkipAbstractClassTes
// Removes the first parameter of the function and returns a new function.
export function stripFirstParameter(type: FunctionType): FunctionType {
return FunctionType.clone(type, true);
if (type.details.parameters.length > 0 && type.details.parameters[0].category === ParameterCategory.Simple) {
return FunctionType.clone(type, true);
}
return type;
}
// Recursively finds all of the type arguments to the specified srcType.

View File

@ -530,52 +530,48 @@ export interface FunctionParameter {
export const enum FunctionTypeFlags {
None = 0,
// Function is an instance method; first parameter is "self";
// can be bound to object instantiated from associated class
InstanceMethod = 1 << 0,
// Function is a __new__ method; first parameter is "cls"
ConstructorMethod = 1 << 1,
ConstructorMethod = 1 << 0,
// Function is decorated with @classmethod; first parameter is "cls";
// can be bound to associated class
ClassMethod = 1 << 2,
ClassMethod = 1 << 1,
// Function is decorated with @staticmethod; cannot be bound to class
StaticMethod = 1 << 3,
StaticMethod = 1 << 2,
// Function is decorated with @abstractmethod
AbstractMethod = 1 << 4,
AbstractMethod = 1 << 3,
// Function contains "yield" or "yield from" statements
Generator = 1 << 5,
Generator = 1 << 4,
// Skip check that validates that all parameters without default
// value expressions have corresponding arguments; used for
// named tuples in some cases
DisableDefaultChecks = 1 << 6,
DisableDefaultChecks = 1 << 5,
// Method has no declaration in user code, it's synthesized; used
// for implied methods such as those used in namedtuple, dataclass, etc.
SynthesizedMethod = 1 << 7,
SynthesizedMethod = 1 << 6,
// Function is decorated with @overload
Overloaded = 1 << 8,
Overloaded = 1 << 7,
// Function is declared with async keyword
Async = 1 << 9,
Async = 1 << 8,
// Indicates that return type should be wrapped in an awaitable type
WrapReturnTypeInAwait = 1 << 10,
WrapReturnTypeInAwait = 1 << 9,
// Function is declared within a type stub fille
StubDefinition = 1 << 11,
StubDefinition = 1 << 10,
// Function is decorated with @final
Final = 1 << 12,
Final = 1 << 11,
// Function has one or more parameters that are missing type annotations
UnannotatedParams = 1 << 13
UnannotatedParams = 1 << 12
}
interface FunctionDetails {
@ -640,8 +636,9 @@ export namespace FunctionType {
// If we strip off the first parameter, this is no longer an
// instance method or class method.
if (deleteFirstParam) {
newFunction.details.flags &= ~(FunctionTypeFlags.InstanceMethod |
newFunction.details.flags &= ~(FunctionTypeFlags.ConstructorMethod |
FunctionTypeFlags.ClassMethod);
newFunction.details.flags |= FunctionTypeFlags.StaticMethod;
newFunction.ignoreFirstParamOfDeclaration = true;
}
@ -673,7 +670,8 @@ export namespace FunctionType {
}
export function isInstanceMethod(type: FunctionType): boolean {
return (type.details.flags & FunctionTypeFlags.InstanceMethod) !== 0;
return (type.details.flags & (FunctionTypeFlags.ConstructorMethod |
FunctionTypeFlags.StaticMethod | FunctionTypeFlags.ClassMethod)) === 0;
}
export function isConstructorMethod(type: FunctionType): boolean {

View File

@ -997,3 +997,8 @@ test('MemberAccess1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['memberAccess1.py']);
validateResults(analysisResults, 0);
});
test('MemberAccess2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['memberAccess2.py']);
validateResults(analysisResults, 0);
});

View File

@ -0,0 +1,43 @@
# This sample verifies that callable variables are bound
# to instances when they appear within most classes, but
# not within TypedDict or data classes.
from typing import Callable, NamedTuple, TypedDict
# This class follows the normal rules where variable
# b, which is callable, acts like a class member and
# is bound to an instance by the member access operator.
class Foo1:
def __init__(self):
self.c = lambda s: s
def a(self, s: str):
return s
b = lambda a_inst, s: a_inst.inner_str + s
sample = Foo1()
a = sample.a('')
b = sample.b('')
c = sample.c('')
d = Foo1.a(Foo1(), '')
e = Foo1.b(Foo1(), '')
# This class is a data class (because it derives from
# named tuple), so all variables that appear to be class
# variables are actually instance variables.
class Foo2(NamedTuple):
a: Callable[[int], int]
foo2 = Foo2(a = lambda a: a)
f = foo2.a(3)
class Foo3(TypedDict):
a: Callable[[int], int]
foo3 = Foo3(a = lambda a: a)
g = foo3['a'](3)