-
Notifications
You must be signed in to change notification settings - Fork 37
Implements opinionated decorator lifecycle #28
Conversation
b7a7681
to
f5a1ba7
Compare
TODO:
|
3c453f4
to
04bab48
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what's net new here? Just the transform decorator? Suggestion on name: augment
. Transform is a bit overloaded a term.
Otherwise the decorator lifecycle is largely a refactor of existing behavior, right?
The decorator lifecycle is solidified and expanded -- moved some things around, but yeah, a refactor of existing behavior. Technically this allows models + parametrization together (whereas before it wouldn't), although that could get a little weird and probably won't be used much. |
4e29797
to
376b8c8
Compare
0ec3f40
to
096082d
Compare
ef9501a
to
c6cfeae
Compare
c6cfeae
to
7495b00
Compare
This adds to the set of decorators that can be applied to a node, and puts them in a uniform setting. The ordering is provided in function_base.resolve_nodes -- the algorithm is as follows: 1. If there is a list of function resolvers, apply them one after the other. Otherwise, apply the default function resolver which will always return just the function. This determines whether to proceed -- if any function resolver is none, short circuit and return an empty list of nodes. 2. If there is a list of node creators, that list must be of length 1 -- this is determined in the node creator class. Apply that to get the initial node. 3. If there is a list of node expanders, apply them. Otherwise apply the default nodeexpander This must be a list of length one. This gives out a list of nodes. 4. If there is a node transformer, apply that. Note that the node transformer gets applied individually to just the sink nodes in the subdag. It subclasses "DagTransformer" to do so. 5. Return the final list of nodes.
This allows us to run nodes like the following: @Transform('a=a+b+c') def a(): ... And now a will be the result of adding a to b to c. Note a, b, c, currently have to be a node or a config item. Some caveats: 1. Its unsafe! We use eval now 2. Its just called transform. Re (2) we should make it so the transform utilizes the same pattern as config: 1. The transform takes in a lambda that takes in a dict 2. transform.to(...) takes in an expression and creates (1) by passing to a superclass
For when the function signature doesn't accurately reflect the dependency type -- E.G. in created nodes (such as a transform) in which we want to use kwargs.
There's some subtlety with the name. Previously, config.when would change the function name accordingly. This was so that later on decorators could refer to the function by its true name. However, this broke depending on decorator ordering. This change adds a sanitize_name functionality to unblock the transform decorator, but does not yet take away the name changing functionality. We'll need to think through that, but this is a happy medium for now I think.
This solves #53 This allows us to decouple the notion of dynamic_transform from model. Model was specific to Stitch Fix's use-case. With this change, we can build more complex dynamic transforms that aren't limited to pd.Series or simple combinations of nodes. This is also more readable/makes more inherent sense. Note that full backwards compatibility has been maintained by subclassing and keeping parameter names.
This is not going to make it into the final release. Instead see draft PR #60
7495b00
to
60f6415
Compare
hamilton/function_modifiers_base.py
Outdated
|
||
|
||
class NodeResolver(NodeTransformLifecycle): | ||
"""Decorator to resolve a nodes function. Can modify anything about the function and is run before the node.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this more apt?
"""Decorator to resolve a nodes function. Can modify anything about the function and is run before the node.""" | |
"""Decorator to resolve a nodes function. Can modify anything about the function and is run at DAG creation time.""" |
pass | ||
|
||
|
||
class NodeExpander(SubDAGModifier): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doc string
hamilton/graph.py
Outdated
@@ -23,6 +24,25 @@ def is_submodule(child: ModuleType, parent: ModuleType): | |||
return parent.__name__ in child.__name__ | |||
|
|||
|
|||
def custom_subclass_check(requested_type: Type[Type], param_type: Type[Type]): | |||
"""This is a custom check as subclass does not work with python generic. | |||
That said, its |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unfinished comment.
- config.when makes it only apply when foo=bar | ||
- @does makes it do the sum pattern | ||
- @parametrized curries the function then turns it into two | ||
- @augment changes the function to make each of those two final nodes take in vars c and d |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove
In all, this outputs two total nodes. | ||
- config.when makes it only apply when foo=bar | ||
- @does makes it do the sum pattern | ||
- @parametrized curries the function then turns it into two |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
say what the names of the two nodes will be?
- @parametrized curries the function then turns it into two | |
- @parametrized curries the function then turns it into two; one named `e` and the other `f`. |
- config.when makes it only apply when foo=bar | ||
- @does makes it do the sum pattern | ||
- @parametrized curries the function then turns it into two | ||
- @augment changes the function to make each of those two final nodes take in vars c and d |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove
In all, this outputs two total nodes. | ||
- config.when makes it only apply when foo=bar | ||
- @does makes it do the sum pattern | ||
- @parametrized curries the function then turns it into two |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the names here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E and f I think? Added docs.
|
||
@does(_sum) | ||
@parametrized(parameter='a', assigned_output={('e', 'First value'): 10, ('f', 'First value'): 20}) | ||
@config.when(foo='bar') | ||
def c__foobar(a: int, b: int) -> int: | ||
"""Demonstrates utilizing a bunch of decorators. | ||
In all, this outputs two total nodes. | ||
- config.when makes it only apply when foo=bar | ||
- @does makes it do the sum pattern | ||
- @parametrized curries the function then turns it into two | ||
- @augment changes the function to make each of those two final nodes take in vars c and d | ||
""" | ||
pass | ||
|
||
|
||
@does(_sum) | ||
@parametrized(parameter='a', assigned_output={('e', 'First value'): 11, ('f', 'First value'): 22}) | ||
@config.when(foo='baz') | ||
def c__foobaz(a: int, b: int) -> int: | ||
"""Demonstrates utilizing a bunch of decorators. | ||
In all, this outputs two total nodes. | ||
- config.when makes it only apply when foo=bar | ||
- @does makes it do the sum pattern | ||
- @parametrized curries the function then turns it into two | ||
- @augment changes the function to make each of those two final nodes take in vars c and d | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably worth commenting about the relationship between these two examples. They're both prefixed with c
... I'm actually still confused what the output node names will be, given that e & f overlap...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are mutually disjoint -- e and f will have different values depending on the value of foo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, duh. But wasn't obvious on first pass :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added docs to clarify :)
This adds to the set of decorators that can be applied to a node,
and puts them in a uniform setting. The ordering is provided in
function_base.resolve_nodes -- the algorithm is as follows:
If there is a list of function resolvers, apply them one
after the other. Otherwise, apply the default function resolver
which will always return just the function. This determines whether to
proceed -- if any function resolver is none, short circuit and return
an empty list of nodes.
If there is a list of node creators, that list must be of length 1
-- this is determined in the node creator class. Apply that to get
the initial node.
If there is a list of node expanders, apply that. This must be a
list of length one. This gives out a list of nodes.
If there is a node transformer, apply that. Note that the node transformer
gets applied individually to just the sink nodes in the subdag. It subclasses
"DagTransformer" to do so.
Return the final list of nodes.
Additions
Removals
Changes
Testing
Screenshots
If applicable
Notes
Todos
Checklist
Testing checklist
Python