Tipragot
628be439b8
Cela permet de ne pas avoir de problèmes de compatibilité car python est dans le git.
476 lines
14 KiB
Python
476 lines
14 KiB
Python
"""Shared definitions used by different parts of semantic analysis."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import abstractmethod
|
|
from typing import Callable, Final, overload
|
|
from typing_extensions import Literal, Protocol
|
|
|
|
from mypy_extensions import trait
|
|
|
|
from mypy import join
|
|
from mypy.errorcodes import LITERAL_REQ, ErrorCode
|
|
from mypy.nodes import (
|
|
CallExpr,
|
|
ClassDef,
|
|
Context,
|
|
DataclassTransformSpec,
|
|
Decorator,
|
|
Expression,
|
|
FuncDef,
|
|
NameExpr,
|
|
Node,
|
|
OverloadedFuncDef,
|
|
RefExpr,
|
|
SymbolNode,
|
|
SymbolTable,
|
|
SymbolTableNode,
|
|
TypeInfo,
|
|
)
|
|
from mypy.plugin import SemanticAnalyzerPluginInterface
|
|
from mypy.tvar_scope import TypeVarLikeScope
|
|
from mypy.type_visitor import ANY_STRATEGY, BoolTypeQuery
|
|
from mypy.types import (
|
|
TPDICT_FB_NAMES,
|
|
AnyType,
|
|
FunctionLike,
|
|
Instance,
|
|
Parameters,
|
|
ParamSpecFlavor,
|
|
ParamSpecType,
|
|
PlaceholderType,
|
|
ProperType,
|
|
TupleType,
|
|
Type,
|
|
TypeOfAny,
|
|
TypeVarId,
|
|
TypeVarLikeType,
|
|
get_proper_type,
|
|
)
|
|
|
|
# Subclasses can override these Var attributes with incompatible types. This can also be
|
|
# set for individual attributes using 'allow_incompatible_override' of Var.
|
|
ALLOW_INCOMPATIBLE_OVERRIDE: Final = ("__slots__", "__deletable__", "__match_args__")
|
|
|
|
|
|
# Priorities for ordering of patches within the "patch" phase of semantic analysis
|
|
# (after the main pass):
|
|
|
|
# Fix fallbacks (does joins)
|
|
PRIORITY_FALLBACKS: Final = 1
|
|
|
|
|
|
@trait
|
|
class SemanticAnalyzerCoreInterface:
|
|
"""A core abstract interface to generic semantic analyzer functionality.
|
|
|
|
This is implemented by both semantic analyzer passes 2 and 3.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def lookup_qualified(
|
|
self, name: str, ctx: Context, suppress_errors: bool = False
|
|
) -> SymbolTableNode | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def lookup_fully_qualified(self, name: str) -> SymbolTableNode:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def lookup_fully_qualified_or_none(self, name: str) -> SymbolTableNode | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def fail(
|
|
self,
|
|
msg: str,
|
|
ctx: Context,
|
|
serious: bool = False,
|
|
*,
|
|
blocker: bool = False,
|
|
code: ErrorCode | None = None,
|
|
) -> None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def note(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def incomplete_feature_enabled(self, feature: str, ctx: Context) -> bool:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def record_incomplete_ref(self) -> None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def defer(self, debug_context: Context | None = None, force_progress: bool = False) -> None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def is_incomplete_namespace(self, fullname: str) -> bool:
|
|
"""Is a module or class namespace potentially missing some definitions?"""
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
@abstractmethod
|
|
def final_iteration(self) -> bool:
|
|
"""Is this the final iteration of semantic analysis?"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def is_future_flag_set(self, flag: str) -> bool:
|
|
"""Is the specific __future__ feature imported"""
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
@abstractmethod
|
|
def is_stub_file(self) -> bool:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def is_func_scope(self) -> bool:
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
@abstractmethod
|
|
def type(self) -> TypeInfo | None:
|
|
raise NotImplementedError
|
|
|
|
|
|
@trait
|
|
class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface):
|
|
"""A limited abstract interface to some generic semantic analyzer pass 2 functionality.
|
|
|
|
We use this interface for various reasons:
|
|
|
|
* Looser coupling
|
|
* Cleaner import graph
|
|
* Less need to pass around callback functions
|
|
"""
|
|
|
|
tvar_scope: TypeVarLikeScope
|
|
|
|
@abstractmethod
|
|
def lookup(
|
|
self, name: str, ctx: Context, suppress_errors: bool = False
|
|
) -> SymbolTableNode | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def named_type(self, fullname: str, args: list[Type] | None = None) -> Instance:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def named_type_or_none(self, fullname: str, args: list[Type] | None = None) -> Instance | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def accept(self, node: Node) -> None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def anal_type(
|
|
self,
|
|
t: Type,
|
|
*,
|
|
tvar_scope: TypeVarLikeScope | None = None,
|
|
allow_tuple_literal: bool = False,
|
|
allow_unbound_tvars: bool = False,
|
|
allow_required: bool = False,
|
|
allow_placeholder: bool = False,
|
|
report_invalid_types: bool = True,
|
|
prohibit_self_type: str | None = None,
|
|
) -> Type | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def get_and_bind_all_tvars(self, type_exprs: list[Expression]) -> list[TypeVarLikeType]:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance, line: int) -> TypeInfo:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def schedule_patch(self, priority: int, fn: Callable[[], None]) -> None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def add_symbol_table_node(self, name: str, stnode: SymbolTableNode) -> bool:
|
|
"""Add node to the current symbol table."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def current_symbol_table(self) -> SymbolTable:
|
|
"""Get currently active symbol table.
|
|
|
|
May be module, class, or local namespace.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def add_symbol(
|
|
self,
|
|
name: str,
|
|
node: SymbolNode,
|
|
context: Context,
|
|
module_public: bool = True,
|
|
module_hidden: bool = False,
|
|
can_defer: bool = True,
|
|
) -> bool:
|
|
"""Add symbol to the current symbol table."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def add_symbol_skip_local(self, name: str, node: SymbolNode) -> None:
|
|
"""Add symbol to the current symbol table, skipping locals.
|
|
|
|
This is used to store symbol nodes in a symbol table that
|
|
is going to be serialized (local namespaces are not serialized).
|
|
See implementation docstring for more details.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def parse_bool(self, expr: Expression) -> bool | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def qualified_name(self, n: str) -> str:
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
@abstractmethod
|
|
def is_typeshed_stub_file(self) -> bool:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def process_placeholder(
|
|
self, name: str | None, kind: str, ctx: Context, force_progress: bool = False
|
|
) -> None:
|
|
raise NotImplementedError
|
|
|
|
|
|
def set_callable_name(sig: Type, fdef: FuncDef) -> ProperType:
|
|
sig = get_proper_type(sig)
|
|
if isinstance(sig, FunctionLike):
|
|
if fdef.info:
|
|
if fdef.info.fullname in TPDICT_FB_NAMES:
|
|
# Avoid exposing the internal _TypedDict name.
|
|
class_name = "TypedDict"
|
|
else:
|
|
class_name = fdef.info.name
|
|
return sig.with_name(f"{fdef.name} of {class_name}")
|
|
else:
|
|
return sig.with_name(fdef.name)
|
|
else:
|
|
return sig
|
|
|
|
|
|
def calculate_tuple_fallback(typ: TupleType) -> None:
|
|
"""Calculate a precise item type for the fallback of a tuple type.
|
|
|
|
This must be called only after the main semantic analysis pass, since joins
|
|
aren't available before that.
|
|
|
|
Note that there is an apparent chicken and egg problem with respect
|
|
to verifying type arguments against bounds. Verifying bounds might
|
|
require fallbacks, but we might use the bounds to calculate the
|
|
fallbacks. In practice this is not a problem, since the worst that
|
|
can happen is that we have invalid type argument values, and these
|
|
can happen in later stages as well (they will generate errors, but
|
|
we don't prevent their existence).
|
|
"""
|
|
fallback = typ.partial_fallback
|
|
assert fallback.type.fullname == "builtins.tuple"
|
|
fallback.args = (join.join_type_list(list(typ.items)),) + fallback.args[1:]
|
|
|
|
|
|
class _NamedTypeCallback(Protocol):
|
|
def __call__(self, fully_qualified_name: str, args: list[Type] | None = None) -> Instance:
|
|
...
|
|
|
|
|
|
def paramspec_args(
|
|
name: str,
|
|
fullname: str,
|
|
id: TypeVarId | int,
|
|
*,
|
|
named_type_func: _NamedTypeCallback,
|
|
line: int = -1,
|
|
column: int = -1,
|
|
prefix: Parameters | None = None,
|
|
) -> ParamSpecType:
|
|
return ParamSpecType(
|
|
name,
|
|
fullname,
|
|
id,
|
|
flavor=ParamSpecFlavor.ARGS,
|
|
upper_bound=named_type_func("builtins.tuple", [named_type_func("builtins.object")]),
|
|
default=AnyType(TypeOfAny.from_omitted_generics),
|
|
line=line,
|
|
column=column,
|
|
prefix=prefix,
|
|
)
|
|
|
|
|
|
def paramspec_kwargs(
|
|
name: str,
|
|
fullname: str,
|
|
id: TypeVarId | int,
|
|
*,
|
|
named_type_func: _NamedTypeCallback,
|
|
line: int = -1,
|
|
column: int = -1,
|
|
prefix: Parameters | None = None,
|
|
) -> ParamSpecType:
|
|
return ParamSpecType(
|
|
name,
|
|
fullname,
|
|
id,
|
|
flavor=ParamSpecFlavor.KWARGS,
|
|
upper_bound=named_type_func(
|
|
"builtins.dict", [named_type_func("builtins.str"), named_type_func("builtins.object")]
|
|
),
|
|
default=AnyType(TypeOfAny.from_omitted_generics),
|
|
line=line,
|
|
column=column,
|
|
prefix=prefix,
|
|
)
|
|
|
|
|
|
class HasPlaceholders(BoolTypeQuery):
|
|
def __init__(self) -> None:
|
|
super().__init__(ANY_STRATEGY)
|
|
|
|
def visit_placeholder_type(self, t: PlaceholderType) -> bool:
|
|
return True
|
|
|
|
|
|
def has_placeholder(typ: Type) -> bool:
|
|
"""Check if a type contains any placeholder types (recursively)."""
|
|
return typ.accept(HasPlaceholders())
|
|
|
|
|
|
def find_dataclass_transform_spec(node: Node | None) -> DataclassTransformSpec | None:
|
|
"""
|
|
Find the dataclass transform spec for the given node, if any exists.
|
|
|
|
Per PEP 681 (https://peps.python.org/pep-0681/#the-dataclass-transform-decorator), dataclass
|
|
transforms can be specified in multiple ways, including decorator functions and
|
|
metaclasses/base classes. This function resolves the spec from any of these variants.
|
|
"""
|
|
|
|
# The spec only lives on the function/class definition itself, so we need to unwrap down to that
|
|
# point
|
|
if isinstance(node, CallExpr):
|
|
# Like dataclasses.dataclass, transform-based decorators can be applied either with or
|
|
# without parameters; ie, both of these forms are accepted:
|
|
#
|
|
# @typing.dataclass_transform
|
|
# class Foo: ...
|
|
# @typing.dataclass_transform(eq=True, order=True, ...)
|
|
# class Bar: ...
|
|
#
|
|
# We need to unwrap the call for the second variant.
|
|
node = node.callee
|
|
|
|
if isinstance(node, RefExpr):
|
|
node = node.node
|
|
|
|
if isinstance(node, Decorator):
|
|
# typing.dataclass_transform usage must always result in a Decorator; it always uses the
|
|
# `@dataclass_transform(...)` syntax and never `@dataclass_transform`
|
|
node = node.func
|
|
|
|
if isinstance(node, OverloadedFuncDef):
|
|
# The dataclass_transform decorator may be attached to any single overload, so we must
|
|
# search them all.
|
|
# Note that using more than one decorator is undefined behavior, so we can just take the
|
|
# first that we find.
|
|
for candidate in node.items:
|
|
spec = find_dataclass_transform_spec(candidate)
|
|
if spec is not None:
|
|
return spec
|
|
return find_dataclass_transform_spec(node.impl)
|
|
|
|
# For functions, we can directly consult the AST field for the spec
|
|
if isinstance(node, FuncDef):
|
|
return node.dataclass_transform_spec
|
|
|
|
if isinstance(node, ClassDef):
|
|
node = node.info
|
|
if isinstance(node, TypeInfo):
|
|
# Search all parent classes to see if any are decorated with `typing.dataclass_transform`
|
|
for base in node.mro[1:]:
|
|
if base.dataclass_transform_spec is not None:
|
|
return base.dataclass_transform_spec
|
|
|
|
# Check if there is a metaclass that is decorated with `typing.dataclass_transform`
|
|
#
|
|
# Note that PEP 681 only discusses using a metaclass that is directly decorated with
|
|
# `typing.dataclass_transform`; subclasses thereof should be treated with dataclass
|
|
# semantics rather than as transforms:
|
|
#
|
|
# > If dataclass_transform is applied to a class, dataclass-like semantics will be assumed
|
|
# > for any class that directly or indirectly derives from the decorated class or uses the
|
|
# > decorated class as a metaclass.
|
|
#
|
|
# The wording doesn't make this entirely explicit, but Pyright (the reference
|
|
# implementation for this PEP) only handles directly-decorated metaclasses.
|
|
metaclass_type = node.metaclass_type
|
|
if metaclass_type is not None and metaclass_type.type.dataclass_transform_spec is not None:
|
|
return metaclass_type.type.dataclass_transform_spec
|
|
|
|
return None
|
|
|
|
|
|
# Never returns `None` if a default is given
|
|
@overload
|
|
def require_bool_literal_argument(
|
|
api: SemanticAnalyzerInterface | SemanticAnalyzerPluginInterface,
|
|
expression: Expression,
|
|
name: str,
|
|
default: Literal[True] | Literal[False],
|
|
) -> bool:
|
|
...
|
|
|
|
|
|
@overload
|
|
def require_bool_literal_argument(
|
|
api: SemanticAnalyzerInterface | SemanticAnalyzerPluginInterface,
|
|
expression: Expression,
|
|
name: str,
|
|
default: None = None,
|
|
) -> bool | None:
|
|
...
|
|
|
|
|
|
def require_bool_literal_argument(
|
|
api: SemanticAnalyzerInterface | SemanticAnalyzerPluginInterface,
|
|
expression: Expression,
|
|
name: str,
|
|
default: bool | None = None,
|
|
) -> bool | None:
|
|
"""Attempt to interpret an expression as a boolean literal, and fail analysis if we can't."""
|
|
value = parse_bool(expression)
|
|
if value is None:
|
|
api.fail(
|
|
f'"{name}" argument must be a True or False literal', expression, code=LITERAL_REQ
|
|
)
|
|
return default
|
|
|
|
return value
|
|
|
|
|
|
def parse_bool(expr: Expression) -> bool | None:
|
|
if isinstance(expr, NameExpr):
|
|
if expr.fullname == "builtins.True":
|
|
return True
|
|
if expr.fullname == "builtins.False":
|
|
return False
|
|
return None
|