Skip to content

Commit 929377a

Browse files
authored
Refactor/unify access to static attributes (#19254)
Fixes #3832 Fixes #5723 Fixes #17174 Improves #7217 This is a sixth "major" PR toward #7724. Previously access to "static" attributes (like type aliases, class objects) was duplicated in four places: * In `analyze_ref_expr()` * In `determine_type_of_member()` (for modules as subtypes of protocols) * In instance attribute access logic * In class attribute logic Most of these were somewhat incomplete and/or inconsistent, this PR unifies all four (there is still tiny duplication because I decided to limit the number of deferrals, i.e. preserve the existing logic in this respect). Some notable things that are not pure refactoring: * Previously we disabled access to type variables as class attributes. This was inconsistent with plain references and instance attributes that just return `Instance("typing.TypeVar")`. * Instance access plugins were only applied on `TypeInfo`s and `TypeAlias`es, now they are applied always. * Previously arguments kinds were sometimes not correct for TypedDict class objects with non-required keys. * I tweaked `TypeOfAny` in couple places to be more logical.
1 parent ac511d6 commit 929377a

15 files changed

+141
-138
lines changed

mypy/checker.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@
117117
TypeAlias,
118118
TypeAliasStmt,
119119
TypeInfo,
120-
TypeVarExpr,
121120
UnaryExpr,
122121
Var,
123122
WhileStmt,
@@ -2858,29 +2857,6 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None:
28582857
if name in base2.names and base2 not in base.mro:
28592858
self.check_compatibility(name, base, base2, typ)
28602859

2861-
def determine_type_of_member(self, sym: SymbolTableNode) -> Type | None:
2862-
# TODO: this duplicates both checkmember.py and analyze_ref_expr(), delete.
2863-
if sym.type is not None:
2864-
return sym.type
2865-
if isinstance(sym.node, SYMBOL_FUNCBASE_TYPES):
2866-
return self.function_type(sym.node)
2867-
if isinstance(sym.node, TypeInfo):
2868-
if sym.node.typeddict_type:
2869-
# We special-case TypedDict, because they don't define any constructor.
2870-
return self.expr_checker.typeddict_callable(sym.node)
2871-
else:
2872-
return type_object_type(sym.node, self.named_type)
2873-
if isinstance(sym.node, TypeVarExpr):
2874-
# Use of TypeVars is rejected in an expression/runtime context, so
2875-
# we don't need to check supertype compatibility for them.
2876-
return AnyType(TypeOfAny.special_form)
2877-
if isinstance(sym.node, TypeAlias):
2878-
with self.msg.filter_errors():
2879-
# Suppress any errors, they will be given when analyzing the corresponding node.
2880-
# Here we may have incorrect options and location context.
2881-
return self.expr_checker.alias_type_in_runtime_context(sym.node, ctx=sym.node)
2882-
return None
2883-
28842860
def check_compatibility(
28852861
self, name: str, base1: TypeInfo, base2: TypeInfo, ctx: TypeInfo
28862862
) -> None:

mypy/checker_shared.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
MypyFile,
2222
Node,
2323
RefExpr,
24-
TypeAlias,
24+
SymbolNode,
2525
TypeInfo,
2626
Var,
2727
)
@@ -64,10 +64,6 @@ def accept(
6464
def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
6565
raise NotImplementedError
6666

67-
@abstractmethod
68-
def module_type(self, node: MypyFile) -> Instance:
69-
raise NotImplementedError
70-
7167
@abstractmethod
7268
def check_call(
7369
self,
@@ -112,24 +108,26 @@ def check_method_call_by_name(
112108
) -> tuple[Type, Type]:
113109
raise NotImplementedError
114110

115-
@abstractmethod
116-
def alias_type_in_runtime_context(
117-
self, alias: TypeAlias, *, ctx: Context, alias_definition: bool = False
118-
) -> Type:
119-
raise NotImplementedError
120-
121111
@abstractmethod
122112
def visit_typeddict_index_expr(
123113
self, td_type: TypedDictType, index: Expression, setitem: bool = False
124114
) -> tuple[Type, set[str]]:
125115
raise NotImplementedError
126116

127117
@abstractmethod
128-
def typeddict_callable(self, info: TypeInfo) -> CallableType:
118+
def infer_literal_expr_type(self, value: LiteralValue, fallback_name: str) -> Type:
129119
raise NotImplementedError
130120

131121
@abstractmethod
132-
def infer_literal_expr_type(self, value: LiteralValue, fallback_name: str) -> Type:
122+
def analyze_static_reference(
123+
self,
124+
node: SymbolNode,
125+
ctx: Context,
126+
is_lvalue: bool,
127+
*,
128+
include_modules: bool = True,
129+
suppress_errors: bool = False,
130+
) -> Type:
133131
raise NotImplementedError
134132

135133

mypy/checkexpr.py

Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import time
88
from collections import defaultdict
99
from collections.abc import Iterable, Iterator, Sequence
10-
from contextlib import contextmanager
10+
from contextlib import contextmanager, nullcontext
1111
from typing import Callable, ClassVar, Final, Optional, cast, overload
1212
from typing_extensions import TypeAlias as _TypeAlias, assert_never
1313

@@ -94,6 +94,7 @@
9494
TypedDictExpr,
9595
TypeInfo,
9696
TypeVarExpr,
97+
TypeVarLikeExpr,
9798
TypeVarTupleExpr,
9899
UnaryExpr,
99100
Var,
@@ -173,6 +174,7 @@
173174
TypeOfAny,
174175
TypeType,
175176
TypeVarId,
177+
TypeVarLikeType,
176178
TypeVarTupleType,
177179
TypeVarType,
178180
UnboundType,
@@ -377,26 +379,24 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
377379
result = self.analyze_var_ref(node, e)
378380
if isinstance(result, PartialType):
379381
result = self.chk.handle_partial_var_type(result, lvalue, node, e)
380-
elif isinstance(node, FuncDef):
381-
# Reference to a global function.
382-
result = function_type(node, self.named_type("builtins.function"))
382+
elif isinstance(node, Decorator):
383+
result = self.analyze_var_ref(node.var, e)
383384
elif isinstance(node, OverloadedFuncDef):
384385
if node.type is None:
385386
if self.chk.in_checked_function() and node.items:
386387
self.chk.handle_cannot_determine_type(node.name, e)
387388
result = AnyType(TypeOfAny.from_error)
388389
else:
389390
result = node.type
390-
elif isinstance(node, TypeInfo):
391-
# Reference to a type object.
392-
if node.typeddict_type:
393-
# We special-case TypedDict, because they don't define any constructor.
394-
result = self.typeddict_callable(node)
395-
elif node.fullname == "types.NoneType":
396-
# We special case NoneType, because its stub definition is not related to None.
397-
result = TypeType(NoneType())
398-
else:
399-
result = type_object_type(node, self.named_type)
391+
elif isinstance(node, (FuncDef, TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)):
392+
result = self.analyze_static_reference(node, e, e.is_alias_rvalue or lvalue)
393+
else:
394+
if isinstance(node, PlaceholderNode):
395+
assert False, f"PlaceholderNode {node.fullname!r} leaked to checker"
396+
# Unknown reference; use any type implicitly to avoid
397+
# generating extra type errors.
398+
result = AnyType(TypeOfAny.from_error)
399+
if isinstance(node, TypeInfo):
400400
if isinstance(result, CallableType) and isinstance( # type: ignore[misc]
401401
result.ret_type, Instance
402402
):
@@ -408,30 +408,56 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
408408
# This is the type in a type[] expression, so substitute type
409409
# variables with Any.
410410
result = erasetype.erase_typevars(result)
411-
elif isinstance(node, MypyFile):
412-
# Reference to a module object.
413-
result = self.module_type(node)
414-
elif isinstance(node, Decorator):
415-
result = self.analyze_var_ref(node.var, e)
411+
assert result is not None
412+
return result
413+
414+
def analyze_static_reference(
415+
self,
416+
node: SymbolNode,
417+
ctx: Context,
418+
is_lvalue: bool,
419+
*,
420+
include_modules: bool = True,
421+
suppress_errors: bool = False,
422+
) -> Type:
423+
"""
424+
This is the version of analyze_ref_expr() that doesn't do any deferrals.
425+
426+
This function can be used by member access to "static" attributes. For example,
427+
when accessing module attributes in protocol checks, or accessing attributes of
428+
special kinds (like TypeAlias, TypeInfo, etc.) on an instance or class object.
429+
# TODO: merge with analyze_ref_expr() when we are confident about performance.
430+
"""
431+
if isinstance(node, (Var, Decorator, OverloadedFuncDef)):
432+
return node.type or AnyType(TypeOfAny.special_form)
433+
elif isinstance(node, FuncDef):
434+
return function_type(node, self.named_type("builtins.function"))
435+
elif isinstance(node, TypeInfo):
436+
# Reference to a type object.
437+
if node.typeddict_type:
438+
# We special-case TypedDict, because they don't define any constructor.
439+
return self.typeddict_callable(node)
440+
elif node.fullname == "types.NoneType":
441+
# We special case NoneType, because its stub definition is not related to None.
442+
return TypeType(NoneType())
443+
else:
444+
return type_object_type(node, self.named_type)
416445
elif isinstance(node, TypeAlias):
417446
# Something that refers to a type alias appears in runtime context.
418447
# Note that we suppress bogus errors for alias redefinitions,
419448
# they are already reported in semanal.py.
420-
result = self.alias_type_in_runtime_context(
421-
node, ctx=e, alias_definition=e.is_alias_rvalue or lvalue
422-
)
449+
with self.msg.filter_errors() if suppress_errors else nullcontext():
450+
return self.alias_type_in_runtime_context(
451+
node, ctx=ctx, alias_definition=is_lvalue
452+
)
423453
elif isinstance(node, TypeVarExpr):
424454
return self.named_type("typing.TypeVar")
425455
elif isinstance(node, (ParamSpecExpr, TypeVarTupleExpr)):
426-
result = self.object_type()
427-
else:
428-
if isinstance(node, PlaceholderNode):
429-
assert False, f"PlaceholderNode {node.fullname!r} leaked to checker"
430-
# Unknown reference; use any type implicitly to avoid
431-
# generating extra type errors.
432-
result = AnyType(TypeOfAny.from_error)
433-
assert result is not None
434-
return result
456+
return self.object_type()
457+
elif isinstance(node, MypyFile):
458+
# Reference to a module object.
459+
return self.module_type(node) if include_modules else AnyType(TypeOfAny.special_form)
460+
return AnyType(TypeOfAny.from_error)
435461

436462
def analyze_var_ref(self, var: Var, context: Context) -> Type:
437463
if var.type:
@@ -459,20 +485,21 @@ def module_type(self, node: MypyFile) -> Instance:
459485
# Fall back to a dummy 'object' type instead to
460486
# avoid a crash.
461487
result = self.named_type("builtins.object")
462-
module_attrs = {}
488+
module_attrs: dict[str, Type] = {}
463489
immutable = set()
464490
for name, n in node.names.items():
465491
if not n.module_public:
466492
continue
467493
if isinstance(n.node, Var) and n.node.is_final:
468494
immutable.add(name)
469-
typ = self.chk.determine_type_of_member(n)
470-
if typ:
471-
module_attrs[name] = typ
495+
if n.node is None:
496+
module_attrs[name] = AnyType(TypeOfAny.from_error)
472497
else:
473498
# TODO: what to do about nested module references?
474499
# They are non-trivial because there may be import cycles.
475-
module_attrs[name] = AnyType(TypeOfAny.special_form)
500+
module_attrs[name] = self.analyze_static_reference(
501+
n.node, n.node, False, include_modules=False, suppress_errors=True
502+
)
476503
result.extra_attrs = ExtraAttrs(module_attrs, immutable, node.fullname)
477504
return result
478505

@@ -961,19 +988,11 @@ def typeddict_callable(self, info: TypeInfo) -> CallableType:
961988
assert info.special_alias is not None
962989
target = info.special_alias.target
963990
assert isinstance(target, ProperType) and isinstance(target, TypedDictType)
964-
expected_types = list(target.items.values())
965-
kinds = [ArgKind.ARG_NAMED] * len(expected_types)
966-
names = list(target.items.keys())
967-
return CallableType(
968-
expected_types,
969-
kinds,
970-
names,
971-
target,
972-
self.named_type("builtins.type"),
973-
variables=info.defn.type_vars,
974-
)
991+
return self.typeddict_callable_from_context(target, info.defn.type_vars)
975992

976-
def typeddict_callable_from_context(self, callee: TypedDictType) -> CallableType:
993+
def typeddict_callable_from_context(
994+
self, callee: TypedDictType, variables: Sequence[TypeVarLikeType] | None = None
995+
) -> CallableType:
977996
return CallableType(
978997
list(callee.items.values()),
979998
[
@@ -983,6 +1002,8 @@ def typeddict_callable_from_context(self, callee: TypedDictType) -> CallableType
9831002
list(callee.items.keys()),
9841003
callee,
9851004
self.named_type("builtins.type"),
1005+
variables=variables,
1006+
is_bound=True,
9861007
)
9871008

9881009
def check_typeddict_call_with_kwargs(

mypy/checkmember.py

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
TempNode,
3535
TypeAlias,
3636
TypeInfo,
37-
TypeVarExpr,
37+
TypeVarLikeExpr,
3838
Var,
3939
is_final_node,
4040
)
@@ -49,7 +49,6 @@
4949
make_simplified_union,
5050
supported_self_type,
5151
tuple_fallback,
52-
type_object_type,
5352
)
5453
from mypy.types import (
5554
AnyType,
@@ -537,24 +536,20 @@ def analyze_member_var_access(
537536
is_trivial_self = vv.func.is_trivial_self and not vv.decorators
538537
if mx.is_super and not mx.suppress_errors:
539538
validate_super_call(vv.func, mx)
539+
if isinstance(v, FuncDef):
540+
assert False, "Did not expect a function"
541+
if isinstance(v, MypyFile):
542+
mx.chk.module_refs.add(v.fullname)
540543

541-
if isinstance(vv, TypeInfo):
544+
if isinstance(vv, (TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)):
542545
# If the associated variable is a TypeInfo synthesize a Var node for
543546
# the purposes of type checking. This enables us to type check things
544-
# like accessing class attributes on an inner class.
545-
v = Var(name, type=type_object_type(vv, mx.named_type))
546-
v.info = info
547-
548-
if isinstance(vv, TypeAlias):
549-
# Similar to the above TypeInfo case, we allow using
550-
# qualified type aliases in runtime context if it refers to an
551-
# instance type. For example:
547+
# like accessing class attributes on an inner class. Similar we allow
548+
# using qualified type aliases in runtime context. For example:
552549
# class C:
553550
# A = List[int]
554551
# x = C.A() <- this is OK
555-
typ = mx.chk.expr_checker.alias_type_in_runtime_context(
556-
vv, ctx=mx.context, alias_definition=mx.is_lvalue
557-
)
552+
typ = mx.chk.expr_checker.analyze_static_reference(vv, mx.context, mx.is_lvalue)
558553
v = Var(name, type=typ)
559554
v.info = info
560555

@@ -567,13 +562,6 @@ def analyze_member_var_access(
567562
check_final_member(name, info, mx.msg, mx.context)
568563

569564
return analyze_var(name, v, itype, mx, implicit=implicit, is_trivial_self=is_trivial_self)
570-
elif isinstance(v, FuncDef):
571-
assert False, "Did not expect a function"
572-
elif isinstance(v, MypyFile):
573-
mx.chk.module_refs.add(v.fullname)
574-
return mx.chk.expr_checker.module_type(v)
575-
elif isinstance(v, TypeVarExpr):
576-
return mx.chk.named_type("typing.TypeVar")
577565
elif (
578566
not v
579567
and name not in ["__getattr__", "__setattr__", "__getattribute__"]
@@ -1259,29 +1247,9 @@ def analyze_class_attribute_access(
12591247
mx.not_ready_callback(name, mx.context)
12601248
return AnyType(TypeOfAny.special_form)
12611249

1262-
if isinstance(node.node, TypeVarExpr):
1263-
mx.fail(message_registry.CANNOT_USE_TYPEVAR_AS_EXPRESSION.format(info.name, name))
1264-
return AnyType(TypeOfAny.from_error)
1265-
1266-
# TODO: some logic below duplicates analyze_ref_expr in checkexpr.py
1267-
if isinstance(node.node, TypeInfo):
1268-
if node.node.typeddict_type:
1269-
# We special-case TypedDict, because they don't define any constructor.
1270-
return mx.chk.expr_checker.typeddict_callable(node.node)
1271-
elif node.node.fullname == "types.NoneType":
1272-
# We special case NoneType, because its stub definition is not related to None.
1273-
return TypeType(NoneType())
1274-
else:
1275-
return type_object_type(node.node, mx.named_type)
1276-
1277-
if isinstance(node.node, MypyFile):
1278-
# Reference to a module object.
1279-
return mx.named_type("types.ModuleType")
1280-
1281-
if isinstance(node.node, TypeAlias):
1282-
return mx.chk.expr_checker.alias_type_in_runtime_context(
1283-
node.node, ctx=mx.context, alias_definition=mx.is_lvalue
1284-
)
1250+
if isinstance(node.node, (TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)):
1251+
# TODO: should we apply class plugin here (similar to instance access)?
1252+
return mx.chk.expr_checker.analyze_static_reference(node.node, mx.context, mx.is_lvalue)
12851253

12861254
if is_decorated:
12871255
assert isinstance(node.node, Decorator)

mypy/message_registry.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
188188

189189
# TypeVar
190190
INCOMPATIBLE_TYPEVAR_VALUE: Final = 'Value of type variable "{}" of {} cannot be {}'
191-
CANNOT_USE_TYPEVAR_AS_EXPRESSION: Final = 'Type variable "{}.{}" cannot be used as an expression'
192191
INVALID_TYPEVAR_AS_TYPEARG: Final = 'Type variable "{}" not valid as type argument value for "{}"'
193192
INVALID_TYPEVAR_ARG_BOUND: Final = 'Type argument {} of "{}" must be a subtype of {}'
194193
INVALID_TYPEVAR_ARG_VALUE: Final = 'Invalid type argument value for "{}"'

0 commit comments

Comments
 (0)