Added the ability to override nodes from later imp...
# contribute
j
Added the ability to override nodes from later imported modules: https://github.com/DAGWorks-Inc/hamilton/pull/1134 The only part I am unsure about is how to update docs.
Could I also get some guidance in how to update the docs?
t
Thanks for the contribution! I see the thread of issues and Slack convo. I think it's worth discussing the feature a bit more before you commit additional time to it! For one, relying on ordering of arguments can be brittle. For example
.with_modules(foo, bar, allow_override=True)
might not equal
.with_modules(bar, foo, allow_override=True)
. It's good to use the flag
allow_override
to prevent users accidentally overriding functions though Since this change involves many core internals (
FunctionGraph
,
Driver
,
Builder
) I'm wondering if there are other ways to support this feature. Hamilton couples a lot of thing to the
module
Python construct (for better or worse). But concretely, the issue seems to be overriding functions. It might be a valuable change to catch duplicate functions "closer to the user" (e.g.,
Builder.build()
rather than in internal components like
FunctionGraph
j
Hi, thanks for checking over. Can you expand on your thoughts a bit, the way I understand you now is that you want to refactor
find_functions()
all the way from
graph.py
and move it to the
Builder
? Or do you propose to double check and pre-prepare the functions before they get passed to the
create_function_graph
? There is another entry point by creating the
Driver
directly without using the
Builder
- not sure how many people use it this way, but one of the first examples on the website does that. I don't think we can only push it onto the task of the
Builder
.
t
An idea would be to use
find_functions()
, which is a public-facing API part of
hamilton.graph_utils
. Ideally, it would allow us to only make "surface level" changes closer to the
Builder
. ## Default behavior We can keep
.with_modules()
as-is and avoid changes to other internals (
Driver
,
FunctionGraph
). This ensures safety for all Hamilton users ## Override behavior When specifying
.with_modules(..., allow_overrides=True)
, the logic of
.with_modules()
could directly call
find_functions()
and dynamically removed duplicate functions
Copy code
def with_modules(*modules):
   if allow_overrides:
      functions_seen = set()
      for module in modules:
         functions = graph_utils.find_functions(module)
         for name, callabl in functions:
            if name in functions_seen
               del getattr(module, name)
            else:
               functions_seen.add(name)
   
    self.modules.extend(modules)
Odd behavior is possible if
Copy code
import foo
import bar

dr = Builder().with_modules(foo, bar, allow_overrides=True).build()

# works because loaded first
foo.my_func()
# error because the function is deleted
bar.my_func()
A way to overcome this could be to create temporary modules instead of directly deleting from the
module
Copy code
def with_modules(*modules):
   if allow_overrides:
      functions_seen = set()
      temp_modules = []
      for module in modules:
         unique_functions = set()
         functions = graph_utils.find_functions(module)
         for name, callabl in functions:
            if name in functions_seen
               del getattr(module, name)
            else:
               unique_functions.add(callabl)
               functions_seen.add(name)

      temp_module = hamilton.ad_hoc_utils.create_temporary_module(...)
      temp_modules.append(temp_module)

    self.modules.extend(modules)
e
@Jernj Frank — really appreciate the contribution! I think this makes sense to use the internal API, and I’m not particularly concerned about relying on ordering. This is already a bit of a power-mode feature, so people will be OK with the caveats of ordering. Re the
allow_overrides
flag — we specifically decided to put it centrally, rather than on the
with_modules
due to the fact that it gets you to odd behavior if it’s on an individual module call. E.G. what if you add it in one with_modules and not the other? More stuff to think about. We also can’t catch it on the builder @Thierry Jean — it has no idea what the nodes are, as we construct the fnction graph. We could always rearchictect (construct the function graph beforehand then merge a bunch), but given the way it’s built, I think this is a very reasonable approach. Function graph is specifically repsonsible for taking all the functions and putting it together. Some nuance in how we handle it with modules/functions/who knows about what, but that’s all stuff that can change because it’s an internal API. The more we push out, the harder of a time we have doing that later.
I’ll be looking over soon, we can have more discussions on the right way of doing it in the PR
👍 1
Regarding documentation — I think it should auto-update for the driver builder, as long as it’s valid .rst format: • https://hamilton.dagworks.io/en/latest/reference/drivers/Driver/ You can always get this from the build, by clicking on the rtd link (details on builder). you can also run locally from the repo root — you’ll neecd all the docs stuff installed + sphinx-autobuild —
sphinx-autobuild -b dirhtml -W -E -T  --watch docs/ -a docs /tmp/mydocs
. Might also be nice to add a note here (linking out to the prior one): https://hamilton.dagworks.io/en/latest/concepts/builder/ on this capability. RST can be an absolute mess (linking/compiling), so ping if you have trouble. You can probably get away just iterating ont he PR after it builds (although I’m not sure if I have to run it or it always will)
j
Seems like it does a new build every time I push and updates the docs as well. Bit of a wait to get to the generate docs part, but better than needing to locally install all the things needed for generating docs
e
Yeah, locally installing is good if you do more than a few iterations (it’s super fast), but is a bit of work to set up