Continue work on decorator usage.

Remove the need for wrappers.py now that there is an advanced
decorator provided that can provide all the same features as
the functor wrapper task class. This new decorator is now used
in the tests instead of the previous usage of the functor task
class.
This commit is contained in:
Joshua Harlow
2013-05-29 21:15:44 -07:00
parent b5901de704
commit bc4859c634
9 changed files with 154 additions and 146 deletions

View File

@@ -47,16 +47,78 @@ def wraps(fn):
return wrapper return wrapper
def task(*args, **kwargs):
"""Decorates a given function and ensures that all needed attributes of
that function are set so that the function can be used as a task."""
def decorator(f):
def noop(*args, **kwargs):
pass
f.revert = kwargs.pop('revert_with', noop)
# Sets the version of the task.
version = kwargs.pop('version', (1, 0))
f = versionize(*version)(f)
# Attach any requirements this function needs for running.
requires_what = kwargs.pop('requires', [])
f = requires(*requires_what, **kwargs)(f)
# Attach any items this function provides as output
provides_what = kwargs.pop('provides', [])
f = provides(*provides_what, **kwargs)(f)
# Associate a name of this task that is the module + function name.
f.name = "%s.%s" % (f.__module__, f.__name__)
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
# This is needed to handle when the decorator has args or the decorator
# doesn't have args, python is rather weird here...
if kwargs or not args:
return decorator
else:
if isinstance(args[0], collections.Callable):
return decorator(args[0])
else:
return decorator
def versionize(major, minor=None):
"""A decorator that marks the wrapped function with a major & minor version
number."""
if minor is None:
minor = 0
def decorator(f):
f.__version__ = (major, minor)
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
return decorator
def requires(*args, **kwargs): def requires(*args, **kwargs):
"""Attaches a set of items that the decorated function requires as input
to the functions underlying dictionary."""
def decorator(f): def decorator(f):
if not hasattr(f, 'requires'): if not hasattr(f, 'requires'):
f.requires = set() f.requires = set()
if kwargs.pop('auto_extract', True): if kwargs.pop('auto_extract', True):
inspect_what = f inspect_what = getattr(f, '__wrapped__', f)
if hasattr(f, '__wrapped__'):
inspect_what = f.__wrapped__
f_args = inspect.getargspec(inspect_what).args f_args = inspect.getargspec(inspect_what).args
f.requires.update([a for a in f_args if _take_arg(a)]) f.requires.update([a for a in f_args if _take_arg(a)])
@@ -80,6 +142,8 @@ def requires(*args, **kwargs):
def provides(*args, **kwargs): def provides(*args, **kwargs):
"""Attaches a set of items that the decorated function provides as output
to the functions underlying dictionary."""
def decorator(f): def decorator(f):
if not hasattr(f, 'provides'): if not hasattr(f, 'provides'):

View File

@@ -17,6 +17,7 @@
# under the License. # under the License.
import abc import abc
import types
from taskflow import exceptions as exc from taskflow import exceptions as exc
from taskflow import states from taskflow import states
@@ -26,28 +27,29 @@ from taskflow.openstack.common import uuidutils
def task_and_state(task, state): def task_and_state(task, state):
name_pieces = [] """Combines a task objects string representation with a state to
try: create a uniquely identifying task+state name."""
name_pieces.append(task.name)
if isinstance(task.version, (list, tuple)): task_name = ""
name_pieces.append(utils.join(task.version, ".")) if isinstance(task, types.FunctionType):
# If its a function look for the attributes that should have been
# set using the task() decorator provided in the decorators file. If
# those have not been set, then we should at least have enough basic
# information (not a version) to form a useful task name.
if hasattr(task, 'name'):
task_name = str(task.name)
else: else:
name_pieces.append(task.version) name_pieces = [a for a in utils.get_many_attr(task,
except AttributeError: '__module__',
pass '__name__')
if not name_pieces: if a is not None]
# Likely a function and not a task object so let us search for these task_name = utils.join(name_pieces, ".")
# attributes to get a good name for this task. version_pieces = utils.get_many_attr(task, '__version__')
name_pieces = [a for a in utils.get_many_attr(task, if version_pieces and version_pieces[0]:
'__module__', task_name += "==" + utils.join(version_pieces[0], with_what=".")
'__name__', else:
'__version__') task_name = str(task)
if a is not None] return "%s;%s" % (task_name, state)
if not name_pieces:
# Ok, unsure what this task is, just use whatever its string
# representation is.
name_pieces.append(task)
return "%s;%s" % (utils.join(name_pieces, ':'), state)
class Claimer(object): class Claimer(object):

View File

