"""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