Merge "Allow instance methods to be wrapped and unwrapped correctly."
This commit is contained in:
@@ -19,14 +19,61 @@
|
|||||||
import collections
|
import collections
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
import types
|
||||||
from taskflow import utils
|
|
||||||
|
|
||||||
# These arguments are ones that we will skip when parsing for requirements
|
# These arguments are ones that we will skip when parsing for requirements
|
||||||
# for a function to operate (when used as a task).
|
# for a function to operate (when used as a task).
|
||||||
AUTO_ARGS = ('self', 'context', 'cls')
|
AUTO_ARGS = ('self', 'context', 'cls')
|
||||||
|
|
||||||
|
|
||||||
|
def is_decorated(functor):
|
||||||
|
if not isinstance(functor, (types.MethodType, types.FunctionType)):
|
||||||
|
return False
|
||||||
|
return getattr(extract(functor), '__task__', False)
|
||||||
|
|
||||||
|
|
||||||
|
def extract(functor):
|
||||||
|
# Extract the underlying functor if its a method since we can not set
|
||||||
|
# attributes on instance methods, this is supposedly fixed in python 3
|
||||||
|
# and later.
|
||||||
|
#
|
||||||
|
# TODO(harlowja): add link to this fix.
|
||||||
|
assert isinstance(functor, (types.MethodType, types.FunctionType))
|
||||||
|
if isinstance(functor, types.MethodType):
|
||||||
|
return functor.__func__
|
||||||
|
else:
|
||||||
|
return functor
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_as_task(functor):
|
||||||
|
setattr(functor, '__task__', True)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_wrapped(function):
|
||||||
|
"""Get the method at the bottom of a stack of decorators."""
|
||||||
|
|
||||||
|
if hasattr(function, '__wrapped__'):
|
||||||
|
return getattr(function, '__wrapped__')
|
||||||
|
|
||||||
|
if not hasattr(function, 'func_closure') or not function.func_closure:
|
||||||
|
return function
|
||||||
|
|
||||||
|
def _get_wrapped_function(function):
|
||||||
|
if not hasattr(function, 'func_closure') or not function.func_closure:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for closure in function.func_closure:
|
||||||
|
func = closure.cell_contents
|
||||||
|
|
||||||
|
deeper_func = _get_wrapped_function(func)
|
||||||
|
if deeper_func:
|
||||||
|
return deeper_func
|
||||||
|
elif hasattr(closure.cell_contents, '__call__'):
|
||||||
|
return closure.cell_contents
|
||||||
|
|
||||||
|
return _get_wrapped_function(function)
|
||||||
|
|
||||||
|
|
||||||
def _take_arg(a):
|
def _take_arg(a):
|
||||||
if a in AUTO_ARGS:
|
if a in AUTO_ARGS:
|
||||||
return False
|
return False
|
||||||
@@ -54,30 +101,35 @@ def task(*args, **kwargs):
|
|||||||
that function are set so that the function can be used as a task."""
|
that function are set so that the function can be used as a task."""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
|
w_f = extract(f)
|
||||||
|
|
||||||
def noop(*args, **kwargs):
|
def noop(*args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
f.revert = kwargs.pop('revert_with', noop)
|
# Mark as being a task
|
||||||
|
_mark_as_task(w_f)
|
||||||
|
|
||||||
|
# By default don't revert this.
|
||||||
|
w_f.revert = kwargs.pop('revert_with', noop)
|
||||||
|
|
||||||
|
# Associate a name of this task that is the module + function name.
|
||||||
|
w_f.name = "%s.%s" % (f.__module__, f.__name__)
|
||||||
|
|
||||||
# Sets the version of the task.
|
# Sets the version of the task.
|
||||||
version = kwargs.pop('version', (1, 0))
|
version = kwargs.pop('version', (1, 0))
|
||||||
f = versionize(*version)(f)
|
f = _versionize(*version)(f)
|
||||||
|
|
||||||
# Attach any requirements this function needs for running.
|
# Attach any requirements this function needs for running.
|
||||||
requires_what = kwargs.pop('requires', [])
|
requires_what = kwargs.pop('requires', [])
|
||||||
f = requires(*requires_what, **kwargs)(f)
|
f = _requires(*requires_what, **kwargs)(f)
|
||||||
|
|
||||||
# Attach any optional requirements this function needs for running.
|
# Attach any optional requirements this function needs for running.
|
||||||
optional_what = kwargs.pop('optional', [])
|
optional_what = kwargs.pop('optional', [])
|
||||||
f = optional(*optional_what, **kwargs)(f)
|
f = _optional(*optional_what, **kwargs)(f)
|
||||||
|
|
||||||
# Attach any items this function provides as output
|
# Attach any items this function provides as output
|
||||||
provides_what = kwargs.pop('provides', [])
|
provides_what = kwargs.pop('provides', [])
|
||||||
f = provides(*provides_what, **kwargs)(f)
|
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)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
@@ -96,7 +148,7 @@ def task(*args, **kwargs):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def versionize(major, minor=None):
|
def _versionize(major, minor=None):
|
||||||
"""A decorator that marks the wrapped function with a major & minor version
|
"""A decorator that marks the wrapped function with a major & minor version
|
||||||
number."""
|
number."""
|
||||||
|
|
||||||
@@ -104,7 +156,8 @@ def versionize(major, minor=None):
|
|||||||
minor = 0
|
minor = 0
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
f.__version__ = (major, minor)
|
w_f = extract(f)
|
||||||
|
w_f.version = (major, minor)
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
@@ -115,15 +168,17 @@ def versionize(major, minor=None):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def optional(*args, **kwargs):
|
def _optional(*args, **kwargs):
|
||||||
"""Attaches a set of items that the decorated function would like as input
|
"""Attaches a set of items that the decorated function would like as input
|
||||||
to the functions underlying dictionary."""
|
to the functions underlying dictionary."""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
if not hasattr(f, 'optional'):
|
w_f = extract(f)
|
||||||
f.optional = set()
|
|
||||||
|
|
||||||
f.optional.update([a for a in args if _take_arg(a)])
|
if not hasattr(w_f, 'optional'):
|
||||||
|
w_f.optional = set()
|
||||||
|
|
||||||
|
w_f.optional.update([a for a in args if _take_arg(a)])
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
@@ -142,24 +197,22 @@ def optional(*args, **kwargs):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def requires(*args, **kwargs):
|
def _requires(*args, **kwargs):
|
||||||
"""Attaches a set of items that the decorated function requires as input
|
"""Attaches a set of items that the decorated function requires as input
|
||||||
to the functions underlying dictionary."""
|
to the functions underlying dictionary."""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
if not hasattr(f, 'requires'):
|
w_f = extract(f)
|
||||||
f.requires = set()
|
|
||||||
|
if not hasattr(w_f, 'requires'):
|
||||||
|
w_f.requires = set()
|
||||||
|
|
||||||
if kwargs.pop('auto_extract', True):
|
if kwargs.pop('auto_extract', True):
|
||||||
inspect_what = getattr(f, '__wrapped__', None)
|
inspect_what = _get_wrapped(f)
|
||||||
|
|
||||||
if not inspect_what:
|
|
||||||
inspect_what = utils.get_wrapped_function(f)
|
|
||||||
|
|
||||||
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)])
|
w_f.requires.update([a for a in f_args if _take_arg(a)])
|
||||||
|
|
||||||
f.requires.update([a for a in args if _take_arg(a)])
|
w_f.requires.update([a for a in args if _take_arg(a)])
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
@@ -178,15 +231,17 @@ def requires(*args, **kwargs):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def provides(*args, **kwargs):
|
def _provides(*args, **kwargs):
|
||||||
"""Attaches a set of items that the decorated function provides as output
|
"""Attaches a set of items that the decorated function provides as output
|
||||||
to the functions underlying dictionary."""
|
to the functions underlying dictionary."""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
if not hasattr(f, 'provides'):
|
w_f = extract(f)
|
||||||
f.provides = set()
|
|
||||||
|
|
||||||
f.provides.update([a for a in args if _take_arg(a)])
|
if not hasattr(f, 'provides'):
|
||||||
|
w_f.provides = set()
|
||||||
|
|
||||||
|
w_f.provides.update([a for a in args if _take_arg(a)])
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
|
|||||||
@@ -32,14 +32,10 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def _get_task_version(task):
|
def _get_task_version(task):
|
||||||
"""Gets a tasks *string* version, whether it is a task object/function."""
|
"""Gets a tasks *string* version, whether it is a task object/function."""
|
||||||
task_version = ''
|
task_version = utils.get_attr(task, 'version')
|
||||||
if isinstance(task, types.FunctionType):
|
|
||||||
task_version = getattr(task, '__version__', '')
|
|
||||||
if not task_version and hasattr(task, 'version'):
|
|
||||||
task_version = task.version
|
|
||||||
if isinstance(task_version, (list, tuple)):
|
if isinstance(task_version, (list, tuple)):
|
||||||
task_version = utils.join(task_version, with_what=".")
|
task_version = utils.join(task_version, with_what=".")
|
||||||
if not isinstance(task_version, basestring):
|
if task_version is not None and not isinstance(task_version, basestring):
|
||||||
task_version = str(task_version)
|
task_version = str(task_version)
|
||||||
return task_version
|
return task_version
|
||||||
|
|
||||||
@@ -47,14 +43,13 @@ def _get_task_version(task):
|
|||||||
def _get_task_name(task):
|
def _get_task_name(task):
|
||||||
"""Gets a tasks *string* name, whether it is a task object/function."""
|
"""Gets a tasks *string* name, whether it is a task object/function."""
|
||||||
task_name = ""
|
task_name = ""
|
||||||
if isinstance(task, types.FunctionType):
|
if isinstance(task, (types.MethodType, types.FunctionType)):
|
||||||
# If its a function look for the attributes that should have been
|
# If its a function look for the attributes that should have been
|
||||||
# set using the task() decorator provided in the decorators file. If
|
# set using the task() decorator provided in the decorators file. If
|
||||||
# those have not been set, then we should at least have enough basic
|
# those have not been set, then we should at least have enough basic
|
||||||
# information (not a version) to form a useful task name.
|
# information (not a version) to form a useful task name.
|
||||||
if hasattr(task, 'name'):
|
task_name = utils.get_attr(task, 'name')
|
||||||
task_name = str(task.name)
|
if not task_name:
|
||||||
else:
|
|
||||||
name_pieces = [a for a in utils.get_many_attr(task,
|
name_pieces = [a for a in utils.get_many_attr(task,
|
||||||
'__module__',
|
'__module__',
|
||||||
'__name__')
|
'__name__')
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from networkx import exception as g_exc
|
|||||||
|
|
||||||
from taskflow import exceptions as exc
|
from taskflow import exceptions as exc
|
||||||
from taskflow.patterns import ordered_flow
|
from taskflow.patterns import ordered_flow
|
||||||
|
from taskflow import utils
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class Flow(ordered_flow.Flow):
|
|||||||
#
|
#
|
||||||
# Only insert the node to start, connect all the edges
|
# Only insert the node to start, connect all the edges
|
||||||
# together later after all nodes have been added.
|
# together later after all nodes have been added.
|
||||||
|
assert isinstance(task, collections.Callable)
|
||||||
self._graph.add_node(task)
|
self._graph.add_node(task)
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
||||||
@@ -54,7 +56,8 @@ class Flow(ordered_flow.Flow):
|
|||||||
def extract_inputs(place_where, would_like, is_optional=False):
|
def extract_inputs(place_where, would_like, is_optional=False):
|
||||||
for n in would_like:
|
for n in would_like:
|
||||||
for (them, there_result) in self.results:
|
for (them, there_result) in self.results:
|
||||||
if not n in set(getattr(them, 'provides', [])):
|
they_provide = utils.get_attr(them, 'provides', [])
|
||||||
|
if n not in set(they_provide):
|
||||||
continue
|
continue
|
||||||
if ((not is_optional and
|
if ((not is_optional and
|
||||||
not self._graph.has_edge(them, task))):
|
not self._graph.has_edge(them, task))):
|
||||||
@@ -68,8 +71,8 @@ class Flow(ordered_flow.Flow):
|
|||||||
elif not is_optional:
|
elif not is_optional:
|
||||||
place_where[n].append(None)
|
place_where[n].append(None)
|
||||||
|
|
||||||
required_inputs = set(getattr(task, 'requires', []))
|
required_inputs = set(utils.get_attr(task, 'requires', []))
|
||||||
optional_inputs = set(getattr(task, 'optional', []))
|
optional_inputs = set(utils.get_attr(task, 'optional', []))
|
||||||
optional_inputs = optional_inputs - required_inputs
|
optional_inputs = optional_inputs - required_inputs
|
||||||
|
|
||||||
task_inputs = collections.defaultdict(list)
|
task_inputs = collections.defaultdict(list)
|
||||||
@@ -103,9 +106,9 @@ class Flow(ordered_flow.Flow):
|
|||||||
provides_what = collections.defaultdict(list)
|
provides_what = collections.defaultdict(list)
|
||||||
requires_what = collections.defaultdict(list)
|
requires_what = collections.defaultdict(list)
|
||||||
for t in self._graph.nodes_iter():
|
for t in self._graph.nodes_iter():
|
||||||
for r in getattr(t, 'requires', []):
|
for r in utils.get_attr(t, 'requires', []):
|
||||||
requires_what[r].append(t)
|
requires_what[r].append(t)
|
||||||
for p in getattr(t, 'provides', []):
|
for p in utils.get_attr(t, 'provides', []):
|
||||||
provides_what[p].append(t)
|
provides_what[p].append(t)
|
||||||
|
|
||||||
def get_providers(node, want_what):
|
def get_providers(node, want_what):
|
||||||
|
|||||||
@@ -16,8 +16,11 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
from taskflow import exceptions as exc
|
from taskflow import exceptions as exc
|
||||||
from taskflow.patterns import ordered_flow
|
from taskflow.patterns import ordered_flow
|
||||||
|
from taskflow import utils
|
||||||
|
|
||||||
|
|
||||||
class Flow(ordered_flow.Flow):
|
class Flow(ordered_flow.Flow):
|
||||||
@@ -30,14 +33,14 @@ class Flow(ordered_flow.Flow):
|
|||||||
self._tasks = []
|
self._tasks = []
|
||||||
|
|
||||||
def _fetch_task_inputs(self, task):
|
def _fetch_task_inputs(self, task):
|
||||||
would_like = set(getattr(task, 'requires', []))
|
would_like = set(utils.get_attr(task, 'requires', []))
|
||||||
would_like.update(getattr(task, 'optional', []))
|
would_like.update(utils.get_attr(task, 'optional', []))
|
||||||
|
|
||||||
inputs = {}
|
inputs = {}
|
||||||
for n in would_like:
|
for n in would_like:
|
||||||
# Find the last task that provided this.
|
# Find the last task that provided this.
|
||||||
for (last_task, last_results) in reversed(self.results):
|
for (last_task, last_results) in reversed(self.results):
|
||||||
if n not in getattr(last_task, 'provides', []):
|
if n not in utils.get_attr(last_task, 'provides', []):
|
||||||
continue
|
continue
|
||||||
if last_results and n in last_results:
|
if last_results and n in last_results:
|
||||||
inputs[n] = last_results[n]
|
inputs[n] = last_results[n]
|
||||||
@@ -50,10 +53,10 @@ class Flow(ordered_flow.Flow):
|
|||||||
def _validate_provides(self, task):
|
def _validate_provides(self, task):
|
||||||
# Ensure that some previous task provides this input.
|
# Ensure that some previous task provides this input.
|
||||||
missing_requires = []
|
missing_requires = []
|
||||||
for r in getattr(task, 'requires', []):
|
for r in utils.get_attr(task, 'requires', []):
|
||||||
found_provider = False
|
found_provider = False
|
||||||
for prev_task in reversed(self._tasks):
|
for prev_task in reversed(self._tasks):
|
||||||
if r in getattr(prev_task, 'provides', []):
|
if r in utils.get_attr(prev_task, 'provides', []):
|
||||||
found_provider = True
|
found_provider = True
|
||||||
break
|
break
|
||||||
if not found_provider:
|
if not found_provider:
|
||||||
@@ -66,6 +69,7 @@ class Flow(ordered_flow.Flow):
|
|||||||
raise exc.InvalidStateException(msg)
|
raise exc.InvalidStateException(msg)
|
||||||
|
|
||||||
def add(self, task):
|
def add(self, task):
|
||||||
|
assert isinstance(task, collections.Callable)
|
||||||
self._validate_provides(task)
|
self._validate_provides(task)
|
||||||
self._tasks.append(task)
|
self._tasks.append(task)
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class LinearFlowTest(unittest2.TestCase):
|
|||||||
def test_functor_flow(self):
|
def test_functor_flow(self):
|
||||||
wf = lw.Flow("the-test-action")
|
wf = lw.Flow("the-test-action")
|
||||||
|
|
||||||
@decorators.provides('a', 'b', 'c')
|
@decorators.task(provides=['a', 'b', 'c'])
|
||||||
def do_apply1(context):
|
def do_apply1(context):
|
||||||
context['1'] = True
|
context['1'] = True
|
||||||
return {
|
return {
|
||||||
@@ -69,7 +69,7 @@ class LinearFlowTest(unittest2.TestCase):
|
|||||||
'c': 3,
|
'c': 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
@decorators.requires('c', 'a', auto_extract=False)
|
@decorators.task(requires=['c', 'a'], auto_extract=False)
|
||||||
def do_apply2(context, **kwargs):
|
def do_apply2(context, **kwargs):
|
||||||
self.assertTrue('c' in kwargs)
|
self.assertTrue('c' in kwargs)
|
||||||
self.assertEquals(1, kwargs['a'])
|
self.assertEquals(1, kwargs['a'])
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ class MemoryBackendTest(unittest2.TestCase):
|
|||||||
while not poison.isSet():
|
while not poison.isSet():
|
||||||
my_jobs = []
|
my_jobs = []
|
||||||
job_board.await(0.05)
|
job_board.await(0.05)
|
||||||
job_search_from = None
|
for j in job_board.posted_after():
|
||||||
for j in job_board.posted_after(job_search_from):
|
|
||||||
if j.owner is not None:
|
if j.owner is not None:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
@@ -61,10 +60,6 @@ class MemoryBackendTest(unittest2.TestCase):
|
|||||||
my_jobs.append(j)
|
my_jobs.append(j)
|
||||||
except exc.UnclaimableJobException:
|
except exc.UnclaimableJobException:
|
||||||
pass
|
pass
|
||||||
if not my_jobs:
|
|
||||||
# No jobs were claimed, lets not search the past again
|
|
||||||
# then, since *likely* those jobs will remain claimed...
|
|
||||||
job_search_from = datetime.datetime.utcnow()
|
|
||||||
if my_jobs and poison.isSet():
|
if my_jobs and poison.isSet():
|
||||||
# Oh crap, we need to unclaim and repost the jobs.
|
# Oh crap, we need to unclaim and repost the jobs.
|
||||||
for j in my_jobs:
|
for j in my_jobs:
|
||||||
|
|||||||
@@ -21,29 +21,18 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from taskflow import decorators
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_wrapped_function(function):
|
def get_attr(task, field, default=None):
|
||||||
"""Get the method at the bottom of a stack of decorators."""
|
if decorators.is_decorated(task):
|
||||||
|
# If its a decorated functor then the attributes will be either
|
||||||
if not hasattr(function, 'func_closure') or not function.func_closure:
|
# in the underlying function of the instancemethod or the function
|
||||||
return function
|
# itself.
|
||||||
|
task = decorators.extract(task)
|
||||||
def _get_wrapped_function(function):
|
return getattr(task, field, default)
|
||||||
if not hasattr(function, 'func_closure') or not function.func_closure:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for closure in function.func_closure:
|
|
||||||
func = closure.cell_contents
|
|
||||||
|
|
||||||
deeper_func = _get_wrapped_function(func)
|
|
||||||
if deeper_func:
|
|
||||||
return deeper_func
|
|
||||||
elif hasattr(closure.cell_contents, '__call__'):
|
|
||||||
return closure.cell_contents
|
|
||||||
|
|
||||||
return _get_wrapped_function(function)
|
|
||||||
|
|
||||||
|
|
||||||
def join(itr, with_what=","):
|
def join(itr, with_what=","):
|
||||||
@@ -54,7 +43,7 @@ def join(itr, with_what=","):
|
|||||||
def get_many_attr(obj, *attrs):
|
def get_many_attr(obj, *attrs):
|
||||||
many = []
|
many = []
|
||||||
for a in attrs:
|
for a in attrs:
|
||||||
many.append(getattr(obj, a, None))
|
many.append(get_attr(obj, a, None))
|
||||||
return many
|
return many
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user