@@ -41,8 +41,7 @@ class Task(object):
self.version = (1, 0) self.version = (1, 0)
def __str__(self): def __str__(self):
return "Task: %s v%s" % (self.name, utils.join(self.version, return "%s==%s" % (self.name, utils.join(self.version, with_what="."))
with_what="."))
@abc.abstractmethod @abc.abstractmethod
def __call__(self, context, *args, **kwargs): def __call__(self, context, *args, **kwargs):

View File

@@ -19,9 +19,9 @@
import collections import collections
import unittest import unittest
from taskflow import decorators
from taskflow import exceptions as excp from taskflow import exceptions as excp
from taskflow import states from taskflow import states
from taskflow import wrappers
from taskflow.patterns import graph_flow as gw from taskflow.patterns import graph_flow as gw
from taskflow.tests import utils from taskflow.tests import utils
@@ -32,25 +32,23 @@ class GraphFlowTest(unittest.TestCase):
flo = gw.Flow("test-flow") flo = gw.Flow("test-flow")
reverted = [] reverted = []
def run1(context): # pylint: disable=W0613
return {
'a': 1,
}
def run1_revert(context, result, cause): # pylint: disable=W0613 def run1_revert(context, result, cause): # pylint: disable=W0613
reverted.append('run1') reverted.append('run1')
self.assertEquals(states.REVERTING, cause.flow.state) self.assertEquals(states.REVERTING, cause.flow.state)
self.assertEquals(result, {'a': 1}) self.assertEquals(result, {'a': 1})
@decorators.task(revert_with=run1_revert, provides=['a'])
def run1(context): # pylint: disable=W0613
return {
'a': 1,
}
@decorators.task(provides=['c'])
def run2(context, a): # pylint: disable=W0613,C0103 def run2(context, a): # pylint: disable=W0613,C0103
raise Exception('Dead') raise Exception('Dead')
flo.add(wrappers.FunctorTask(None, run1, run1_revert, flo.add(run1)
provides_what=['a'], flo.add(run2)
extract_requires=True))
flo.add(wrappers.FunctorTask(None, run2, utils.null_functor,
provides_what=['c'],
extract_requires=True))
self.assertEquals(states.PENDING, flo.state) self.assertEquals(states.PENDING, flo.state)
self.assertRaises(Exception, flo.run, {}) self.assertRaises(Exception, flo.run, {})
@@ -147,20 +145,19 @@ class GraphFlowTest(unittest.TestCase):
def test_connect_requirement_failure(self): def test_connect_requirement_failure(self):
@decorators.task(provides=['a'])
def run1(context): # pylint: disable=W0613 def run1(context): # pylint: disable=W0613
return { return {
'a': 1, 'a': 1,
} }
@decorators.task
def run2(context, b, c, d): # pylint: disable=W0613,C0103 def run2(context, b, c, d): # pylint: disable=W0613,C0103
return None return None
flo = gw.Flow("test-flow") flo = gw.Flow("test-flow")
flo.add(wrappers.FunctorTask(None, run1, utils.null_functor, flo.add(run1)
provides_what=['a'], flo.add(run2)
extract_requires=True))
flo.add(wrappers.FunctorTask(None, run2, utils.null_functor,
extract_requires=True))
self.assertRaises(excp.InvalidStateException, flo.connect) self.assertRaises(excp.InvalidStateException, flo.connect)
self.assertRaises(excp.InvalidStateException, flo.run, {}) self.assertRaises(excp.InvalidStateException, flo.run, {})
@@ -172,40 +169,37 @@ class GraphFlowTest(unittest.TestCase):
run_order = [] run_order = []
f_args = {} f_args = {}
@decorators.task(provides=['a'])
def run1(context): # pylint: disable=W0613,C0103 def run1(context): # pylint: disable=W0613,C0103
run_order.append('ran1') run_order.append('ran1')
return { return {
'a': 1, 'a': 1,
} }
@decorators.task(provides=['c'])
def run2(context, a): # pylint: disable=W0613,C0103 def run2(context, a): # pylint: disable=W0613,C0103
run_order.append('ran2') run_order.append('ran2')
return { return {
'c': 3, 'c': 3,
} }
@decorators.task(provides=['b'])
def run3(context, a): # pylint: disable=W0613,C0103 def run3(context, a): # pylint: disable=W0613,C0103
run_order.append('ran3') run_order.append('ran3')
return { return {
'b': 2, 'b': 2,
} }
@decorators.task
def run4(context, b, c): # pylint: disable=W0613,C0103 def run4(context, b, c): # pylint: disable=W0613,C0103
run_order.append('ran4') run_order.append('ran4')
f_args['b'] = b f_args['b'] = b
f_args['c'] = c f_args['c'] = c
flo.add(wrappers.FunctorTask(None, run1, utils.null_functor, flo.add(run1)
provides_what=['a'], flo.add(run2)
extract_requires=True)) flo.add(run3)
flo.add(wrappers.FunctorTask(None, run2, utils.null_functor, flo.add(run4)
provides_what=['c'],
extract_requires=True))
flo.add(wrappers.FunctorTask(None, run3, utils.null_functor,
provides_what=['b'],
extract_requires=True))
flo.add(wrappers.FunctorTask(None, run4, utils.null_functor,
extract_requires=True))
flo.run({}) flo.run({})
self.assertEquals(['ran1', 'ran2', 'ran3', 'ran4'], sorted(run_order)) self.assertEquals(['ran1', 'ran2', 'ran3', 'ran4'], sorted(run_order))

