Proves that we can use builtin python types to fake modules
diff --git a/hamilton/ad_hoc_utils.py b/hamilton/ad_hoc_utils.py
new file mode 100644
index 0000000..0b15748
--- /dev/null
+++ b/hamilton/ad_hoc_utils.py
@@ -0,0 +1,48 @@
+"""A suite of tools for ad-hoc use"""
+import sys
+from typing import Callable
+from types import ModuleType
+import uuid
+
+import types
+
+
+def _copy_func(f):
+ """Returns a function with the same properties as the original one"""
+ fn = types.FunctionType(f.__code__, f.__globals__, f.__name__,
+ f.__defaults__, f.__closure__)
+ # in case f was given attrs (note this dict is a shallow copy):
+ fn.__dict__.update(f.__dict__)
+ fn.__annotations__ = f.__annotations__ # No idea why this is not a parameter...
+ return fn
+
+
+def _generate_unique_temp_module_name() -> str:
+ """Generates a unique module name that is a valid python variable."""
+ return f"temporary_module_{str(uuid.uuid4()).replace('-', '_')}"
+
+
+def create_temporary_module(*functions: Callable, module_name: str = None) -> ModuleType:
+ """Creates a temporary module usable by hamilton. Note this should *not* be used in production --
+ you should really be organizing your functions into modules. This is perfect in a jupyter notebook,
+ however, where you have a few functions that you want to string together to build an ad-hoc driver.
+
+ Note that this is slightly dangerous -- we want the module to look and feel like an actual module
+ so we can fully duck-type it. We thus stick it in sys.modules (checking if it already exists)
+
+ @param functions: Functions to use
+ @param module_name: Module name to use. if not provided will default to a unique one.
+ @return: The module that was created.
+ """
+ module_name = module_name if module_name is not None else _generate_unique_temp_module_name()
+ if module_name in sys.modules:
+ raise ValueError(f"Module already exists with name: {module_name}, please make it unique.")
+ module = ModuleType(module_name)
+ for fn in map(_copy_func, functions): # Just use copies so we don't mess with the original functions
+ fn_name = fn.__name__
+ if hasattr(module, fn_name):
+ raise ValueError(f"Duplicate/reserved function name: {fn_name} cannot be used to create a dynamic module.")
+ fn.__module__ = module.__name__
+ setattr(module, fn_name, fn)
+ sys.modules[module_name] = module
+ return module
diff --git a/hamilton/base.py b/hamilton/base.py
index a962192..59285a0 100644
--- a/hamilton/base.py
+++ b/hamilton/base.py
@@ -167,23 +167,3 @@
def build_result(self, **outputs: typing.Dict[str, typing.Any]) -> typing.Any:
"""Delegates to the result builder function supplied."""
return self.result_builder.build_result(**outputs)
-
-
-class TemporaryFunctionModule(object):
- """This class represents a way to encapsulate a set of functions for DAG construction.
-
- It is meant to be used in a notebook, e.g. google collab, like context where you are iterating and prototyping.
-
- Once you're happy with the functions you have created, you're supposed to transition the functions into a
- proper python module.
- """
- __name__ = 'TemporaryFunctionModule'
-
- def __init__(self, *functions: Callable):
- """Constructor.
-
- Takes in functions as arguments.
- """
- for function in functions:
- # this sets each function as an attribute on this object.
- setattr(self, function.__name__, function)
diff --git a/hamilton/driver.py b/hamilton/driver.py
index e992ecd..3dbbce9 100644
--- a/hamilton/driver.py
+++ b/hamilton/driver.py
@@ -44,7 +44,7 @@
def __init__(self,
config: Dict[str, Any],
- *modules: Union[ModuleType, base.TemporaryFunctionModule],
+ *modules: ModuleType,
adapter: base.HamiltonGraphAdapter = None):
"""Constructor: creates a DAG given the configuration & modules to crawl.
diff --git a/hamilton/graph.py b/hamilton/graph.py
index e38cbba..6b93fd2 100644
--- a/hamilton/graph.py
+++ b/hamilton/graph.py
@@ -94,7 +94,7 @@
return False
-def find_functions(function_module: Union[ModuleType, base.TemporaryFunctionModule]) -> List[Tuple[str, Callable]]:
+def find_functions(function_module: ModuleType) -> List[Tuple[str, Callable]]:
"""Function to determine the set of functions we want to build a graph from.
This iterates through the function module and grabs all function definitions.
@@ -103,8 +103,7 @@
def valid_fn(fn):
if inspect.isfunction(fn) and not fn.__name__.startswith('_'):
- return (is_submodule(inspect.getmodule(fn), function_module)
- or isinstance(function_module, base.TemporaryFunctionModule))
+ return is_submodule(inspect.getmodule(fn), function_module)
return False
return [f for f in inspect.getmembers(function_module, predicate=valid_fn)]
@@ -139,8 +138,8 @@
required_node.depended_on_by.append(func_node)
-def create_function_graph(*modules: Union[ModuleType, base.TemporaryFunctionModule],
- config: Dict[str, Any], adapter: base.HamiltonGraphAdapter) -> Dict[str, node.Node]:
+def create_function_graph(*modules: ModuleType, config: Dict[str, Any],
+ adapter: base.HamiltonGraphAdapter) -> Dict[str, node.Node]:
"""Creates a graph of all available functions & their dependencies.
:param modules: A set of modules over which one wants to compute the function graph
:param config: Dictionary that we will inspect to get values from in building the function graph.
@@ -222,7 +221,7 @@
"""
def __init__(self,
- *modules: Union[ModuleType, base.TemporaryFunctionModule],
+ *modules: ModuleType,
config: Dict[str, Any],
adapter: base.HamiltonGraphAdapter = None):
"""Initializes a function graph by crawling through modules. Function graph must have a config,
diff --git a/tests/test_graph.py b/tests/test_graph.py
index 579d046..494359a 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -19,7 +19,7 @@
import tests.resources.parametrized_inputs
import tests.resources.parametrized_nodes
import tests.resources.typing_vs_not_typing
-from hamilton import graph, base
+from hamilton import graph, base, ad_hoc_utils
from hamilton import node
from hamilton.node import NodeSource
@@ -39,12 +39,13 @@
expected = [('A', tests.resources.dummy_functions.A),
('B', tests.resources.dummy_functions.B),
('C', tests.resources.dummy_functions.C)]
- func_module = base.TemporaryFunctionModule(tests.resources.dummy_functions.A,
+ func_module = ad_hoc_utils.create_temporary_module(tests.resources.dummy_functions.A,
tests.resources.dummy_functions.B,
tests.resources.dummy_functions.C)
actual = graph.find_functions(func_module)
assert len(actual) == len(expected)
- assert actual == expected
+ assert [node_name for node_name, _ in actual] == [node_name for node_name, _ in expected]
+ assert [fn.__code__ for _, fn in actual] == [fn.__code__ for _, fn in expected] # easy way to say they're the same
def test_add_dependency_missing_param_type():
@@ -666,7 +667,7 @@
def my_function(A: int, b: int, c: int) -> int:
"""Function for input below"""
return A + b + c
- f_module = base.TemporaryFunctionModule(my_function)
+ f_module = ad_hoc_utils.create_temporary_module(my_function)
fg = graph.FunctionGraph(tests.resources.dummy_functions, f_module, config={'b': 3, 'c': 1})
results = fg.execute([n for n in fg.get_nodes() if n.name in ['my_function', 'A']])
assert results == {'A': 4, 'b': 3, 'c': 1, 'my_function': 8}