From 30cf71a80962f90789147384519565cd9eadacf1 Mon Sep 17 00:00:00 2001 From: "Ivan A. Melnikov" Date: Thu, 22 Aug 2013 11:45:01 +0400 Subject: [PATCH] Nicer way to make task out of any callable This commit introduces taskflow.functor_task.FunctorTask class, which is adapter that can be used to make a task from any callable. Dependencies, revert callable, name and version can be specified at the moment of construction. Partially implements blueprint refactor-decorators Change-Id: If92de20a67ea6c228abb0a78edaa837b98581646 --- taskflow/functor_task.py | 79 ++++++++++++++++++++++++ taskflow/tests/unit/test_functor_task.py | 67 ++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 taskflow/functor_task.py create mode 100644 taskflow/tests/unit/test_functor_task.py diff --git a/taskflow/functor_task.py b/taskflow/functor_task.py new file mode 100644 index 00000000..28e0dc1a --- /dev/null +++ b/taskflow/functor_task.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2012-2013 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. + +import inspect +from taskflow import task as base + +# These arguments are ones that we will skip when parsing for requirements +# for a function to operate (when used as a task). +AUTO_ARGS = ('self', 'context', 'cls') + + +def _take_arg(a): + if a in AUTO_ARGS: + return False + # In certain decorator cases it seems like we get the function to be + # decorated as an argument, we don't want to take that as a real argument. + if not isinstance(a, basestring): + return False + return True + + +class FunctorTask(base.Task): + """Adaptor to make task from a callable + + Take any callable and make a task from it. + """ + @staticmethod + def _callable_name(function): + """Generate a name from callable""" + im_class = getattr(function, 'im_class', None) + if im_class is not None: + parts = (im_class.__module__, im_class.__name__, function.__name__) + else: + parts = (function.__module__, function.__name__) + return '.'.join(parts) + + def __init__(self, execute_with, **kwargs): + name = kwargs.pop('name', None) + if name is None: + name = self._callable_name(execute_with) + super(FunctorTask, self).__init__(name, kwargs.pop('task_id', None)) + self._execute_with = execute_with + self._revert_with = kwargs.pop('revert_with', None) + self.version = kwargs.pop('version', self.version) + + self.requires.update(kwargs.pop('requires', ())) + if kwargs.pop('auto_extract', True): + f_args = inspect.getargspec(execute_with).args + self.requires.update([a for a in f_args if _take_arg(a)]) + + self.optional.update(kwargs.pop('optional', ())) + self.provides.update(kwargs.pop('provides', ())) + if kwargs: + raise TypeError('__init__() got an unexpected keyword argument %r' + % kwargs.keys[0]) + + def __call__(self, *args, **kwargs): + return self._execute_with(*args, **kwargs) + + def revert(self, *args, **kwargs): + if self._revert_with: + return self._revert_with(*args, **kwargs) + else: + return None diff --git a/taskflow/tests/unit/test_functor_task.py b/taskflow/tests/unit/test_functor_task.py new file mode 100644 index 00000000..e9be3468 --- /dev/null +++ b/taskflow/tests/unit/test_functor_task.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (C) 2012-2013 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. + +import unittest2 + +from taskflow import functor_task +from taskflow.patterns import linear_flow + + +def add(a, b): + return a + b + + +class BunchOfFunctions(object): + + def __init__(self, values): + self.values = values + + def run_one(self, *args, **kwargs): + self.values.append('one') + + def revert_one(self, *args, **kwargs): + self.values.append('revert one') + + def run_fail(self, *args, **kwargs): + self.values.append('fail') + raise RuntimeError('Woot!') + + +class FunctorTaskTest(unittest2.TestCase): + + def test_simple(self): + task = functor_task.FunctorTask(add) + self.assertEquals(task.name, __name__ + '.add') + + def test_other_name(self): + task = functor_task.FunctorTask(add, name='my task') + self.assertEquals(task.name, 'my task') + + def test_it_runs(self): + values = [] + bof = BunchOfFunctions(values) + t = functor_task.FunctorTask + + flow = linear_flow.Flow('test') + flow.add_many(( + t(bof.run_one, revert_with=bof.revert_one), + t(bof.run_fail) + )) + with self.assertRaisesRegexp(RuntimeError, '^Woot'): + flow.run(None) + self.assertEquals(values, ['one', 'fail', 'revert one'])