Skip to content

Commit

Permalink
Update dependencies whenever an expression task executes. Closes #8.
Browse files Browse the repository at this point in the history
  • Loading branch information
tshead committed Nov 8, 2020
1 parent 5dd1029 commit fc57ad3
Show file tree
Hide file tree
Showing 3 changed files with 593 additions and 100 deletions.
9 changes: 4 additions & 5 deletions features/graph.feature
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,13 @@ Feature: Graph
And the tasks ["A", "B", "C"] should be finished
And the task ["A", "B", "C"] outputs should be [1, 2, 7]
When changing the expression task "C" to expression "out('A') + 1.1"
Then the graph should contain links [("A", ("C", graphcat.Input.DEPENDENCY))]
And the tasks ["A", "B", "C"] should be finished
And the task ["A", "B", "C"] outputs should be [1, 2, 2.1]
Then the task ["A", "B", "C"] outputs should be [1, 2, 2.1]
And the graph should contain links [("A", ("C", graphcat.Input.AUTODEPENDENCY))]
When the task "A" function is changed to graphcat.constant(3)
Then the task ["A", "B", "C"] outputs should be [3, 2, 4.1]
When changing the expression task "C" to expression "out('B') + 1.1"
Then the graph should contain links [("B", ("C", graphcat.Input.DEPENDENCY))]
And the task ["A", "B", "C"] outputs should be [3, 2, 3.1]
Then the task ["A", "B", "C"] outputs should be [3, 2, 3.1]
And the graph should contain links [("B", ("C", graphcat.Input.AUTODEPENDENCY))]


Scenario: Graph Logger
Expand Down
60 changes: 41 additions & 19 deletions graphcat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,33 @@
log = logging.getLogger(__name__)


class AutomaticDependencies(object):
"""Function decorator that automatically tracks dependencies.
"""
def __init__(self, graph, name, fn):
self.graph = graph
self.name = name
self.fn = fn

def __call__(self, *args, **kwargs):
# Remove old, automatically generated dependencies.
edges = list(self.graph._graph.out_edges(self.name, data="input", keys=True))
for target, source, key, input in edges:
if input == Input.AUTODEPENDENCY:
self.graph._graph.remove_edge(target, source, key)

# Keep track of dependencies while the task executes.
updated = UpdatedTasks(self.graph)
result = self.fn(*args, **kwargs)

# Create new dependencies.
sources = updated.tasks.difference([self.name])
for source in sources:
self.graph._graph.add_edge(self.name, source, input=Input.AUTODEPENDENCY)

return result


class DeprecationWarning(Warning):
"""Warning category for deprecated code."""
pass
Expand Down Expand Up @@ -386,7 +413,8 @@ def output(self, name):
def set_expression(self, name, expression, locals={}):
"""Create a task that will execute a Python expression.
The task will automatically track implicit dependencies.
The task will automatically track implicit dependencies that
arise from executing the expression.
Parameters
----------
Expand All @@ -398,18 +426,9 @@ def set_expression(self, name, expression, locals={}):
Optional dictionary containing local objects that will be available for
use in the expression.
"""
self.set_task(name, execute(expression, locals))

sources = list(self._graph.successors(name))
for source in sources:
self._graph.remove_edge(name, source)

updated = UpdatedTasks(self)
self.update(name)

sources = updated.tasks.difference([name])
for source in sources:
self._graph.add_edge(name, source, input=Input.DEPENDENCY)
fn = execute(expression, locals)
fn = AutomaticDependencies(self, name, fn)
self.set_task(name, fn)


def set_links(self, source, targets):
Expand Down Expand Up @@ -596,8 +615,8 @@ def update(self, name):

class Input(enum.Enum):
"""Enumerates special :class:`Graph` named inputs."""
DEPENDENCY = 1
"""Named input for links that are used only as dependencies, not data sources."""
AUTODEPENDENCY = 1
"""Named input for links that are generated automatically for use as dependencies, not data sources."""


class Logger(object):
Expand Down Expand Up @@ -697,13 +716,13 @@ def tasks(self):
Returns
-------
tasks: set
Python set containing the names for every task that has been updated.
tasks: :class:`set`
Python :class:`set` containing the names for every task that has
been updated.
"""
return self._tasks



def constant(value):
"""Factory for task functions that return constant values when executed.
Expand Down Expand Up @@ -754,7 +773,10 @@ def execute(code, locals={}):
task is executed.
"""
def implementation(name, inputs):
return eval(code, {}, dict(locals))
try:
return eval(code, {}, dict(locals))
except Exception as e: # pragma: no cover
raise RuntimeError(f"Uncaught exception executing expression {code!r}: {e}")
return implementation


Expand Down
Loading

0 comments on commit fc57ad3

Please sign in to comment.