aboutsummaryrefslogtreecommitdiff
blob: 031ad630088fb94edf95d5bab5a3c8f8a97449b3 (plain)
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
"""Metaclass to inject dependencies into method calls.

Roughly, if you have 3 methods- that must be ran in the order of start, transfer, finish,
this metaclass enables you to force it such that if *finish* is called first,
start, than transfer, finally finish will be invoked transparently.

Methods involved should all require just ``self``.

The main usage for this code is to enable long chains of steps to be broken
down into individual methods, and the consuming api not being required to
know the proper order of invocation for that api unless they want to.

Most consumers of this metaclass wind up making a ``finish`` method the final step-
via that, consuming api's only requirement is knowing that if they invoke ``finish``
all necessary steps will be ran in the correct order.

Example usage:

>>> from snakeoil.dependant_methods import ForcedDepends
>>> class foo(metaclass=ForcedDepends):
...   stage_depends = {"finish": ("do_step1", "do_step2"),
...     "do_step1":"start", "do_step2": "start"}
...
...   def finish(self):
...     print("finish invoked")
...     return True
...   def do_step1(self):
...     print("running step1")
...     return True
...   def do_step2(self):
...     print("running step2")
...     return True
...   def start(self):
...     print("starting")
...     return True
>>>
>>> obj = foo()
>>> result = obj.finish()
starting
running step1
running step2
finish invoked
>>> result = obj.finish()
>>> # note, no output since finish has already been ran.
"""

from .currying import pre_curry
from .sequences import iflatten_instance

__all__ = ("ForcedDepends",)


def _ensure_deps(cls_id, name, func, self, *a, **kw):
    ignore_deps = kw.pop("ignore_deps", False)
    if id(self.__class__) != cls_id:
        # child class calling parent, or something similar. for cls_id
        # don't fire the dependants, nor update state
        return func(self, *a, **kw)

    if ignore_deps:
        s = [name]
    else:
        s = _yield_deps(self, self.stage_depends, name)

    r = True
    if not hasattr(self, "_stage_state"):
        self._stage_state = set()
    for dep in s:
        if dep not in self._stage_state:
            r = getattr(self, dep).sd_raw_func(self, *a, **kw)
            if not r:
                return r
            self._stage_state.add(dep)
            self.__stage_step_callback__(dep)
    return r


def _yield_deps(inst, d, k):
    # While at first glance this looks like should use expandable_chain,
    # it shouldn't. --charlie
    if k not in d:
        yield k
        return
    s = [k, iflatten_instance(d.get(k, ()))]
    while s:
        if isinstance(s[-1], str):
            yield s.pop(-1)
            continue
        exhausted = True
        for x in s[-1]:
            v = d.get(x)
            if v:
                s.append(x)
                s.append(iflatten_instance(v))
                exhausted = False
                break
            yield x
        if exhausted:
            s.pop(-1)


def __wrap_stage_dependencies__(cls):
    stage_depends = cls.stage_depends
    # we use id instead of the cls itself to prevent strong ref issues.
    cls_id = id(cls)
    for x in set(x for x in iflatten_instance(iter(stage_depends.items())) if x):
        try:
            f = getattr(cls, x)
        except AttributeError:
            raise TypeError(
                "class %r stage_depends specifies %r, which doesn't exist" % (cls, x)
            )
        f2 = pre_curry(_ensure_deps, cls_id, x, f)
        f2.sd_raw_func = f
        setattr(cls, x, f2)


def __unwrap_stage_dependencies__(cls):
    stage_depends = cls.stage_depends
    for x in set(x for x in iflatten_instance(iter(stage_depends.items())) if x):
        try:
            f = getattr(cls, x)
        except AttributeError:
            raise TypeError(
                "class %r stage_depends specifies %r, which doesn't exist" % (cls, x)
            )
        setattr(cls, x, getattr(f, "sd_raw_func", f))


def __set_stage_state__(self, state):
    """set the completed stages to this sequence

    :param state: a sequence of stage names.  The names are not checked
      for validity- you can state that stage *x* has finished when there is no
      stage x.

      this should be used only when you know what you're doing
    """
    self._stage_state = set(state)


def __stage_step_callback__(self, stage):
    """callback invoked whenever a stage is completed with the completed stage name"""


class ForcedDepends(type):
    """
    Metaclass forcing methods to run in a certain order.

    Dependencies are controlled by the existance of a stage_depends
    dict in the class namespace. Its keys are method names, values are
    either a string (name of preceeding method), or list/tuple
    (proceeding methods).

    :cvar stage_depends: mapping of *stage* -> stages that must be ran first
        Dependant stages can either be a string (single stage), or a tuple- multiple stages,
        required in the order the're specified
    :cvar __stage_step_callback__: callback accepting a single arg, the phase that just ran.
        This method/callback is primarily useful for getting raw notification of stages that
        have just completed- whether for notifying a user, or for debugging.
    :cvar __set_stage_state__: method accepting a sequence; the stages completed for this
        instance are set to that sequence.  This should be used with extreme care; primarily
        useful for resuming a sequence of stages midway through.
    """

    def __new__(cls, name, bases, d):
        obj = super(ForcedDepends, cls).__new__(cls, name, bases, d)
        if not hasattr(obj, "stage_depends"):
            obj.stage_depends = {}
        for x in ("wrap", "unwrap"):
            s = "__%s_stage_dependencies__" % x
            if not hasattr(obj, s):
                setattr(obj, s, classmethod(globals()[s]))

        obj.__unwrap_stage_dependencies__()
        obj.__wrap_stage_dependencies__()
        if not hasattr(obj, "__force_stage_state__"):
            obj.__set_stage_state__ = __set_stage_state__
        if not hasattr(obj, "__stage_step_callback__"):
            obj.__stage_step_callback__ = __stage_step_callback__
        return obj