View File

@@ -22,7 +22,6 @@ import unittest
from taskflow import decorators from taskflow import decorators
from taskflow import exceptions as exc from taskflow import exceptions as exc
from taskflow import states from taskflow import states
from taskflow import wrappers
from taskflow.patterns import linear_flow as lw from taskflow.patterns import linear_flow as lw
from taskflow.tests import utils from taskflow.tests import utils
@@ -31,32 +30,33 @@ from taskflow.tests import utils
class LinearFlowTest(unittest.TestCase): class LinearFlowTest(unittest.TestCase):
def make_reverting_task(self, token, blowup=False): def make_reverting_task(self, token, blowup=False):
def do_apply(token, context, *_args, **_kwargs): def do_revert(context, *args, **kwargs):
context[token] = 'passed'
def do_revert(token, context, *_args, **_kwargs):
context[token] = 'reverted' context[token] = 'reverted'
def blow_up(_context, *_args, **_kwargs): @decorators.task(revert_with=do_revert)
def do_apply(context, *args, **kwargs):
context[token] = 'passed'
@decorators.task
def blow_up(context, *args, **kwargs):
raise Exception("I blew up") raise Exception("I blew up")
if blowup: if blowup:
return wrappers.FunctorTask('task-%s' % (token), # Alter the task name so that its unique by including the token.
functools.partial(blow_up, token), blow_up.name += str(token)
utils.null_functor) return blow_up
else: else:
return wrappers.FunctorTask('task-%s' % (token), # Alter the task name so that its unique by including the token.
functools.partial(do_apply, token), do_apply.name += str(token)
functools.partial(do_revert, token)) return do_apply
def make_interrupt_task(self, token, wf): def make_interrupt_task(self, token, wf):
def do_interrupt(_context, *_args, **_kwargs): @decorators.task
def do_interrupt(context, *args, **kwargs):
wf.interrupt() wf.interrupt()
return wrappers.FunctorTask('task-%s' % (token), return do_interrupt
do_interrupt,
utils.null_functor)
def test_functor_flow(self): def test_functor_flow(self):
wf = lw.Flow("the-test-action") wf = lw.Flow("the-test-action")
@@ -148,31 +148,27 @@ class LinearFlowTest(unittest.TestCase):
def test_not_satisfied_inputs_previous(self): def test_not_satisfied_inputs_previous(self):
wf = lw.Flow("the-test-action") wf = lw.Flow("the-test-action")
@decorators.task
def task_a(context, *args, **kwargs): def task_a(context, *args, **kwargs):
pass pass
@decorators.task
def task_b(context, c, *args, **kwargs): def task_b(context, c, *args, **kwargs):
pass pass
wf.add(wrappers.FunctorTask(None, task_a, utils.null_functor, wf.add(task_a)
extract_requires=True))
self.assertRaises(exc.InvalidStateException, self.assertRaises(exc.InvalidStateException,
wf.add, wf.add, task_b)
wrappers.FunctorTask(None, task_b,
utils.null_functor,
extract_requires=True))
def test_not_satisfied_inputs_no_previous(self): def test_not_satisfied_inputs_no_previous(self):
wf = lw.Flow("the-test-action") wf = lw.Flow("the-test-action")
@decorators.task
def task_a(context, c, *args, **kwargs): def task_a(context, c, *args, **kwargs):
pass pass
self.assertRaises(exc.InvalidStateException, self.assertRaises(exc.InvalidStateException,
wf.add, wf.add, task_a)
wrappers.FunctorTask(None, task_a,
utils.null_functor,
extract_requires=True))
def test_flow_add_order(self): def test_flow_add_order(self):
wf = lw.Flow("the-test-action") wf = lw.Flow("the-test-action")

View File

