Skip to content

Refactor/unify access to static attributes #19254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@
TypeAlias,
TypeAliasStmt,
TypeInfo,
TypeVarExpr,
UnaryExpr,
Var,
WhileStmt,
Expand Down Expand Up @@ -2858,29 +2857,6 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None:
if name in base2.names and base2 not in base.mro:
self.check_compatibility(name, base, base2, typ)

def determine_type_of_member(self, sym: SymbolTableNode) -> Type | None:
# TODO: this duplicates both checkmember.py and analyze_ref_expr(), delete.
if sym.type is not None:
return sym.type
if isinstance(sym.node, SYMBOL_FUNCBASE_TYPES):
return self.function_type(sym.node)
if isinstance(sym.node, TypeInfo):
if sym.node.typeddict_type:
# We special-case TypedDict, because they don't define any constructor.
return self.expr_checker.typeddict_callable(sym.node)
else:
return type_object_type(sym.node, self.named_type)
if isinstance(sym.node, TypeVarExpr):
# Use of TypeVars is rejected in an expression/runtime context, so
# we don't need to check supertype compatibility for them.
return AnyType(TypeOfAny.special_form)
if isinstance(sym.node, TypeAlias):
with self.msg.filter_errors():
# Suppress any errors, they will be given when analyzing the corresponding node.
# Here we may have incorrect options and location context.
return self.expr_checker.alias_type_in_runtime_context(sym.node, ctx=sym.node)
return None

