|
6 | 6 | from typing_extensions import Final, Literal
|
7 | 7 |
|
8 | 8 | import mypy.plugin # To avoid circular imports.
|
| 9 | +from mypy.checker import TypeChecker |
9 | 10 | from mypy.errorcodes import LITERAL_REQ
|
10 | 11 | from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
|
| 12 | +from mypy.messages import format_type_bare |
11 | 13 | from mypy.nodes import (
|
12 | 14 | ARG_NAMED,
|
13 | 15 | ARG_NAMED_OPT,
|
|
77 | 79 | SELF_TVAR_NAME: Final = "_AT"
|
78 | 80 | MAGIC_ATTR_NAME: Final = "__attrs_attrs__"
|
79 | 81 | MAGIC_ATTR_CLS_NAME_TEMPLATE: Final = "__{}_AttrsAttributes__" # The tuple subclass pattern.
|
| 82 | +ATTRS_INIT_NAME: Final = "__attrs_init__" |
80 | 83 |
|
81 | 84 |
|
82 | 85 | class Converter:
|
@@ -330,7 +333,7 @@ def attr_class_maker_callback(
|
330 | 333 |
|
331 | 334 | adder = MethodAdder(ctx)
|
332 | 335 | # If __init__ is not being generated, attrs still generates it as __attrs_init__ instead.
|
333 |
| - _add_init(ctx, attributes, adder, "__init__" if init else "__attrs_init__") |
| 336 | + _add_init(ctx, attributes, adder, "__init__" if init else ATTRS_INIT_NAME) |
334 | 337 | if order:
|
335 | 338 | _add_order(ctx, adder)
|
336 | 339 | if frozen:
|
@@ -888,3 +891,64 @@ def add_method(
|
888 | 891 | """
|
889 | 892 | self_type = self_type if self_type is not None else self.self_type
|
890 | 893 | add_method(self.ctx, method_name, args, ret_type, self_type, tvd)
|
| 894 | + |
| 895 | + |
| 896 | +def _get_attrs_init_type(typ: Type) -> CallableType | None: |
| 897 | + """ |
| 898 | + If `typ` refers to an attrs class, gets the type of its initializer method. |
| 899 | + """ |
| 900 | + typ = get_proper_type(typ) |
| 901 | + if not isinstance(typ, Instance): |
| 902 | + return None |
| 903 | + magic_attr = typ.type.get(MAGIC_ATTR_NAME) |
| 904 | + if magic_attr is None or not magic_attr.plugin_generated: |
| 905 | + return None |
| 906 | + init_method = typ.type.get_method("__init__") or typ.type.get_method(ATTRS_INIT_NAME) |
| 907 | + if not isinstance(init_method, FuncDef) or not isinstance(init_method.type, CallableType): |
| 908 | + return None |
| 909 | + return init_method.type |
| 910 | + |
| 911 | + |
| 912 | +def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> CallableType: |
| 913 | + """ |
| 914 | + Generates a signature for the 'attr.evolve' function that's specific to the call site |
| 915 | + and dependent on the type of the first argument. |
| 916 | + """ |
| 917 | + if len(ctx.args) != 2: |
| 918 | + # Ideally the name and context should be callee's, but we don't have it in FunctionSigContext. |
| 919 | + ctx.api.fail(f'"{ctx.default_signature.name}" has unexpected type annotation', ctx.context) |
| 920 | + return ctx.default_signature |
| 921 | + |
| 922 | + if len(ctx.args[0]) != 1: |
| 923 | + return ctx.default_signature # leave it to the type checker to complain |
| 924 | + |
| 925 | + inst_arg = ctx.args[0][0] |
| 926 | + |
| 927 | + # <hack> |
| 928 | + assert isinstance(ctx.api, TypeChecker) |
| 929 | + inst_type = ctx.api.expr_checker.accept(inst_arg) |
| 930 | + # </hack> |
| 931 | + |
| 932 | + inst_type = get_proper_type(inst_type) |
| 933 | + if isinstance(inst_type, AnyType): |
| 934 | + return ctx.default_signature |
| 935 | + inst_type_str = format_type_bare(inst_type) |
| 936 | + |
| 937 | + attrs_init_type = _get_attrs_init_type(inst_type) |
| 938 | + if not attrs_init_type: |
| 939 | + ctx.api.fail( |
| 940 | + f'Argument 1 to "evolve" has incompatible type "{inst_type_str}"; expected an attrs class', |
| 941 | + ctx.context, |
| 942 | + ) |
| 943 | + return ctx.default_signature |
| 944 | + |
| 945 | + # AttrClass.__init__ has the following signature (or similar, if having kw-only & defaults): |
| 946 | + # def __init__(self, attr1: Type1, attr2: Type2) -> None: |
| 947 | + # We want to generate a signature for evolve that looks like this: |
| 948 | + # def evolve(inst: AttrClass, *, attr1: Type1 = ..., attr2: Type2 = ...) -> AttrClass: |
| 949 | + return attrs_init_type.copy_modified( |
| 950 | + arg_names=["inst"] + attrs_init_type.arg_names[1:], |
| 951 | + arg_kinds=[ARG_POS] + [ARG_NAMED_OPT for _ in attrs_init_type.arg_kinds[1:]], |
| 952 | + ret_type=inst_type, |
| 953 | + name=f"{ctx.default_signature.name} of {inst_type_str}", |
| 954 | + ) |
0 commit comments