Welcome, guest | Sign In | My Account | Store | Cart

TL;DR: Short way of doing stuff like def __init__(self, a, b): self.a = a; self.b = b, using type annotations from PEP 484, inspired by Dart's MyConstructor(this.a, this.b) and Ruby's/Crystal's/(Moon|Coffee)Script's constructor/initialize(@a, @b).

Python, 63 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import sys


if sys.version_info[0:2] < (3, 5):
    raise ValueError('Python >=3.5 is required!')


# Example usage:

# from auto_assign import *
# class Xyz:
#     @auto_assign
#     def __init__(self, x: AutoAssign[int]) -> None:
#         pass

# xyz = Xyz(1)
# print(xyz.x)  # 1

# (See the description for more details...)


from typing import Any, Callable, TypeVar, Union, cast
import inspect


class _AutoAssignMagic: pass
_T = TypeVar('_T')
AutoAssign = Union[_AutoAssignMagic, _T]


_F = TypeVar('_F', bound=Callable[..., Any])


def auto_assign(func: _F) -> _F:
    def _wrap(*args, **kw):
        if not args:
            # Where is self? Let the function figure it out!
            func(*args, **kw)

        self = args[0]

        sig = inspect.signature(func)
        auto_args = set()

        for param in sig.parameters.values():
            # XXX: Hacky way of checking for AutoAssign.
            if hasattr(param.annotation, '__origin__') and \
               param.annotation.__origin__ is AutoAssign:
                auto_args.add(param.name)

        try:
            bound = sig.bind(*args, **kw)
        except TypeError:
            # Just let it propagate down to the function itself.
            pass
        else:
            bound.apply_defaults()
            for auto_arg in auto_args:
                setattr(self, auto_arg, bound.arguments[auto_arg])

        func(*args, **kw)

    return cast(_F, _wrap)

Highlights:

  • Plays nicely with PEP 484.
  • Dead-easy to use.
  • Looks pretty!!

For instance:

class MyClass:
    myattr1: int  # to appease type checkers!

    # The auto_assign decorator will pick out all the
    # AutoAssign annotations and assume those are what
    # should be assigned to `self`.

    # Note that, thanks to some Union magic, this will still
    # type-check correctly. e.g. Mypy won't flag MyClass(1)
    # or similar because of type errors.

    # This is inspired a bit by both Dart and Crystal. The
    # __init__(self, myattr1: AutoAssign[int]) corresponds to
    # MyClass(this.myattr) or initialize(@myattr1 : Int32).
    @auto_assign
    def __init__(self, myattr1: AutoAssign[int]):
        print(self.myattr1)

MyClass(123)

1 comment

Matthias Rahlf 4 years, 6 months ago  # | flag

There should be a return after line 38 or else args[0] on line 40 will raise an exception.