@@ -22,10 +22,10 @@ import functools
import threading import threading
import unittest import unittest
from taskflow import decorators
from taskflow import exceptions as exc from taskflow import exceptions as exc
from taskflow import job from taskflow import job
from taskflow import states from taskflow import states
from taskflow import wrappers as wrap
from taskflow.backends import memory from taskflow.backends import memory
from taskflow.patterns import linear_flow as lw from taskflow.patterns import linear_flow as lw
@@ -79,10 +79,7 @@ class MemoryBackendTest(unittest.TestCase):
# Create some dummy flow for the job # Create some dummy flow for the job
wf = lw.Flow('dummy') wf = lw.Flow('dummy')
for _i in range(0, 5): for _i in range(0, 5):
t = wrap.FunctorTask(None, wf.add(utils.null_functor)
utils.null_functor,
utils.null_functor)
wf.add(t)
j.associate(wf) j.associate(wf)
j.state = states.RUNNING j.state = states.RUNNING
wf.run(j.context) wf.run(j.context)
@@ -131,18 +128,21 @@ class MemoryBackendTest(unittest.TestCase):
call_log = [] call_log = []
def do_1(_context, *_args, **_kwargs): @decorators.task
def do_1(context, *args, **kwargs):
call_log.append(1) call_log.append(1)
def do_2(_context, *_args, **_kwargs): @decorators.task
def do_2(context, *args, **kwargs):
call_log.append(2) call_log.append(2)
def do_interrupt(_context, *_args, **_kwargs): @decorators.task
def do_interrupt(context, *args, **kwargs):
wf.interrupt() wf.interrupt()
task_1 = wrap.FunctorTask(None, do_1, utils.null_functor) task_1 = do_1
task_1_5 = wrap.FunctorTask(None, do_interrupt, utils.null_functor) task_1_5 = do_interrupt
task_2 = wrap.FunctorTask(None, do_2, utils.null_functor) task_2 = do_2
wf.add(task_1) wf.add(task_1)
wf.add(task_1_5) # Interrupt it after task_1 finishes wf.add(task_1_5) # Interrupt it after task_1 finishes
@@ -180,14 +180,16 @@ class MemoryBackendTest(unittest.TestCase):
call_log = [] call_log = []
def do_1(_context, *_args, **_kwargs): @decorators.task
def do_1(context, *args, **kwargs):
call_log.append(1) call_log.append(1)
def do_2(_context, *_args, **_kwargs): @decorators.task
def do_2(context, *args, **kwargs):
call_log.append(2) call_log.append(2)
wf.add(wrap.FunctorTask(None, do_1, utils.null_functor)) wf.add(do_1)
wf.add(wrap.FunctorTask(None, do_2, utils.null_functor)) wf.add(do_2)
wf.run(j.context) wf.run(j.context)
self.assertEquals(1, len(j.logbook)) self.assertEquals(1, len(j.logbook))

View File

@@ -37,8 +37,8 @@ def null_functor(*args, **kwargs): # pylint: disable=W0613
class ProvidesRequiresTask(task.Task): class ProvidesRequiresTask(task.Task):
def __init__(self, name, provides, requires): def __init__(self, name, provides, requires):
super(ProvidesRequiresTask, self).__init__(name) super(ProvidesRequiresTask, self).__init__(name)
self.provides = provides self.provides.update(provides)
self.requires = requires self.requires.update(requires)
def __call__(self, context, *args, **kwargs): def __call__(self, context, *args, **kwargs):
outs = { outs = {

View File

@@ -195,4 +195,3 @@ class LazyPluggable(object):
def __getattr__(self, key): def __getattr__(self, key):
backend = self.__get_backend() backend = self.__get_backend()
return getattr(backend, key) return getattr(backend, key)

View File

@@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from taskflow import decorators
from taskflow import task
class FunctorTask(task.Task):
"""A simple task that can wrap two given functions and allow them to be
in combination used in apply and reverting a given task. Useful for
situations where existing functions already are in place and you just want
to wrap them up."""
def __init__(self, name, apply_functor, revert_functor=None,
provides_what=None, extract_requires=False):
if not name:
name = "_".join([apply_functor.__name__, revert_functor.__name__])
super(FunctorTask, self).__init__(name)
if extract_requires:
self._apply_functor = decorators.requires(apply_functor)
self.requires.update(self._apply_functor.requires)
else:
self._apply_functor = apply_functor
self._revert_functor = revert_functor
if provides_what:
self.provides.update(provides_what)
def __call__(self, context, *args, **kwargs):
return self._apply_functor(context, *args, **kwargs)
def revert(self, context, result, cause):
if self._revert_functor:
self._revert_functor(context, result, cause)