| """Plugin for supporting the attrs library (http://www.attrs.org)""" |
| |
| from __future__ import annotations |
| |
| from collections import defaultdict |
| from collections.abc import Iterable, Mapping |
| from functools import reduce |
| from typing import Final, Literal, cast |
| |
| import mypy.plugin # To avoid circular imports. |
| from mypy.applytype import apply_generic_arguments |
| from mypy.errorcodes import LITERAL_REQ |
| from mypy.expandtype import expand_type, expand_type_by_instance |
| from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type |
| from mypy.meet import meet_types |
| from mypy.messages import format_type_bare |
| from mypy.nodes import ( |
| ARG_NAMED, |
| ARG_NAMED_OPT, |
| ARG_OPT, |
| ARG_POS, |
| MDEF, |
| Argument, |
| AssignmentStmt, |
| CallExpr, |
| Context, |
| Decorator, |
| Expression, |
| FuncDef, |
| IndexExpr, |
| JsonDict, |
| LambdaExpr, |
| ListExpr, |
| MemberExpr, |
| NameExpr, |
| OverloadedFuncDef, |
| PlaceholderNode, |
| RefExpr, |
| SymbolTableNode, |
| TempNode, |
| TupleExpr, |
| TypeApplication, |
| TypeInfo, |
| TypeVarExpr, |
| Var, |
| is_class_var, |
| ) |
| from mypy.plugin import SemanticAnalyzerPluginInterface |
| from mypy.plugins.common import ( |
| _get_argument, |
| _get_bool_argument, |
| _get_decorator_bool_argument, |
| add_attribute_to_class, |
| add_method_to_class, |
| deserialize_and_fixup_type, |
| ) |
| from mypy.server.trigger import make_wildcard_trigger |
| from mypy.state import state |
| from mypy.typeops import ( |
| get_type_vars, |
| make_simplified_union, |
| map_type_from_supertype, |
| type_object_type, |
| ) |
| from mypy.types import ( |
| AnyType, |
| CallableType, |
| FunctionLike, |
| Instance, |
| LiteralType, |
| NoneType, |
| Overloaded, |
| ProperType, |
| TupleType, |
| Type, |
| TypeOfAny, |
| TypeType, |
| TypeVarId, |
| TypeVarType, |
| UninhabitedType, |
| UnionType, |
| get_proper_type, |
| ) |
| from mypy.typevars import fill_typevars |
| from mypy.util import unmangle |
| |
| # The names of the different functions that create classes or arguments. |
| attr_class_makers: Final = {"attr.s", "attr.attrs", "attr.attributes"} |
| attr_dataclass_makers: Final = {"attr.dataclass"} |
| attr_frozen_makers: Final = {"attr.frozen", "attrs.frozen"} |
| attr_define_makers: Final = {"attr.define", "attr.mutable", "attrs.define", "attrs.mutable"} |
| attr_attrib_makers: Final = {"attr.ib", "attr.attrib", "attr.attr", "attr.field", "attrs.field"} |
| attr_optional_converters: Final = {"attr.converters.optional", "attrs.converters.optional"} |
| |
| SELF_TVAR_NAME: Final = "_AT" |
| MAGIC_ATTR_NAME: Final = "__attrs_attrs__" |
| MAGIC_ATTR_CLS_NAME_TEMPLATE: Final = "__{}_AttrsAttributes__" # The tuple subclass pattern. |
| ATTRS_INIT_NAME: Final = "__attrs_init__" |
| |
| |
| class Converter: |
| """Holds information about a `converter=` argument""" |
| |
| def __init__(self, init_type: Type | None = None, ret_type: Type | None = None) -> None: |
| self.init_type = init_type |
| self.ret_type = ret_type |
| |
| |
| class Attribute: |
| """The value of an attr.ib() call.""" |
| |
| def __init__( |
| self, |
| name: str, |
| alias: str | None, |
| info: TypeInfo, |
| has_default: bool, |
| init: bool, |
| kw_only: bool, |
| converter: Converter | None, |
| context: Context, |
| init_type: Type | None, |
| ) -> None: |
| self.name = name |
| self.alias = alias |
| self.info = info |
| self.has_default = has_default |
| self.init = init |
| self.kw_only = kw_only |
| self.converter = converter |
| self.context = context |
| self.init_type = init_type |
| |
| def argument(self, ctx: mypy.plugin.ClassDefContext) -> Argument: |
| """Return this attribute as an argument to __init__.""" |
| assert self.init |
| init_type: Type | None = None |
| if self.converter: |
| if self.converter.init_type: |
| init_type = self.converter.init_type |
| if init_type and self.init_type and self.converter.ret_type: |
| # The converter return type should be the same type as the attribute type. |
| # Copy type vars from attr type to converter. |
| converter_vars = get_type_vars(self.converter.ret_type) |
| init_vars = get_type_vars(self.init_type) |
| if converter_vars and len(converter_vars) == len(init_vars): |
| variables = { |
| binder.id: arg for binder, arg in zip(converter_vars, init_vars) |
| } |
| init_type = expand_type(init_type, variables) |
| else: |
| ctx.api.fail("Cannot determine __init__ type from converter", self.context) |
| init_type = AnyType(TypeOfAny.from_error) |
| else: # There is no converter, the init type is the normal type. |
| init_type = self.init_type or self.info[self.name].type |
| |
| unannotated = False |
| if init_type is None: |
| unannotated = True |
| # Convert type not set to Any. |
| init_type = AnyType(TypeOfAny.unannotated) |
| else: |
| proper_type = get_proper_type(init_type) |
| if isinstance(proper_type, AnyType): |
| if proper_type.type_of_any == TypeOfAny.unannotated: |
| unannotated = True |
| |
| if unannotated and ctx.api.options.disallow_untyped_defs: |
| # This is a compromise. If you don't have a type here then the |
| # __init__ will be untyped. But since the __init__ is added it's |
| # pointing at the decorator. So instead we also show the error in the |
| # assignment, which is where you would fix the issue. |
| node = self.info[self.name].node |
| assert node is not None |
| ctx.api.msg.need_annotation_for_var(node, self.context) |
| |
| if self.kw_only: |
| arg_kind = ARG_NAMED_OPT if self.has_default else ARG_NAMED |
| else: |
| arg_kind = ARG_OPT if self.has_default else ARG_POS |
| |
| # Attrs removes leading underscores when creating the __init__ arguments. |
| name = self.alias or self.name.lstrip("_") |
| return Argument(Var(name, init_type), init_type, None, arg_kind) |
| |
| def serialize(self) -> JsonDict: |
| """Serialize this object so it can be saved and restored.""" |
| return { |
| "name": self.name, |
| "alias": self.alias, |
| "has_default": self.has_default, |
| "init": self.init, |
| "kw_only": self.kw_only, |
| "has_converter": self.converter is not None, |
| "converter_init_type": ( |
| self.converter.init_type.serialize() |
| if self.converter and self.converter.init_type |
| else None |
| ), |
| "context_line": self.context.line, |
| "context_column": self.context.column, |
| "init_type": self.init_type.serialize() if self.init_type else None, |
| } |
| |
| @classmethod |
| def deserialize( |
| cls, info: TypeInfo, data: JsonDict, api: SemanticAnalyzerPluginInterface |
| ) -> Attribute: |
| """Return the Attribute that was serialized.""" |
| raw_init_type = data["init_type"] |
| init_type = deserialize_and_fixup_type(raw_init_type, api) if raw_init_type else None |
| raw_converter_init_type = data["converter_init_type"] |
| converter_init_type = ( |
| deserialize_and_fixup_type(raw_converter_init_type, api) |
| if raw_converter_init_type |
| else None |
| ) |
| |
| return Attribute( |
| data["name"], |
| data["alias"], |
| info, |
| data["has_default"], |
| data["init"], |
| data["kw_only"], |
| Converter(converter_init_type) if data["has_converter"] else None, |
| Context(line=data["context_line"], column=data["context_column"]), |
| init_type, |
| ) |
| |
| def expand_typevar_from_subtype(self, sub_type: TypeInfo) -> None: |
| """Expands type vars in the context of a subtype when an attribute is inherited |
| from a generic super type.""" |
| if self.init_type: |
| self.init_type = map_type_from_supertype(self.init_type, sub_type, self.info) |
| else: |
| self.init_type = None |
| |
| |
| def _determine_eq_order(ctx: mypy.plugin.ClassDefContext) -> bool: |
| """ |
| Validate the combination of *cmp*, *eq*, and *order*. Derive the effective |
| value of order. |
| """ |
| cmp = _get_decorator_optional_bool_argument(ctx, "cmp") |
| eq = _get_decorator_optional_bool_argument(ctx, "eq") |
| order = _get_decorator_optional_bool_argument(ctx, "order") |
| |
| if cmp is not None and any((eq is not None, order is not None)): |
| ctx.api.fail('Don\'t mix "cmp" with "eq" and "order"', ctx.reason) |
| |
| # cmp takes precedence due to bw-compatibility. |
| if cmp is not None: |
| return cmp |
| |
| # If left None, equality is on and ordering mirrors equality. |
| if eq is None: |
| eq = True |
| |
| if order is None: |
| order = eq |
| |
| if eq is False and order is True: |
| ctx.api.fail("eq must be True if order is True", ctx.reason) |
| |
| return order |
| |
| |
| def _get_decorator_optional_bool_argument( |
| ctx: mypy.plugin.ClassDefContext, name: str, default: bool | None = None |
| ) -> bool | None: |
| """Return the Optional[bool] argument for the decorator. |
| |
| This handles both @decorator(...) and @decorator. |
| """ |
| if isinstance(ctx.reason, CallExpr): |
| attr_value = _get_argument(ctx.reason, name) |
| if attr_value: |
| if isinstance(attr_value, NameExpr): |
| if attr_value.fullname == "builtins.True": |
| return True |
| if attr_value.fullname == "builtins.False": |
| return False |
| if attr_value.fullname == "builtins.None": |
| return None |
| ctx.api.fail( |
| f'"{name}" argument must be a True, False, or None literal', |
| ctx.reason, |
| code=LITERAL_REQ, |
| ) |
| return default |
| return default |
| else: |
| return default |
| |
| |
| def attr_tag_callback(ctx: mypy.plugin.ClassDefContext) -> None: |
| """Record that we have an attrs class in the main semantic analysis pass. |
| |
| The later pass implemented by attr_class_maker_callback will use this |
| to detect attrs classes in base classes. |
| """ |
| # The value is ignored, only the existence matters. |
| ctx.cls.info.metadata["attrs_tag"] = {} |
| |
| |
| def attr_class_maker_callback( |
| ctx: mypy.plugin.ClassDefContext, |
| auto_attribs_default: bool | None = False, |
| frozen_default: bool = False, |
| slots_default: bool = False, |
| ) -> bool: |
| """Add necessary dunder methods to classes decorated with attr.s. |
| |
| attrs is a package that lets you define classes without writing dull boilerplate code. |
| |
| At a quick glance, the decorator searches the class body for assignments of `attr.ib`s (or |
| annotated variables if auto_attribs=True), then depending on how the decorator is called, |
| it will add an __init__ or all the compare methods. |
| For frozen=True it will turn the attrs into properties. |
| |
| Hashability will be set according to https://www.attrs.org/en/stable/hashing.html. |
| |
| See https://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. |
| |
| If this returns False, some required metadata was not ready yet, and we need another |
| pass. |
| """ |
| with state.strict_optional_set(ctx.api.options.strict_optional): |
| # This hook is called during semantic analysis, but it uses a bunch of |
| # type-checking ops, so it needs the strict optional set properly. |
| return attr_class_maker_callback_impl( |
| ctx, auto_attribs_default, frozen_default, slots_default |
| ) |
| |
| |
| def attr_class_maker_callback_impl( |
| ctx: mypy.plugin.ClassDefContext, |
| auto_attribs_default: bool | None, |
| frozen_default: bool, |
| slots_default: bool, |
| ) -> bool: |
| info = ctx.cls.info |
| |
| init = _get_decorator_bool_argument(ctx, "init", True) |
| frozen = _get_frozen(ctx, frozen_default) |
| order = _determine_eq_order(ctx) |
| slots = _get_decorator_bool_argument(ctx, "slots", slots_default) |
| |
| auto_attribs = _get_decorator_optional_bool_argument(ctx, "auto_attribs", auto_attribs_default) |
| kw_only = _get_decorator_bool_argument(ctx, "kw_only", False) |
| match_args = _get_decorator_bool_argument(ctx, "match_args", True) |
| |
| for super_info in ctx.cls.info.mro[1:-1]: |
| if "attrs_tag" in super_info.metadata and "attrs" not in super_info.metadata: |
| # Super class is not ready yet. Request another pass. |
| return False |
| |
| attributes = _analyze_class(ctx, auto_attribs, kw_only) |
| |
| # Check if attribute types are ready. |
| for attr in attributes: |
| node = info.get(attr.name) |
| if node is None: |
| # This name is likely blocked by some semantic analysis error that |
| # should have been reported already. |
| _add_empty_metadata(info) |
| return True |
| |
| _add_attrs_magic_attribute(ctx, [(attr.name, info[attr.name].type) for attr in attributes]) |
| if slots: |
| _add_slots(ctx, attributes) |
| if match_args and ctx.api.options.python_version[:2] >= (3, 10): |
| # `.__match_args__` is only added for python3.10+, but the argument |
| # exists for earlier versions as well. |
| _add_match_args(ctx, attributes) |
| |
| # Save the attributes so that subclasses can reuse them. |
| ctx.cls.info.metadata["attrs"] = { |
| "attributes": [attr.serialize() for attr in attributes], |
| "frozen": frozen, |
| } |
| |
| adder = MethodAdder(ctx) |
| # If __init__ is not being generated, attrs still generates it as __attrs_init__ instead. |
| _add_init(ctx, attributes, adder, "__init__" if init else ATTRS_INIT_NAME) |
| |
| if order: |
| _add_order(ctx, adder) |
| if frozen: |
| _make_frozen(ctx, attributes) |
| # Frozen classes are hashable by default, even if inheriting from non-frozen ones. |
| hashable: bool | None = _get_decorator_bool_argument( |
| ctx, "hash", True |
| ) and _get_decorator_bool_argument(ctx, "unsafe_hash", True) |
| else: |
| hashable = _get_decorator_optional_bool_argument(ctx, "unsafe_hash") |
| if hashable is None: # unspecified |
| hashable = _get_decorator_optional_bool_argument(ctx, "hash") |
| |
| eq = _get_decorator_optional_bool_argument(ctx, "eq") |
| has_own_hash = "__hash__" in ctx.cls.info.names |
| |
| if has_own_hash or (hashable is None and eq is False): |
| pass # Do nothing. |
| elif hashable: |
| # We copy the `__hash__` signature from `object` to make them hashable. |
| ctx.cls.info.names["__hash__"] = ctx.cls.info.mro[-1].names["__hash__"] |
| else: |
| _remove_hashability(ctx) |
| |
| return True |
| |
| |
| def _get_frozen(ctx: mypy.plugin.ClassDefContext, frozen_default: bool) -> bool: |
| """Return whether this class is frozen.""" |
| if _get_decorator_bool_argument(ctx, "frozen", frozen_default): |
| return True |
| # Subclasses of frozen classes are frozen so check that. |
| for super_info in ctx.cls.info.mro[1:-1]: |
| if "attrs" in super_info.metadata and super_info.metadata["attrs"]["frozen"]: |
| return True |
| return False |
| |
| |
| def _analyze_class( |
| ctx: mypy.plugin.ClassDefContext, auto_attribs: bool | None, kw_only: bool |
| ) -> list[Attribute]: |
| """Analyze the class body of an attr maker, its parents, and return the Attributes found. |
| |
| auto_attribs=True means we'll generate attributes from type annotations also. |
| auto_attribs=None means we'll detect which mode to use. |
| kw_only=True means that all attributes created here will be keyword only args in __init__. |
| """ |
| own_attrs: dict[str, Attribute] = {} |
| if auto_attribs is None: |
| auto_attribs = _detect_auto_attribs(ctx) |
| |
| # Walk the body looking for assignments and decorators. |
| for stmt in ctx.cls.defs.body: |
| if isinstance(stmt, AssignmentStmt): |
| for attr in _attributes_from_assignment(ctx, stmt, auto_attribs, kw_only): |
| # When attrs are defined twice in the same body we want to use the 2nd definition |
| # in the 2nd location. So remove it from the OrderedDict. |
| # Unless it's auto_attribs in which case we want the 2nd definition in the |
| # 1st location. |
| if not auto_attribs and attr.name in own_attrs: |
| del own_attrs[attr.name] |
| own_attrs[attr.name] = attr |
| elif isinstance(stmt, Decorator): |
| _cleanup_decorator(stmt, own_attrs) |
| |
| for attribute in own_attrs.values(): |
| # Even though these look like class level assignments we want them to look like |
| # instance level assignments. |
| if attribute.name in ctx.cls.info.names: |
| node = ctx.cls.info.names[attribute.name].node |
| if isinstance(node, PlaceholderNode): |
| # This node is not ready yet. |
| continue |
| assert isinstance(node, Var) |
| node.is_initialized_in_class = False |
| |
| # Traverse the MRO and collect attributes from the parents. |
| taken_attr_names = set(own_attrs) |
| super_attrs = [] |
| for super_info in ctx.cls.info.mro[1:-1]: |
| if "attrs" in super_info.metadata: |
| # Each class depends on the set of attributes in its attrs ancestors. |
| ctx.api.add_plugin_dependency(make_wildcard_trigger(super_info.fullname)) |
| |
| for data in super_info.metadata["attrs"]["attributes"]: |
| # Only add an attribute if it hasn't been defined before. This |
| # allows for overwriting attribute definitions by subclassing. |
| if data["name"] not in taken_attr_names: |
| a = Attribute.deserialize(super_info, data, ctx.api) |
| a.expand_typevar_from_subtype(ctx.cls.info) |
| super_attrs.append(a) |
| taken_attr_names.add(a.name) |
| attributes = super_attrs + list(own_attrs.values()) |
| |
| # Check the init args for correct default-ness. Note: This has to be done after all the |
| # attributes for all classes have been read, because subclasses can override parents. |
| last_default = False |
| |
| for i, attribute in enumerate(attributes): |
| if not attribute.init: |
| continue |
| |
| if attribute.kw_only: |
| # Keyword-only attributes don't care whether they are default or not. |
| continue |
| |
| # If the issue comes from merging different classes, report it |
| # at the class definition point. |
| context = attribute.context if i >= len(super_attrs) else ctx.cls |
| |
| if not attribute.has_default and last_default: |
| ctx.api.fail("Non-default attributes not allowed after default attributes.", context) |
| last_default |= attribute.has_default |
| |
| return attributes |
| |
| |
| def _add_empty_metadata(info: TypeInfo) -> None: |
| """Add empty metadata to mark that we've finished processing this class.""" |
| info.metadata["attrs"] = {"attributes": [], "frozen": False} |
| |
| |
| def _detect_auto_attribs(ctx: mypy.plugin.ClassDefContext) -> bool: |
| """Return whether auto_attribs should be enabled or disabled. |
| |
| It's disabled if there are any unannotated attribs() |
| """ |
| for stmt in ctx.cls.defs.body: |
| if isinstance(stmt, AssignmentStmt): |
| for lvalue in stmt.lvalues: |
| lvalues, rvalues = _parse_assignments(lvalue, stmt) |
| |
| if len(lvalues) != len(rvalues): |
| # This means we have some assignment that isn't 1 to 1. |
| # It can't be an attrib. |
| continue |
| |
| for lhs, rvalue in zip(lvalues, rvalues): |
| # Check if the right hand side is a call to an attribute maker. |
| if ( |
| isinstance(rvalue, CallExpr) |
| and isinstance(rvalue.callee, RefExpr) |
| and rvalue.callee.fullname in attr_attrib_makers |
| and not stmt.new_syntax |
| ): |
| # This means we have an attrib without an annotation and so |
| # we can't do auto_attribs=True |
| return False |
| return True |
| |
| |
| def _attributes_from_assignment( |
| ctx: mypy.plugin.ClassDefContext, stmt: AssignmentStmt, auto_attribs: bool, kw_only: bool |
| ) -> Iterable[Attribute]: |
| """Return Attribute objects that are created by this assignment. |
| |
| The assignments can look like this: |
| x = attr.ib() |
| x = y = attr.ib() |
| x, y = attr.ib(), attr.ib() |
| or if auto_attribs is enabled also like this: |
| x: type |
| x: type = default_value |
| x: type = attr.ib(...) |
| """ |
| for lvalue in stmt.lvalues: |
| lvalues, rvalues = _parse_assignments(lvalue, stmt) |
| |
| if len(lvalues) != len(rvalues): |
| # This means we have some assignment that isn't 1 to 1. |
| # It can't be an attrib. |
| continue |
| |
| for lhs, rvalue in zip(lvalues, rvalues): |
| # Check if the right hand side is a call to an attribute maker. |
| if ( |
| isinstance(rvalue, CallExpr) |
| and isinstance(rvalue.callee, RefExpr) |
| and rvalue.callee.fullname in attr_attrib_makers |
| ): |
| attr = _attribute_from_attrib_maker(ctx, auto_attribs, kw_only, lhs, rvalue, stmt) |
| if attr: |
| yield attr |
| elif auto_attribs and stmt.type and stmt.new_syntax and not is_class_var(lhs): |
| yield _attribute_from_auto_attrib(ctx, kw_only, lhs, rvalue, stmt) |
| |
| |
| def _cleanup_decorator(stmt: Decorator, attr_map: dict[str, Attribute]) -> None: |
| """Handle decorators in class bodies. |
| |
| `x.default` will set a default value on x |
| `x.validator` and `x.default` will get removed to avoid throwing a type error. |
| """ |
| remove_me = [] |
| for func_decorator in stmt.decorators: |
| if ( |
| isinstance(func_decorator, MemberExpr) |
| and isinstance(func_decorator.expr, NameExpr) |
| and func_decorator.expr.name in attr_map |
| ): |
| if func_decorator.name == "default": |
| attr_map[func_decorator.expr.name].has_default = True |
| |
| if func_decorator.name in ("default", "validator"): |
| # These are decorators on the attrib object that only exist during |
| # class creation time. In order to not trigger a type error later we |
| # just remove them. This might leave us with a Decorator with no |
| # decorators (Emperor's new clothes?) |
| # TODO: It would be nice to type-check these rather than remove them. |
| # default should be Callable[[], T] |
| # validator should be Callable[[Any, 'Attribute', T], Any] |
| # where T is the type of the attribute. |
| remove_me.append(func_decorator) |
| for dec in remove_me: |
| stmt.decorators.remove(dec) |
| |
| |
| def _attribute_from_auto_attrib( |
| ctx: mypy.plugin.ClassDefContext, |
| kw_only: bool, |
| lhs: NameExpr, |
| rvalue: Expression, |
| stmt: AssignmentStmt, |
| ) -> Attribute: |
| """Return an Attribute for a new type assignment.""" |
| name = unmangle(lhs.name) |
| # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) |
| has_rhs = not isinstance(rvalue, TempNode) |
| sym = ctx.cls.info.names.get(name) |
| init_type = sym.type if sym else None |
| return Attribute(name, None, ctx.cls.info, has_rhs, True, kw_only, None, stmt, init_type) |
| |
| |
| def _attribute_from_attrib_maker( |
| ctx: mypy.plugin.ClassDefContext, |
| auto_attribs: bool, |
| kw_only: bool, |
| lhs: NameExpr, |
| rvalue: CallExpr, |
| stmt: AssignmentStmt, |
| ) -> Attribute | None: |
| """Return an Attribute from the assignment or None if you can't make one.""" |
| if auto_attribs and not stmt.new_syntax: |
| # auto_attribs requires an annotation on *every* attr.ib. |
| assert lhs.node is not None |
| ctx.api.msg.need_annotation_for_var(lhs.node, stmt) |
| return None |
| |
| if len(stmt.lvalues) > 1: |
| ctx.api.fail("Too many names for one attribute", stmt) |
| return None |
| |
| # This is the type that belongs in the __init__ method for this attrib. |
| init_type = stmt.type |
| |
| # Read all the arguments from the call. |
| init = _get_bool_argument(ctx, rvalue, "init", True) |
| # Note: If the class decorator says kw_only=True the attribute is ignored. |
| # See https://github.com/python-attrs/attrs/issues/481 for explanation. |
| kw_only |= _get_bool_argument(ctx, rvalue, "kw_only", False) |
| |
| # TODO: Check for attr.NOTHING |
| attr_has_default = bool(_get_argument(rvalue, "default")) |
| attr_has_factory = bool(_get_argument(rvalue, "factory")) |
| |
| if attr_has_default and attr_has_factory: |
| ctx.api.fail('Can\'t pass both "default" and "factory".', rvalue) |
| elif attr_has_factory: |
| attr_has_default = True |
| |
| # If the type isn't set through annotation but is passed through `type=` use that. |
| type_arg = _get_argument(rvalue, "type") |
| if type_arg and not init_type: |
| try: |
| un_type = expr_to_unanalyzed_type(type_arg, ctx.api.options, ctx.api.is_stub_file) |
| except TypeTranslationError: |
| ctx.api.fail("Invalid argument to type", type_arg) |
| else: |
| init_type = ctx.api.anal_type(un_type) |
| if init_type and isinstance(lhs.node, Var) and not lhs.node.type: |
| # If there is no annotation, add one. |
| lhs.node.type = init_type |
| lhs.is_inferred_def = False |
| |
| # Note: convert is deprecated but works the same as converter. |
| converter = _get_argument(rvalue, "converter") |
| convert = _get_argument(rvalue, "convert") |
| if convert and converter: |
| ctx.api.fail('Can\'t pass both "convert" and "converter".', rvalue) |
| elif convert: |
| ctx.api.fail("convert is deprecated, use converter", rvalue) |
| converter = convert |
| converter_info = _parse_converter(ctx, converter) |
| |
| # Custom alias might be defined: |
| alias = None |
| alias_expr = _get_argument(rvalue, "alias") |
| if alias_expr: |
| alias = ctx.api.parse_str_literal(alias_expr) |
| if alias is None: |
| ctx.api.fail( |
| '"alias" argument to attrs field must be a string literal', |
| rvalue, |
| code=LITERAL_REQ, |
| ) |
| name = unmangle(lhs.name) |
| return Attribute( |
| name, alias, ctx.cls.info, attr_has_default, init, kw_only, converter_info, stmt, init_type |
| ) |
| |
| |
| def _parse_converter( |
| ctx: mypy.plugin.ClassDefContext, converter_expr: Expression | None |
| ) -> Converter | None: |
| """Return the Converter object from an Expression.""" |
| # TODO: Support complex converters, e.g. lambdas, calls, etc. |
| if not converter_expr: |
| return None |
| converter_info = Converter() |
| if ( |
| isinstance(converter_expr, CallExpr) |
| and isinstance(converter_expr.callee, RefExpr) |
| and converter_expr.callee.fullname in attr_optional_converters |
| and converter_expr.args |
| and converter_expr.args[0] |
| ): |
| # Special handling for attr.converters.optional(type) |
| # We extract the type and add make the init_args Optional in Attribute.argument |
| converter_expr = converter_expr.args[0] |
| is_attr_converters_optional = True |
| else: |
| is_attr_converters_optional = False |
| |
| converter_type: Type | None = None |
| if isinstance(converter_expr, RefExpr) and converter_expr.node: |
| if isinstance(converter_expr.node, FuncDef): |
| if converter_expr.node.type and isinstance(converter_expr.node.type, FunctionLike): |
| converter_type = converter_expr.node.type |
| else: # The converter is an unannotated function. |
| converter_info.init_type = AnyType(TypeOfAny.unannotated) |
| return converter_info |
| elif isinstance(converter_expr.node, OverloadedFuncDef) and is_valid_overloaded_converter( |
| converter_expr.node |
| ): |
| converter_type = converter_expr.node.type |
| elif isinstance(converter_expr.node, TypeInfo): |
| converter_type = type_object_type(converter_expr.node, ctx.api.named_type) |
| elif ( |
| isinstance(converter_expr, IndexExpr) |
| and isinstance(converter_expr.analyzed, TypeApplication) |
| and isinstance(converter_expr.base, RefExpr) |
| and isinstance(converter_expr.base.node, TypeInfo) |
| ): |
| # The converter is a generic type. |
| converter_type = type_object_type(converter_expr.base.node, ctx.api.named_type) |
| if isinstance(converter_type, CallableType): |
| converter_type = apply_generic_arguments( |
| converter_type, |
| converter_expr.analyzed.types, |
| ctx.api.msg.incompatible_typevar_value, |
| converter_type, |
| ) |
| else: |
| converter_type = None |
| |
| if isinstance(converter_expr, LambdaExpr): |
| # TODO: should we send a fail if converter_expr.min_args > 1? |
| converter_info.init_type = AnyType(TypeOfAny.unannotated) |
| return converter_info |
| |
| if not converter_type: |
| # Signal that we have an unsupported converter. |
| ctx.api.fail( |
| "Unsupported converter, only named functions, types and lambdas are currently " |
| "supported", |
| converter_expr, |
| ) |
| converter_info.init_type = AnyType(TypeOfAny.from_error) |
| return converter_info |
| |
| converter_type = get_proper_type(converter_type) |
| if isinstance(converter_type, CallableType) and converter_type.arg_types: |
| converter_info.init_type = converter_type.arg_types[0] |
| if not is_attr_converters_optional: |
| converter_info.ret_type = converter_type.ret_type |
| elif isinstance(converter_type, Overloaded): |
| types: list[Type] = [] |
| for item in converter_type.items: |
| # Walk the overloads looking for methods that can accept one argument. |
| num_arg_types = len(item.arg_types) |
| if not num_arg_types: |
| continue |
| if num_arg_types > 1 and any(kind == ARG_POS for kind in item.arg_kinds[1:]): |
| continue |
| types.append(item.arg_types[0]) |
| # Make a union of all the valid types. |
| if types: |
| converter_info.init_type = make_simplified_union(types) |
| |
| if is_attr_converters_optional and converter_info.init_type: |
| # If the converter was attr.converter.optional(type) then add None to |
| # the allowed init_type. |
| converter_info.init_type = UnionType.make_union([converter_info.init_type, NoneType()]) |
| |
| return converter_info |
| |
| |
| def is_valid_overloaded_converter(defn: OverloadedFuncDef) -> bool: |
| return all( |
| (not isinstance(item, Decorator) or isinstance(item.func.type, FunctionLike)) |
| for item in defn.items |
| ) |
| |
| |
| def _parse_assignments( |
| lvalue: Expression, stmt: AssignmentStmt |
| ) -> tuple[list[NameExpr], list[Expression]]: |
| """Convert a possibly complex assignment expression into lists of lvalues and rvalues.""" |
| lvalues: list[NameExpr] = [] |
| rvalues: list[Expression] = [] |
| if isinstance(lvalue, (TupleExpr, ListExpr)): |
| if all(isinstance(item, NameExpr) for item in lvalue.items): |
| lvalues = cast(list[NameExpr], lvalue.items) |
| if isinstance(stmt.rvalue, (TupleExpr, ListExpr)): |
| rvalues = stmt.rvalue.items |
| elif isinstance(lvalue, NameExpr): |
| lvalues = [lvalue] |
| rvalues = [stmt.rvalue] |
| return lvalues, rvalues |
| |
| |
| def _add_order(ctx: mypy.plugin.ClassDefContext, adder: MethodAdder) -> None: |
| """Generate all the ordering methods for this class.""" |
| bool_type = ctx.api.named_type("builtins.bool") |
| object_type = ctx.api.named_type("builtins.object") |
| # Make the types be: |
| # AT = TypeVar('AT') |
| # def __lt__(self: AT, other: AT) -> bool |
| # This way comparisons with subclasses will work correctly. |
| fullname = f"{ctx.cls.info.fullname}.{SELF_TVAR_NAME}" |
| tvd = TypeVarType( |
| SELF_TVAR_NAME, |
| fullname, |
| # Namespace is patched per-method below. |
| id=TypeVarId(-1, namespace=""), |
| values=[], |
| upper_bound=object_type, |
| default=AnyType(TypeOfAny.from_omitted_generics), |
| ) |
| self_tvar_expr = TypeVarExpr( |
| SELF_TVAR_NAME, fullname, [], object_type, AnyType(TypeOfAny.from_omitted_generics) |
| ) |
| ctx.cls.info.names[SELF_TVAR_NAME] = SymbolTableNode(MDEF, self_tvar_expr) |
| |
| for method in ["__lt__", "__le__", "__gt__", "__ge__"]: |
| namespace = f"{ctx.cls.info.fullname}.{method}" |
| tvd = tvd.copy_modified(id=TypeVarId(tvd.id.raw_id, namespace=namespace)) |
| args = [Argument(Var("other", tvd), tvd, None, ARG_POS)] |
| adder.add_method(method, args, bool_type, self_type=tvd, tvd=tvd) |
| |
| |
| def _make_frozen(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute]) -> None: |
| """Turn all the attributes into properties to simulate frozen classes.""" |
| for attribute in attributes: |
| if attribute.name in ctx.cls.info.names: |
| # This variable belongs to this class so we can modify it. |
| node = ctx.cls.info.names[attribute.name].node |
| if not isinstance(node, Var): |
| # The superclass attribute was overridden with a non-variable. |
| # No need to do anything here, override will be verified during |
| # type checking. |
| continue |
| node.is_property = True |
| else: |
| # This variable belongs to a super class so create new Var so we |
| # can modify it. |
| var = Var(attribute.name, attribute.init_type) |
| var.info = ctx.cls.info |
| var._fullname = f"{ctx.cls.info.fullname}.{var.name}" |
| ctx.cls.info.names[var.name] = SymbolTableNode(MDEF, var) |
| var.is_property = True |
| |
| |
| def _add_init( |
| ctx: mypy.plugin.ClassDefContext, |
| attributes: list[Attribute], |
| adder: MethodAdder, |
| method_name: Literal["__init__", "__attrs_init__"], |
| ) -> None: |
| """Generate an __init__ method for the attributes and add it to the class.""" |
| # Convert attributes to arguments with kw_only arguments at the end of |
| # the argument list |
| pos_args = [] |
| kw_only_args = [] |
| sym_table = ctx.cls.info.names |
| for attribute in attributes: |
| if not attribute.init: |
| continue |
| if attribute.kw_only: |
| kw_only_args.append(attribute.argument(ctx)) |
| else: |
| pos_args.append(attribute.argument(ctx)) |
| |
| # If the attribute is Final, present in `__init__` and has |
| # no default, make sure it doesn't error later. |
| if not attribute.has_default and attribute.name in sym_table: |
| sym_node = sym_table[attribute.name].node |
| if isinstance(sym_node, Var) and sym_node.is_final: |
| sym_node.final_set_in_init = True |
| args = pos_args + kw_only_args |
| if all( |
| # We use getattr rather than instance checks because the variable.type |
| # might be wrapped into a Union or some other type, but even non-Any |
| # types reliably track the fact that the argument was not annotated. |
| getattr(arg.variable.type, "type_of_any", None) == TypeOfAny.unannotated |
| for arg in args |
| ): |
| # This workaround makes --disallow-incomplete-defs usable with attrs, |
| # but is definitely suboptimal as a long-term solution. |
| # See https://github.com/python/mypy/issues/5954 for discussion. |
| for a in args: |
| a.variable.type = AnyType(TypeOfAny.implementation_artifact) |
| a.type_annotation = AnyType(TypeOfAny.implementation_artifact) |
| adder.add_method(method_name, args, NoneType()) |
| |
| |
| def _add_attrs_magic_attribute( |
| ctx: mypy.plugin.ClassDefContext, attrs: list[tuple[str, Type | None]] |
| ) -> None: |
| any_type = AnyType(TypeOfAny.explicit) |
| attributes_types: list[Type] = [ |
| ctx.api.named_type_or_none("attr.Attribute", [attr_type or any_type]) or any_type |
| for _, attr_type in attrs |
| ] |
| fallback_type = ctx.api.named_type( |
| "builtins.tuple", [ctx.api.named_type_or_none("attr.Attribute", [any_type]) or any_type] |
| ) |
| |
| attr_name = MAGIC_ATTR_CLS_NAME_TEMPLATE.format(ctx.cls.fullname.replace(".", "_")) |
| ti = ctx.api.basic_new_typeinfo(attr_name, fallback_type, 0) |
| for (name, _), attr_type in zip(attrs, attributes_types): |
| var = Var(name, attr_type) |
| var._fullname = name |
| var.is_property = True |
| proper_type = get_proper_type(attr_type) |
| if isinstance(proper_type, Instance): |
| var.info = proper_type.type |
| ti.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True) |
| attributes_type = Instance(ti, []) |
| |
| # We need to stash the type of the magic attribute so it can be |
| # loaded on cached runs. |
| ctx.cls.info.names[attr_name] = SymbolTableNode(MDEF, ti, plugin_generated=True) |
| |
| add_attribute_to_class( |
| ctx.api, |
| ctx.cls, |
| MAGIC_ATTR_NAME, |
| TupleType(attributes_types, fallback=attributes_type), |
| fullname=f"{ctx.cls.fullname}.{MAGIC_ATTR_NAME}", |
| override_allow_incompatible=True, |
| is_classvar=True, |
| ) |
| |
| |
| def _add_slots(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute]) -> None: |
| if any(p.slots is None for p in ctx.cls.info.mro[1:-1]): |
| # At least one type in mro (excluding `self` and `object`) |
| # does not have concrete `__slots__` defined. Ignoring. |
| return |
| |
| # Unlike `@dataclasses.dataclass`, `__slots__` is rewritten here. |
| ctx.cls.info.slots = {attr.name for attr in attributes} |
| |
| # Also, inject `__slots__` attribute to class namespace: |
| slots_type = TupleType( |
| [ctx.api.named_type("builtins.str") for _ in attributes], |
| fallback=ctx.api.named_type("builtins.tuple"), |
| ) |
| add_attribute_to_class(api=ctx.api, cls=ctx.cls, name="__slots__", typ=slots_type) |
| |
| |
| def _add_match_args(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute]) -> None: |
| if ( |
| "__match_args__" not in ctx.cls.info.names |
| or ctx.cls.info.names["__match_args__"].plugin_generated |
| ): |
| str_type = ctx.api.named_type("builtins.str") |
| match_args = TupleType( |
| [ |
| str_type.copy_modified(last_known_value=LiteralType(attr.name, fallback=str_type)) |
| for attr in attributes |
| if not attr.kw_only and attr.init |
| ], |
| fallback=ctx.api.named_type("builtins.tuple"), |
| ) |
| add_attribute_to_class(api=ctx.api, cls=ctx.cls, name="__match_args__", typ=match_args) |
| |
| |
| def _remove_hashability(ctx: mypy.plugin.ClassDefContext) -> None: |
| """Remove hashability from a class.""" |
| add_attribute_to_class( |
| ctx.api, ctx.cls, "__hash__", NoneType(), is_classvar=True, overwrite_existing=True |
| ) |
| |
| |
| class MethodAdder: |
| """Helper to add methods to a TypeInfo. |
| |
| ctx: The ClassDefCtx we are using on which we will add methods. |
| """ |
| |
| # TODO: Combine this with the code build_namedtuple_typeinfo to support both. |
| |
| def __init__(self, ctx: mypy.plugin.ClassDefContext) -> None: |
| self.ctx = ctx |
| self.self_type = fill_typevars(ctx.cls.info) |
| |
| def add_method( |
| self, |
| method_name: str, |
| args: list[Argument], |
| ret_type: Type, |
| self_type: Type | None = None, |
| tvd: TypeVarType | None = None, |
| ) -> None: |
| """Add a method: def <method_name>(self, <args>) -> <ret_type>): ... to info. |
| |
| self_type: The type to use for the self argument or None to use the inferred self type. |
| tvd: If the method is generic these should be the type variables. |
| """ |
| self_type = self_type if self_type is not None else self.self_type |
| add_method_to_class( |
| self.ctx.api, self.ctx.cls, method_name, args, ret_type, self_type, tvd |
| ) |
| |
| |
| def _get_attrs_init_type(typ: Instance) -> CallableType | None: |
| """ |
| If `typ` refers to an attrs class, get the type of its initializer method. |
| """ |
| magic_attr = typ.type.get(MAGIC_ATTR_NAME) |
| if magic_attr is None or not magic_attr.plugin_generated: |
| return None |
| init_method = typ.type.get_method("__init__") or typ.type.get_method(ATTRS_INIT_NAME) |
| if not isinstance(init_method, FuncDef) or not isinstance(init_method.type, CallableType): |
| return None |
| return init_method.type |
| |
| |
| def _fail_not_attrs_class(ctx: mypy.plugin.FunctionSigContext, t: Type, parent_t: Type) -> None: |
| t_name = format_type_bare(t, ctx.api.options) |
| if parent_t is t: |
| msg = ( |
| f'Argument 1 to "evolve" has a variable type "{t_name}" not bound to an attrs class' |
| if isinstance(t, TypeVarType) |
| else f'Argument 1 to "evolve" has incompatible type "{t_name}"; expected an attrs class' |
| ) |
| else: |
| pt_name = format_type_bare(parent_t, ctx.api.options) |
| msg = ( |
| f'Argument 1 to "evolve" has type "{pt_name}" whose item "{t_name}" is not bound to an attrs class' |
| if isinstance(t, TypeVarType) |
| else f'Argument 1 to "evolve" has incompatible type "{pt_name}" whose item "{t_name}" is not an attrs class' |
| ) |
| |
| ctx.api.fail(msg, ctx.context) |
| |
| |
| def _get_expanded_attr_types( |
| ctx: mypy.plugin.FunctionSigContext, |
| typ: ProperType, |
| display_typ: ProperType, |
| parent_typ: ProperType, |
| ) -> list[Mapping[str, Type]] | None: |
| """ |
| For a given type, determine what attrs classes it can be: for each class, return the field types. |
| For generic classes, the field types are expanded. |
| If the type contains Any or a non-attrs type, returns None; in the latter case, also reports an error. |
| """ |
| if isinstance(typ, AnyType): |
| return None |
| elif isinstance(typ, UnionType): |
| ret: list[Mapping[str, Type]] | None = [] |
| for item in typ.relevant_items(): |
| item = get_proper_type(item) |
| item_types = _get_expanded_attr_types(ctx, item, item, parent_typ) |
| if ret is not None and item_types is not None: |
| ret += item_types |
| else: |
| ret = None # but keep iterating to emit all errors |
| return ret |
| elif isinstance(typ, TypeVarType): |
| return _get_expanded_attr_types( |
| ctx, get_proper_type(typ.upper_bound), display_typ, parent_typ |
| ) |
| elif isinstance(typ, Instance): |
| init_func = _get_attrs_init_type(typ) |
| if init_func is None: |
| _fail_not_attrs_class(ctx, display_typ, parent_typ) |
| return None |
| init_func = expand_type_by_instance(init_func, typ) |
| # [1:] to skip the self argument of AttrClass.__init__ |
| field_names = cast(list[str], init_func.arg_names[1:]) |
| field_types = init_func.arg_types[1:] |
| return [dict(zip(field_names, field_types))] |
| else: |
| _fail_not_attrs_class(ctx, display_typ, parent_typ) |
| return None |
| |
| |
| def _meet_fields(types: list[Mapping[str, Type]]) -> Mapping[str, Type]: |
| """ |
| "Meet" the fields of a list of attrs classes, i.e. for each field, its new type will be the lower bound. |
| """ |
| field_to_types = defaultdict(list) |
| for fields in types: |
| for name, typ in fields.items(): |
| field_to_types[name].append(typ) |
| |
| return { |
| name: ( |
| get_proper_type(reduce(meet_types, f_types)) |
| if len(f_types) == len(types) |
| else UninhabitedType() |
| ) |
| for name, f_types in field_to_types.items() |
| } |
| |
| |
| def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> CallableType: |
| """ |
| Generate a signature for the 'attr.evolve' function that's specific to the call site |
| and dependent on the type of the first argument. |
| """ |
| if len(ctx.args) != 2: |
| # Ideally the name and context should be callee's, but we don't have it in FunctionSigContext. |
| ctx.api.fail(f'"{ctx.default_signature.name}" has unexpected type annotation', ctx.context) |
| return ctx.default_signature |
| |
| if len(ctx.args[0]) != 1: |
| return ctx.default_signature # leave it to the type checker to complain |
| |
| inst_arg = ctx.args[0][0] |
| inst_type = get_proper_type(ctx.api.get_expression_type(inst_arg)) |
| inst_type_str = format_type_bare(inst_type, ctx.api.options) |
| |
| attr_types = _get_expanded_attr_types(ctx, inst_type, inst_type, inst_type) |
| if attr_types is None: |
| return ctx.default_signature |
| fields = _meet_fields(attr_types) |
| |
| return CallableType( |
| arg_names=["inst", *fields.keys()], |
| arg_kinds=[ARG_POS] + [ARG_NAMED_OPT] * len(fields), |
| arg_types=[inst_type, *fields.values()], |
| ret_type=inst_type, |
| fallback=ctx.default_signature.fallback, |
| name=f"{ctx.default_signature.name} of {inst_type_str}", |
| ) |
| |
| |
| def fields_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> CallableType: |
| """Provide the signature for `attrs.fields`.""" |
| if len(ctx.args) != 1 or len(ctx.args[0]) != 1: |
| return ctx.default_signature |
| |
| proper_type = get_proper_type(ctx.api.get_expression_type(ctx.args[0][0])) |
| |
| # fields(Any) -> Any, fields(type[Any]) -> Any |
| if ( |
| isinstance(proper_type, AnyType) |
| or isinstance(proper_type, TypeType) |
| and isinstance(proper_type.item, AnyType) |
| ): |
| return ctx.default_signature |
| |
| cls = None |
| arg_types = ctx.default_signature.arg_types |
| |
| if isinstance(proper_type, TypeVarType): |
| inner = get_proper_type(proper_type.upper_bound) |
| if isinstance(inner, Instance): |
| # We need to work arg_types to compensate for the attrs stubs. |
| arg_types = [proper_type] |
| cls = inner.type |
| elif isinstance(proper_type, CallableType): |
| cls = proper_type.type_object() |
| |
| if cls is not None and MAGIC_ATTR_NAME in cls.names: |
| # This is a proper attrs class. |
| ret_type = cls.names[MAGIC_ATTR_NAME].type |
| assert ret_type is not None |
| return ctx.default_signature.copy_modified(arg_types=arg_types, ret_type=ret_type) |
| |
| return ctx.default_signature |