diff --git a/hamilton/graph_types.py b/hamilton/graph_types.py index 815d41c26..0b699a542 100644 --- a/hamilton/graph_types.py +++ b/hamilton/graph_types.py @@ -4,6 +4,7 @@ import functools import hashlib import inspect +import logging import typing from dataclasses import dataclass @@ -18,6 +19,8 @@ if typing.TYPE_CHECKING: from hamilton import graph +logger = logging.getLogger(__name__) + def _remove_docs_and_comments(source: str) -> str: """Remove the docs and comments from a source code string. @@ -93,7 +96,7 @@ class HamiltonNode: type: typing.Type tags: typing.Dict[str, typing.Union[str, typing.List[str]]] is_external_input: bool - originating_functions: typing.Tuple[typing.Callable, ...] + originating_functions: typing.Optional[typing.Tuple[typing.Callable, ...]] documentation: typing.Optional[str] required_dependencies: typing.Set[str] optional_dependencies: typing.Set[str] @@ -142,13 +145,24 @@ def from_node(n: node.Node) -> "HamiltonNode": ) @functools.cached_property - def version(self) -> str: + def version(self) -> typing.Optional[str]: """Generate a hash of the node originating function source code. + Note that this will be `None` if the node is an external input/has no + originating functions. + The option `strip=True` means docstring and comments are ignored when hashing the function. """ - return hash_source_code(self.originating_functions[0], strip=True) + if not self.originating_functions: + return None + try: + return hash_source_code(self.originating_functions[0], strip=True) + except Exception: + logger.warning( + f"Failed to hash source code for node {self.name}. Certain environments do not allow it. In this case, version will be None." + ) + return None def __repr__(self): return f"{self.name}: {htypes.get_type_as_string(self.type)}" diff --git a/tests/test_graph_types.py b/tests/test_graph_types.py index f44914788..ff60d1ffa 100644 --- a/tests/test_graph_types.py +++ b/tests/test_graph_types.py @@ -6,6 +6,7 @@ import pytest from hamilton import graph_types, node +from hamilton.node import Node, NodeType from tests import nodes as test_nodes @@ -112,6 +113,14 @@ def node_to_create(required_dep: int, optional_dep: int = 1) -> str: } +def test_create_hamilton_node_missing_version(): + n = Node("foo", int, node_source=NodeType.EXTERNAL) + hamilton_node = graph_types.HamiltonNode.from_node(n) + # the above will have no specified versions + assert hamilton_node.version is None + assert hamilton_node.as_dict()["version"] is None + + def test_json_serializable_dict(): for name, obj in inspect.getmembers(test_nodes): if inspect.isfunction(obj) and not name.startswith("_"):