187 lines
7.5 KiB
Python
187 lines
7.5 KiB
Python
|
"""Calculate some properties of classes.
|
||
|
|
||
|
These happen after semantic analysis and before type checking.
|
||
|
"""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
from typing import Final
|
||
|
|
||
|
from mypy.errors import Errors
|
||
|
from mypy.nodes import (
|
||
|
IMPLICITLY_ABSTRACT,
|
||
|
IS_ABSTRACT,
|
||
|
CallExpr,
|
||
|
Decorator,
|
||
|
FuncDef,
|
||
|
Node,
|
||
|
OverloadedFuncDef,
|
||
|
PromoteExpr,
|
||
|
SymbolTable,
|
||
|
TypeInfo,
|
||
|
Var,
|
||
|
)
|
||
|
from mypy.options import Options
|
||
|
from mypy.types import MYPYC_NATIVE_INT_NAMES, Instance, ProperType
|
||
|
|
||
|
# Hard coded type promotions (shared between all Python versions).
|
||
|
# These add extra ad-hoc edges to the subtyping relation. For example,
|
||
|
# int is considered a subtype of float, even though there is no
|
||
|
# subclass relationship.
|
||
|
# Note that the bytearray -> bytes promotion is a little unsafe
|
||
|
# as some functions only accept bytes objects. Here convenience
|
||
|
# trumps safety.
|
||
|
TYPE_PROMOTIONS: Final = {
|
||
|
"builtins.int": "float",
|
||
|
"builtins.float": "complex",
|
||
|
"builtins.bytearray": "bytes",
|
||
|
"builtins.memoryview": "bytes",
|
||
|
}
|
||
|
|
||
|
|
||
|
def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: Errors) -> None:
|
||
|
"""Calculate abstract status of a class.
|
||
|
|
||
|
Set is_abstract of the type to True if the type has an unimplemented
|
||
|
abstract attribute. Also compute a list of abstract attributes.
|
||
|
Report error is required ABCMeta metaclass is missing.
|
||
|
"""
|
||
|
if typ.typeddict_type:
|
||
|
return # TypedDict can't be abstract
|
||
|
concrete: set[str] = set()
|
||
|
# List of abstract attributes together with their abstract status
|
||
|
abstract: list[tuple[str, int]] = []
|
||
|
abstract_in_this_class: list[str] = []
|
||
|
if typ.is_newtype:
|
||
|
# Special case: NewTypes are considered as always non-abstract, so they can be used as:
|
||
|
# Config = NewType('Config', Mapping[str, str])
|
||
|
# default = Config({'cannot': 'modify'}) # OK
|
||
|
typ.abstract_attributes = []
|
||
|
return
|
||
|
for base in typ.mro:
|
||
|
for name, symnode in base.names.items():
|
||
|
node = symnode.node
|
||
|
if isinstance(node, OverloadedFuncDef):
|
||
|
# Unwrap an overloaded function definition. We can just
|
||
|
# check arbitrarily the first overload item. If the
|
||
|
# different items have a different abstract status, there
|
||
|
# should be an error reported elsewhere.
|
||
|
if node.items: # can be empty for invalid overloads
|
||
|
func: Node | None = node.items[0]
|
||
|
else:
|
||
|
func = None
|
||
|
else:
|
||
|
func = node
|
||
|
if isinstance(func, Decorator):
|
||
|
func = func.func
|
||
|
if isinstance(func, FuncDef):
|
||
|
if (
|
||
|
func.abstract_status in (IS_ABSTRACT, IMPLICITLY_ABSTRACT)
|
||
|
and name not in concrete
|
||
|
):
|
||
|
typ.is_abstract = True
|
||
|
abstract.append((name, func.abstract_status))
|
||
|
if base is typ:
|
||
|
abstract_in_this_class.append(name)
|
||
|
elif isinstance(node, Var):
|
||
|
if node.is_abstract_var and name not in concrete:
|
||
|
typ.is_abstract = True
|
||
|
abstract.append((name, IS_ABSTRACT))
|
||
|
if base is typ:
|
||
|
abstract_in_this_class.append(name)
|
||
|
concrete.add(name)
|
||
|
# In stubs, abstract classes need to be explicitly marked because it is too
|
||
|
# easy to accidentally leave a concrete class abstract by forgetting to
|
||
|
# implement some methods.
|
||
|
typ.abstract_attributes = sorted(abstract)
|
||
|
if is_stub_file:
|
||
|
if typ.declared_metaclass and typ.declared_metaclass.type.has_base("abc.ABCMeta"):
|
||
|
return
|
||
|
if typ.is_protocol:
|
||
|
return
|
||
|
if abstract and not abstract_in_this_class:
|
||
|
|
||
|
def report(message: str, severity: str) -> None:
|
||
|
errors.report(typ.line, typ.column, message, severity=severity)
|
||
|
|
||
|
attrs = ", ".join(f'"{attr}"' for attr, _ in sorted(abstract))
|
||
|
report(f"Class {typ.fullname} has abstract attributes {attrs}", "error")
|
||
|
report(
|
||
|
"If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass", "note"
|
||
|
)
|
||
|
if typ.is_final and abstract:
|
||
|
attrs = ", ".join(f'"{attr}"' for attr, _ in sorted(abstract))
|
||
|
errors.report(
|
||
|
typ.line, typ.column, f"Final class {typ.fullname} has abstract attributes {attrs}"
|
||
|
)
|
||
|
|
||
|
|
||
|
def check_protocol_status(info: TypeInfo, errors: Errors) -> None:
|
||
|
"""Check that all classes in MRO of a protocol are protocols"""
|
||
|
if info.is_protocol:
|
||
|
for type in info.bases:
|
||
|
if not type.type.is_protocol and type.type.fullname != "builtins.object":
|
||
|
|
||
|
def report(message: str, severity: str) -> None:
|
||
|
errors.report(info.line, info.column, message, severity=severity)
|
||
|
|
||
|
report("All bases of a protocol must be protocols", "error")
|
||
|
|
||
|
|
||
|
def calculate_class_vars(info: TypeInfo) -> None:
|
||
|
"""Try to infer additional class variables.
|
||
|
|
||
|
Subclass attribute assignments with no type annotation are assumed
|
||
|
to be classvar if overriding a declared classvar from the base
|
||
|
class.
|
||
|
|
||
|
This must happen after the main semantic analysis pass, since
|
||
|
this depends on base class bodies having been fully analyzed.
|
||
|
"""
|
||
|
for name, sym in info.names.items():
|
||
|
node = sym.node
|
||
|
if isinstance(node, Var) and node.info and node.is_inferred and not node.is_classvar:
|
||
|
for base in info.mro[1:]:
|
||
|
member = base.names.get(name)
|
||
|
if member is not None and isinstance(member.node, Var) and member.node.is_classvar:
|
||
|
node.is_classvar = True
|
||
|
|
||
|
|
||
|
def add_type_promotion(
|
||
|
info: TypeInfo, module_names: SymbolTable, options: Options, builtin_names: SymbolTable
|
||
|
) -> None:
|
||
|
"""Setup extra, ad-hoc subtyping relationships between classes (promotion).
|
||
|
|
||
|
This includes things like 'int' being compatible with 'float'.
|
||
|
"""
|
||
|
defn = info.defn
|
||
|
promote_targets: list[ProperType] = []
|
||
|
for decorator in defn.decorators:
|
||
|
if isinstance(decorator, CallExpr):
|
||
|
analyzed = decorator.analyzed
|
||
|
if isinstance(analyzed, PromoteExpr):
|
||
|
# _promote class decorator (undocumented feature).
|
||
|
promote_targets.append(analyzed.type)
|
||
|
if not promote_targets:
|
||
|
if defn.fullname in TYPE_PROMOTIONS:
|
||
|
target_sym = module_names.get(TYPE_PROMOTIONS[defn.fullname])
|
||
|
if defn.fullname == "builtins.bytearray" and options.disable_bytearray_promotion:
|
||
|
target_sym = None
|
||
|
elif defn.fullname == "builtins.memoryview" and options.disable_memoryview_promotion:
|
||
|
target_sym = None
|
||
|
# With test stubs, the target may not exist.
|
||
|
if target_sym:
|
||
|
target_info = target_sym.node
|
||
|
assert isinstance(target_info, TypeInfo)
|
||
|
promote_targets.append(Instance(target_info, []))
|
||
|
# Special case the promotions between 'int' and native integer types.
|
||
|
# These have promotions going both ways, such as from 'int' to 'i64'
|
||
|
# and 'i64' to 'int', for convenience.
|
||
|
if defn.fullname in MYPYC_NATIVE_INT_NAMES:
|
||
|
int_sym = builtin_names["int"]
|
||
|
assert isinstance(int_sym.node, TypeInfo)
|
||
|
int_sym.node._promote.append(Instance(defn.info, []))
|
||
|
defn.info.alt_promote = Instance(int_sym.node, [])
|
||
|
if promote_targets:
|
||
|
defn.info._promote.extend(promote_targets)
|