def check_compatibility(
self, name: str, base1: TypeInfo, base2: TypeInfo, ctx: TypeInfo
) -> None:
Expand Down
24 changes: 11 additions & 13 deletions mypy/checker_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
MypyFile,
Node,
RefExpr,
TypeAlias,
SymbolNode,
TypeInfo,
Var,
)
Expand Down Expand Up @@ -64,10 +64,6 @@ def accept(
def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
raise NotImplementedError

@abstractmethod
def module_type(self, node: MypyFile) -> Instance:
raise NotImplementedError

@abstractmethod
def check_call(
self,
Expand Down Expand Up @@ -112,24 +108,26 @@ def check_method_call_by_name(
) -> tuple[Type, Type]:
raise NotImplementedError

@abstractmethod
def alias_type_in_runtime_context(
self, alias: TypeAlias, *, ctx: Context, alias_definition: bool = False
) -> Type:
raise NotImplementedError

@abstractmethod
def visit_typeddict_index_expr(
self, td_type: TypedDictType, index: Expression, setitem: bool = False
) -> tuple[Type, set[str]]:
raise NotImplementedError

@abstractmethod
def typeddict_callable(self, info: TypeInfo) -> CallableType:
def infer_literal_expr_type(self, value: LiteralValue, fallback_name: str) -> Type:
raise NotImplementedError

@abstractmethod
def infer_literal_expr_type(self, value: LiteralValue, fallback_name: str) -> Type:
def analyze_static_reference(
self,
node: SymbolNode,
ctx: Context,
is_lvalue: bool,
*,
include_modules: bool = True,
suppress_errors: bool = False,
) -> Type:
raise NotImplementedError


Expand Down
117 changes: 69 additions & 48 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import time
from collections import defaultdict
from collections.abc import Iterable, Iterator, Sequence
from contextlib import contextmanager
from contextlib import contextmanager, nullcontext
from typing import Callable, ClassVar, Final, Optional, cast, overload
from typing_extensions import TypeAlias as _TypeAlias, assert_never

Expand Down Expand Up @@ -94,6 +94,7 @@
TypedDictExpr,
TypeInfo,
TypeVarExpr,
TypeVarLikeExpr,
TypeVarTupleExpr,
UnaryExpr,
Var,
Expand Down Expand Up @@ -173,6 +174,7 @@
TypeOfAny,
TypeType,
TypeVarId,
TypeVarLikeType,
TypeVarTupleType,
TypeVarType,
UnboundType,
Expand Down Expand Up @@ -377,26 +379,24 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
result = self.analyze_var_ref(node, e)
if isinstance(result, PartialType):
result = self.chk.handle_partial_var_type(result, lvalue, node, e)
elif isinstance(node, FuncDef):
# Reference to a global function.
result = function_type(node, self.named_type("builtins.function"))
elif isinstance(node, Decorator):
result = self.analyze_var_ref(node.var, e)
elif isinstance(node, OverloadedFuncDef):
if node.type is None:
if self.chk.in_checked_function() and node.items:
self.chk.handle_cannot_determine_type(node.name, e)
result = AnyType(TypeOfAny.from_error)
else:
result = node.type
elif isinstance(node, TypeInfo):
# Reference to a type object.
if node.typeddict_type:
# We special-case TypedDict, because they don't define any constructor.
result = self.typeddict_callable(node)
elif node.fullname == "types.NoneType":
# We special case NoneType, because its stub definition is not related to None.
result = TypeType(NoneType())
else:
result = type_object_type(node, self.named_type)
elif isinstance(node, (FuncDef, TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)):
result = self.analyze_static_reference(node, e, e.is_alias_rvalue or lvalue)
else:
if isinstance(node, PlaceholderNode):
assert False, f"PlaceholderNode {node.fullname!r} leaked to checker"
# Unknown reference; use any type implicitly to avoid
# generating extra type errors.
result = AnyType(TypeOfAny.from_error)
if isinstance(node, TypeInfo):
if isinstance(result, CallableType) and isinstance( # type: ignore[misc]
result.ret_type, Instance
):
Expand All @@ -408,30 +408,56 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
# This is the type in a type[] expression, so substitute type
# variables with Any.
result = erasetype.erase_typevars(result)
elif isinstance(node, MypyFile):
# Reference to a module object.
result = self.module_type(node)
elif isinstance(node, Decorator):
result = self.analyze_var_ref(node.var, e)
assert result is not None
return result

def analyze_static_reference(
self,
node: SymbolNode,
ctx: Context,
is_lvalue: bool,
*,
include_modules: bool = True,
suppress_errors: bool = False,
) -> Type:
"""
This is the version of analyze_ref_expr() that doesn't do any deferrals.

This function can be used by member access to "static" attributes. For example,
when accessing module attributes in protocol checks, or accessing attributes of
special kinds (like TypeAlias, TypeInfo, etc.) on an instance or class object.
# TODO: merge with analyze_ref_expr() when we are confident about performance.
"""
if isinstance(node, (Var, Decorator, OverloadedFuncDef)):
return node.type or AnyType(TypeOfAny.special_form)
elif isinstance(node, FuncDef):
return function_type(node, self.named_type("builtins.function"))
elif isinstance(node, TypeInfo):
# Reference to a type object.
if node.typeddict_type:
# We special-case TypedDict, because they don't define any constructor.
return self.typeddict_callable(node)
elif node.fullname == "types.NoneType":
# We special case NoneType, because its stub definition is not related to None.
return TypeType(NoneType())
else:
return type_object_type(node, self.named_type)
elif isinstance(node, TypeAlias):
# Something that refers to a type alias appears in runtime context.
# Note that we suppress bogus errors for alias redefinitions,
# they are already reported in semanal.py.
result = self.alias_type_in_runtime_context(
node, ctx=e, alias_definition=e.is_alias_rvalue or lvalue
)
with self.msg.filter_errors() if suppress_errors else nullcontext():
return self.alias_type_in_runtime_context(
node, ctx=ctx, alias_definition=is_lvalue
)
elif isinstance(node, TypeVarExpr):
return self.named_type("typing.TypeVar")
elif isinstance(node, (ParamSpecExpr, TypeVarTupleExpr)):
result = self.object_type()
else:
if isinstance(node, PlaceholderNode):
assert False, f"PlaceholderNode {node.fullname!r} leaked to checker"
# Unknown reference; use any type implicitly to avoid
# generating extra type errors.
result = AnyType(TypeOfAny.from_error)
assert result is not None
return result
return self.object_type()
elif isinstance(node, MypyFile):
# Reference to a module object.
return self.module_type(node) if include_modules else AnyType(TypeOfAny.special_form)
return AnyType(TypeOfAny.from_error)

def analyze_var_ref(self, var: Var, context: Context) -> Type:
if var.type:
Expand Down Expand Up @@ -459,20 +485,21 @@ def module_type(self, node: MypyFile) -> Instance:
# Fall back to a dummy 'object' type instead to
# avoid a crash.
result = self.named_type("builtins.object")
module_attrs = {}
module_attrs: dict[str, Type] = {}
immutable = set()
for name, n in node.names.items():
if not n.module_public:
continue
if isinstance(n.node, Var) and n.node.is_final:
immutable.add(name)
typ = self.chk.determine_type_of_member(n)
if typ:
module_attrs[name] = typ
if n.node is None:
module_attrs[name] = AnyType(TypeOfAny.from_error)
else:
# TODO: what to do about nested module references?
# They are non-trivial because there may be import cycles.
module_attrs[name] = AnyType(TypeOfAny.special_form)
module_attrs[name] = self.analyze_static_reference(
n.node, n.node, False, include_modules=False, suppress_errors=True
)
result.extra_attrs = ExtraAttrs(module_attrs, immutable, node.fullname)
return result

Expand Down Expand Up @@ -961,19 +988,11 @@ def typeddict_callable(self, info: TypeInfo) -> CallableType:
assert info.special_alias is not None
target = info.special_alias.target
assert isinstance(target, ProperType) and isinstance(target, TypedDictType)
expected_types = list(target.items.values())
kinds = [ArgKind.ARG_NAMED] * len(expected_types)
names = list(target.items.keys())
return CallableType(
expected_types,
kinds,
names,
target,
self.named_type("builtins.type"),
variables=info.defn.type_vars,
)
return self.typeddict_callable_from_context(target, info.defn.type_vars)

def typeddict_callable_from_context(self, callee: TypedDictType) -> CallableType:
def typeddict_callable_from_context(
self, callee: TypedDictType, variables: Sequence[TypeVarLikeType] | None = None
) -> CallableType:
return CallableType(
list(callee.items.values()),
[
Expand All @@ -983,6 +1002,8 @@ def typeddict_callable_from_context(self, callee: TypedDictType) -> CallableType
list(callee.items.keys()),
callee,
self.named_type("builtins.type"),
variables=variables,
is_bound=True,
)

def check_typeddict_call_with_kwargs(
Expand Down
56 changes: 12 additions & 44 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
TempNode,
TypeAlias,
TypeInfo,
TypeVarExpr,
TypeVarLikeExpr,
Var,
is_final_node,
)
Expand All @@ -49,7 +49,6 @@
make_simplified_union,
supported_self_type,
tuple_fallback,
type_object_type,
)
from mypy.types import (
AnyType,
Expand Down Expand Up @@ -537,24 +536,20 @@ def analyze_member_var_access(
is_trivial_self = vv.func.is_trivial_self and not vv.decorators
if mx.is_super and not mx.suppress_errors:
validate_super_call(vv.func, mx)
if isinstance(v, FuncDef):
assert False, "Did not expect a function"
if isinstance(v, MypyFile):
mx.chk.module_refs.add(v.fullname)

if isinstance(vv, TypeInfo):
if isinstance(vv, (TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)):
# If the associated variable is a TypeInfo synthesize a Var node for
# the purposes of type checking. This enables us to type check things
# like accessing class attributes on an inner class.
v = Var(name, type=type_object_type(vv, mx.named_type))
v.info = info

if isinstance(vv, TypeAlias):
# Similar to the above TypeInfo case, we allow using
# qualified type aliases in runtime context if it refers to an
# instance type. For example:
# like accessing class attributes on an inner class. Similar we allow
# using qualified type aliases in runtime context. For example:
# class C:
# A = List[int]
# x = C.A() <- this is OK
typ = mx.chk.expr_checker.alias_type_in_runtime_context(
vv, ctx=mx.context, alias_definition=mx.is_lvalue
)
typ = mx.chk.expr_checker.analyze_static_reference(vv, mx.context, mx.is_lvalue)
v = Var(name, type=typ)
v.info = info

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

return analyze_var(name, v, itype, mx, implicit=implicit, is_trivial_self=is_trivial_self)
elif isinstance(v, FuncDef):
assert False, "Did not expect a function"
elif isinstance(v, MypyFile):
mx.chk.module_refs.add(v.fullname)
return mx.chk.expr_checker.module_type(v)
elif isinstance(v, TypeVarExpr):
return mx.chk.named_type("typing.TypeVar")
elif (
not v
and name not in ["__getattr__", "__setattr__", "__getattribute__"]
Expand Down Expand Up @@ -1259,29 +1247,9 @@ def analyze_class_attribute_access(
mx.not_ready_callback(name, mx.context)
return AnyType(TypeOfAny.special_form)

if isinstance(node.node, TypeVarExpr):
mx.fail(message_registry.CANNOT_USE_TYPEVAR_AS_EXPRESSION.format(info.name, name))
return AnyType(TypeOfAny.from_error)

# TODO: some logic below duplicates analyze_ref_expr in checkexpr.py
if isinstance(node.node, TypeInfo):
if node.node.typeddict_type:
# We special-case TypedDict, because they don't define any constructor.
return mx.chk.expr_checker.typeddict_callable(node.node)
elif node.node.fullname == "types.NoneType":
# We special case NoneType, because its stub definition is not related to None.
return TypeType(NoneType())
else:
return type_object_type(node.node, mx.named_type)

if isinstance(node.node, MypyFile):
# Reference to a module object.
return mx.named_type("types.ModuleType")

if isinstance(node.node, TypeAlias):
return mx.chk.expr_checker.alias_type_in_runtime_context(
node.node, ctx=mx.context, alias_definition=mx.is_lvalue
)
if isinstance(node.node, (TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)):
# TODO: should we apply class plugin here (similar to instance access)?
return mx.chk.expr_checker.analyze_static_reference(node.node, mx.context, mx.is_lvalue)

if is_decorated:
assert isinstance(node.node, Decorator)
Expand Down
1 change: 0 additions & 1 deletion mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ def with_additional_msg(self, info: str) -> ErrorMessage:

# TypeVar
INCOMPATIBLE_TYPEVAR_VALUE: Final = 'Value of type variable "{}" of {} cannot be {}'
CANNOT_USE_TYPEVAR_AS_EXPRESSION: Final = 'Type variable "{}.{}" cannot be used as an expression'
INVALID_TYPEVAR_AS_TYPEARG: Final = 'Type variable "{}" not valid as type argument value for "{}"'
INVALID_TYPEVAR_ARG_BOUND: Final = 'Type argument {} of "{}" must be a subtype of {}'
INVALID_TYPEVAR_ARG_VALUE: Final = 'Invalid type argument value for "{}"'
Expand Down
Loading