Skip to content

Commit

Permalink
Added support for custom metaclasses that inject instance variables i…
Browse files Browse the repository at this point in the history
…nto the classes they construct. This addresses #5897. (#5898)

Co-authored-by: Eric Traut <[email protected]>
  • Loading branch information
erictraut and msfterictraut authored Sep 5, 2023
1 parent ffb6eb8 commit 0602883
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 43 deletions.
27 changes: 26 additions & 1 deletion packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
memberAccessFlags = MemberAccessFlags.None,
bindToType?: ClassType | TypeVarType
): TypeResult | undefined {
const memberInfo = getTypeOfClassMemberName(
let memberInfo = getTypeOfClassMemberName(
errorNode,
ClassType.cloneAsInstantiable(objectType),
/* isAccessedThroughObject */ true,
Expand All @@ -1938,6 +1938,27 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
bindToType
);

if (!memberInfo) {
const metaclass = objectType.details.effectiveMetaclass;
if (
metaclass &&
isInstantiableClass(metaclass) &&
!ClassType.isBuiltIn(metaclass, 'type') &&
!ClassType.isSameGenericClass(metaclass, objectType)
) {
memberInfo = getTypeOfClassMemberName(
errorNode,
metaclass,
/* isAccessedThroughObject */ false,
memberName,
usage,
/* diag */ undefined,
memberAccessFlags | MemberAccessFlags.AccessInstanceMembersOnly,
ClassType.cloneAsInstantiable(objectType)
);
}
}

if (memberInfo) {
return {
type: memberInfo.type,
Expand All @@ -1946,6 +1967,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
isAsymmetricAccessor: memberInfo.isAsymmetricAccessor,
};
}

return undefined;
}

Expand Down Expand Up @@ -5495,6 +5517,9 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
if (flags & MemberAccessFlags.AccessClassMembersOnly) {
classLookupFlags |= ClassMemberLookupFlags.SkipInstanceVariables;
}
if (flags & MemberAccessFlags.AccessInstanceMembersOnly) {
classLookupFlags |= ClassMemberLookupFlags.SkipClassVariables;
}
if (flags & MemberAccessFlags.SkipBaseClasses) {
classLookupFlags |= ClassMemberLookupFlags.SkipBaseClasses;
}
Expand Down
20 changes: 12 additions & 8 deletions packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,37 +410,41 @@ export const enum MemberAccessFlags {
// the class are considered.
AccessClassMembersOnly = 1 << 0,

// Consider only instance members, not members that could be
// class members.
AccessInstanceMembersOnly = 1 << 1,

// By default, members of base classes are also searched.
// Set this flag to consider only the specified class' members.
SkipBaseClasses = 1 << 1,
SkipBaseClasses = 1 << 2,

// Do not include the "object" base class in the search.
SkipObjectBaseClass = 1 << 2,
SkipObjectBaseClass = 1 << 3,

// Consider writes to symbols flagged as ClassVars as an error.
DisallowClassVarWrites = 1 << 3,
DisallowClassVarWrites = 1 << 4,

// Normally __new__ is treated as a static method, but when
// it is invoked implicitly through a constructor call, it
// acts like a class method instead.
TreatConstructorAsClassMethod = 1 << 4,
TreatConstructorAsClassMethod = 1 << 5,

// By default, class member lookups start with the class itself
// and fall back on the metaclass if it's not found. This option
// skips the first check.
ConsiderMetaclassOnly = 1 << 5,
ConsiderMetaclassOnly = 1 << 6,

// If an attribute cannot be found when looking for instance
// members, normally an attribute access override method
// (__getattr__, etc.) may provide the missing attribute type.
// This disables this check.
SkipAttributeAccessOverride = 1 << 6,
SkipAttributeAccessOverride = 1 << 7,

// Do not include the class itself, only base classes.
SkipOriginalClass = 1 << 7,
SkipOriginalClass = 1 << 8,

// Do not include the "type" base class in the search.
SkipTypeBaseClass = 1 << 8,
SkipTypeBaseClass = 1 << 9,
}

export interface ValidateTypeArgsOptions {
Expand Down
74 changes: 40 additions & 34 deletions packages/pyright-internal/src/analyzer/typeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,17 @@ export const enum ClassMemberLookupFlags {
// If this flag is set, the instance variables are skipped.
SkipInstanceVariables = 1 << 3,

// By default, both class and instance variables are searched.
// If this flag is set, the class variables are skipped.
SkipClassVariables = 1 << 4,

// By default, the first symbol is returned even if it has only
// an inferred type associated with it. If this flag is set,
// the search looks only for symbols with declared types.
DeclaredTypesOnly = 1 << 4,
DeclaredTypesOnly = 1 << 5,

// Skip the 'type' base class in particular.
SkipTypeBaseClass = 1 << 5,
SkipTypeBaseClass = 1 << 6,
}

export const enum ClassIteratorFlags {
Expand Down Expand Up @@ -1436,40 +1440,42 @@ export function* getClassMemberIterator(
}

// Next look at class members.
const symbol = memberFields.get(memberName);
if (symbol && symbol.isClassMember()) {
const hasDeclaredType = symbol.hasTypedDeclarations();
if (!declaredTypesOnly || hasDeclaredType) {
let isInstanceMember = symbol.isInstanceMember();
let isClassMember = true;

// 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.
const isDataclass = ClassType.isDataClass(specializedMroClass);
const isTypedDict = ClassType.isTypedDictClass(specializedMroClass);
if (hasDeclaredType && (isDataclass || isTypedDict)) {
const decls = symbol.getDeclarations();
if (decls.length > 0 && decls[0].type === DeclarationType.Variable) {
isInstanceMember = true;
isClassMember = isDataclass;
if ((flags & ClassMemberLookupFlags.SkipClassVariables) === 0) {
const symbol = memberFields.get(memberName);
if (symbol && symbol.isClassMember()) {
const hasDeclaredType = symbol.hasTypedDeclarations();
if (!declaredTypesOnly || hasDeclaredType) {
let isInstanceMember = symbol.isInstanceMember();
let isClassMember = true;

// 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.
const isDataclass = ClassType.isDataClass(specializedMroClass);
const isTypedDict = ClassType.isTypedDictClass(specializedMroClass);
if (hasDeclaredType && (isDataclass || isTypedDict)) {
const decls = symbol.getDeclarations();
if (decls.length > 0 && decls[0].type === DeclarationType.Variable) {
isInstanceMember = true;
isClassMember = isDataclass;
}
}
}

const cm: ClassMember = {
symbol,
isInstanceMember,
isClassMember,
isClassVar: symbol.isClassVar(),
classType: specializedMroClass,
isTypeDeclared: hasDeclaredType,
skippedUndeclaredType,
};
yield cm;
} else {
skippedUndeclaredType = true;
const cm: ClassMember = {
symbol,
isInstanceMember,
isClassMember,
isClassVar: symbol.isClassVar(),
classType: specializedMroClass,
isTypeDeclared: hasDeclaredType,
skippedUndeclaredType,
};
yield cm;
} else {
skippedUndeclaredType = true;
}
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions packages/pyright-internal/src/tests/samples/metaclass11.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This sample verifies that the type checker allows access
# to instance variables provided by a metaclass.

from enum import Enum
from typing import Mapping


class Meta(type):
var0 = 3

def __init__(cls, name, bases, dct):
cls.var1 = "hi"


class MyClass(metaclass=Meta):
pass


# This should generate an error because var0 isn't
# accessible via an instance of this class.
MyClass().var0
reveal_type(MyClass.var0, expected_text="int")
MyClass.var0 = 1

reveal_type(MyClass().var1, expected_text="str")
reveal_type(MyClass.var1, expected_text="str")

MyClass.var1 = "hi"
MyClass().var1 = "hi"
5 changes: 5 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ test('Metaclass10', () => {
TestUtils.validateResults(analysisResults, 0);
});

test('Metaclass11', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['metaclass11.py']);
TestUtils.validateResults(analysisResults, 1);
});

test('AssignmentExpr1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['assignmentExpr1.py']);
TestUtils.validateResults(analysisResults, 7);
Expand Down

0 comments on commit 0602883

Please sign in to comment.