Skip to content

Commit 4e67fed

Browse files
committed
Add custom hook
1 parent 81795da commit 4e67fed

10 files changed

Lines changed: 254 additions & 125 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# Changelogs
22

3+
## Version 1.0.0
4+
(Released : April 2, 2021) : tag 1.0.0
5+
* Complete documentation
6+
* Test and implement custom visitor hook
37

4-
## Version 1.0.0
8+
## Version 0.0.0
9+
(Alpha released : March 27, 2021) : tag 0.1.0
510
* First implementation:
611
* Execution graph implementation
712
* Visitor implementation
@@ -15,9 +20,9 @@
1520

1621
## Documentation todo
1722

18-
* Add doc about node / graph creation
19-
* Add documentation about graph nodes
20-
* Add documentation about custom hook
23+
* ~~Add doc about node / graph creation~~
24+
* ~~Add documentation about graph nodes~~
25+
* ~~Add documentation about custom hook~~
2126

2227
## Feature todo
2328

@@ -26,4 +31,4 @@
2631
## Testing todo
2732

2833
* ~~Test Composed Visitor~~
29-
* Custom hook
34+
* ~~Custom hook~~

README.md

Lines changed: 124 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ It provide:
2929
* [networkx](https://networkx.org/) : FreExGraph is a layer on top of networkx
3030
* [tqdm](https://github.com/tqdm/tqdm) : provide a nice way to display progression on visitation
3131

32+
---
33+
3234
# Documentation
3335

3436
The goal of this library is to provide a standardized way to represent an execution graph and to visit it via visitor. Some visitors are provided by FreExGraph but the more important thing is the ability to very easily provide its own visitor and it's own node type.
@@ -39,7 +41,7 @@ The goal of this library is to provide a standardized way to represent an execut
3941

4042
In order to use FreExGraph properly, it is required to provide your own node type. A node is a class that inherit from FreExNode class. It provides the following interface:
4143
```python
42-
from freeexgraph import FreExNode, AbstractVisitor
44+
from freexgraph import FreExNode, AbstractVisitor
4345

4446
class MyCustomNode(FreExNode):
4547
def accept(self, visitor: AbstractVisitor) -> bool:
@@ -62,6 +64,7 @@ Here is a complete example that can be used as a good start to show how you shou
6264
```python
6365

6466
# We have Three different kind of node : Alpha, Beta, Gamma
67+
from freexgraph import FreExNode, AbstractVisitor
6568

6669
class MyNodeAlpha(FreExNode):
6770
def accept(self, visitor: AbstractVisitor) -> bool:
@@ -97,7 +100,7 @@ class MyBaseVisitor(AbstractVisitor):
97100
# My actual Visitor implementation, this one just need a specific action
98101
# to be made on MyNodeGamma node type. The rest has a default behaviour.
99102

100-
class CustomVisitor(MyBaseVisitor)
103+
class CustomVisitor(MyBaseVisitor):
101104
def visit_default(self, node: FreExNode):
102105
print("This is the behavior for Alpha and Beta nodes")
103106
return True
@@ -114,19 +117,98 @@ class CustomVisitor(MyBaseVisitor)
114117

115118
After creating node types (see above), we can create an execution graph that will use those nodes.
116119

120+
A graph is created with the class `FreExGraph`, which doesn't require any parameter, as follow:
121+
```python
122+
from freexgraph import FreExGraph
123+
124+
execution_graph = FreExGraph()
125+
```
126+
117127
> it is important to note that the character ':' is forbidden in node id, as it is used for internal node (fork uniqueness and/or root_node)
118128
119-
**add_node:**
129+
When the graph instance is done, you need to add nodes to them. The two following methods makes it possible.
120130

121-
**add_nodes:**
131+
**add_node:** Of the method of signature `add_node(self, node: FreExNode)`. This method will add a node to the graph. If no parents are set in the given node,
132+
the node will be linked with the root node that is automatically generated by the FreExGraph. If parents are set, edges will be made.
133+
* Node id HAS to not be present in the graph
134+
* A node's parent HAS to already be present in the graph.
135+
* The id of the graph cannot contains ':' which is a forbidden character.
136+
* No infinite looping of the node are accepted.
122137

138+
In the case any of those rules is not respected. An Assertion error is thrown.
139+
140+
example:
141+
```python
142+
# The graph below would look like that.
143+
#
144+
# id1
145+
# |
146+
# id2 ____.
147+
# | \
148+
# | id4
149+
# | / |
150+
# id3 ----' |
151+
# | |
152+
# id5 -------`
153+
#
154+
execution_graph = FreExGraph()
155+
id1 = f"id1"
156+
id2 = f"id2"
157+
id3 = f"id3"
158+
id4 = f"id4"
159+
id5 = f"id5"
160+
161+
execution_graph.add_node(FreExNode(id1))
162+
execution_graph.add_node(FreExNode(id2, parents={id1}))
163+
execution_graph.add_node(FreExNode(id4, parents={id2}))
164+
execution_graph.add_node(FreExNode(id3, parents={id2, id4}))
165+
execution_graph.add_node(FreExNode(id5, parents={id4, id3}))
166+
```
167+
168+
**add_nodes:**: It can be cumbersome to ensure the ordering of the nodes you want to add (with the parents order). In order to avoid this issue, you can use add_nodes with the signature `add_nodes(self, nodes: List[AnyFreExNode])`.
169+
This method is going to re-order the nodes depending on their parents in order to add them (calling add_node internally) properly.
170+
`add_node` being called. All the rules applicable on add_node has to be respected with add_nodes (unicity of id and so on...)
171+
172+
173+
example
174+
```python
175+
# we want to make the same graph as the one tested above for add_node
176+
execution_graph = FreExGraph()
177+
node_1 = FreExNode(id1)
178+
node_2 = FreExNode(id2, parents={id1})
179+
node_3 = FreExNode(id3, parents={id2, id4}
180+
node_4 = FreExNode(id4, parents={id2})
181+
node_5 = FreExNode(id5, parents={id4, id3}))
182+
183+
# adding them in complete disorder is okay
184+
execution_graph.add_nodes([node_5, node_2, node_1, node_3, node_4])
185+
```
123186

124187
### Graph Node
125188

126189
It is possible to embed a graph into another thanks to a graph node. Any visitation going through a graph node is going to be propagated to the inner graph.
127190

128-
< TODO >
191+
To make one you need to create a FreExGraph (that will be in the GraphNode).
192+
example :
129193

194+
```python
195+
# We will do the following graph
196+
#
197+
# id1___,
198+
# | \
199+
# | id_graph (graph_node)
200+
# | /
201+
# id2---`
202+
#
203+
execution_graph = FreExGraph()
204+
205+
inner_graph = FreExGraph()
206+
# fill inner_graph as you want
207+
208+
execution_graph.add_node(NodeForTest(id1))
209+
execution_graph.add_node(GraphNode(id_graph, parents={id1}, graph=inner_graph))
210+
execution_graph.add_node(NodeForTest(idc, parents={id1, id_graph}))
211+
```
130212

131213
### Fork
132214

@@ -149,7 +231,7 @@ _Given the following graph in `execution_graph`:_
149231
#
150232

151233
# If we decide to fork (with the fork id f1) from the node id4
152-
valid_basic_execution_graph.fork_from_node(FreExNode(uid="id4", fork_id="f1"))
234+
execution_graph.fork_from_node(FreExNode(uid="id4", fork_id="f1"))
153235

154236
# The result would be the following:
155237

@@ -170,27 +252,35 @@ Obviously if you want the node id4::f1 (which is of type FreExNode as you asked)
170252

171253
It is also possible to provide a join node. It will be a node used as join for the fork in order to not have to fork the whole graph from the source of the fork. It is usefull if you have to multiply a big chunk of execution graph because one node has to change some internal values (in experimentation fields, it can be useful for parameter explorations).
172254

173-
> **IN CASE OF MAP REDUCE CASES DO NOT USE FORKS**. It is possible to do so with a join_id.. But it is prefered to do manually your map reduce (with add_node / add_nodes) than to use fork.
255+
> **Try avoiding forks** : This is a mecanism that can be useful in certain cases (the main one would be parameter exploration on an experimentation) But when it comes to map reduce for example, it is advised to manually fo the nodes you want instead (improve readibility of what you are doing when making your graph). A chaining of fork can start being very hard to understand for the user.
174256
175-
Join is do-able by adding the
257+
But if you want to do a map reduce with a fork, it is do-able by setting the join_id to the `fork_from_node` method. The join_id has to be an existing node on which, for every parents that are part of the fork has only this join node as child.
258+
See [test using this mechanism](https://github.com/FreeYourSoul/FreExGraph/blob/ae707cf0fcb8486bde783cd0c7fe67217a56b3d2/test/fork_test.py#L41-L66) for more details
176259

177260
## Visitors
178261

179262
### Abstract Visitor hooks
180263

181264
Abstract visitor provide some default hooks that can be overridden from custom visitors in order to implement more complex logic depending on the graph visit.
182-
* `hook_start()` : This hook is called when the visitation of the graph start (an interesting way to use this hook could be to reinitialize your visitor in case of re-use).
183-
* `hook_end()` : This hook is called when the visitation of the graph end.
184-
* `hook_fork_started(n: FreExNode, fork_id: str)` : This hook is called when a fork has been entered (when visiting the first node of a fork).
265+
* `hook_start()`: This hook is called when the visitation of the graph start (an interesting way to use this hook could be to reinitialize your visitor in case of re-use).
266+
* `hook_end()`: This hook is called when the visitation of the graph end.
185267
* `hook_start_graph_node(gn: GraphNode)` : This hook is called when a graphnode recursion start (graph node given as parameter of the hook).
186268
* `hook_end_graph_node(gn: GraphNode)` : This hook is called when a graphnode visitation end (graph node given as parameter of the hook).
187269

188-
Custom hooks can be implemented if you must trigger a specific action that depend on the business data stored in your node.
189-
To do so, in the `__init__` of your custom visitor, use the method `register_custom_hook(predicate: Callable, hook: Callable)` : This method will trigger the provided hook if the given predicate return true for a node.
270+
Custom hooks can be implemented on any visitor if you want to trigger a specific action that depend on the business data stored in your node.
271+
272+
To do so, on a visitor instance, use the method `register_custom_hook(predicate: Callable, hook: Callable)` : This method will trigger the provided hook if the given predicate return true for a node. Custom hooks are all executed just before the visitation of each node.
190273

191274
**Example of a custom hook:**
192275
```python
193-
# TODO
276+
visitor_test.register_custom_hook(
277+
predicate=lambda n: n.id.startswith("id3"), hook=hook
278+
)
279+
visitor_test.register_custom_hook(
280+
predicate=lambda n: not n.id.startswith("id3"),
281+
hook=hook_2,
282+
)
283+
visitor_test.visit(execution_graph.root)
194284
```
195285

196286
### Standard Visitor provided by FreExGraph
@@ -243,13 +333,28 @@ v = ValidateGraphIntegrity()
243333
# visitation does assertion to verify if the graph is correct
244334
v.visit(graph_above.root())
245335
```
246-
Those visitor implement the start hook that reinitialize their state as if they were new freshly created visitor. Which is why an instance of a standard visitor can be re-used. To implement this kind of behaviour in your own visitors, check out [visitor hooks](#abstract-visitor-hooks).
336+
Those visitors implement the start hook that reinitialize their state as if they were new freshly created visitor. Which is why an instance of a standard visitor can be re-used. To implement this kind of behaviour in your own visitors, check out [visitor hooks](#abstract-visitor-hooks).
337+
338+
### Progression visit
339+
340+
tqdm is implemented in AbstractVisitor.
341+
It makes possible to have a progress bar while visiting your graph. To do so, when instantiating the AbstractVisitor from `super()` within your custom visitor.
342+
Just set the boolean parameter `with_progress_bar` in the constructor to True.
343+
```python
344+
class MyCustomVisitor(AbstractVisitor):
345+
def __init__(self):
346+
super().__init__(with_progress_bar=True)
347+
348+
# rest of your custom visitor implementation
349+
...
350+
```
247351

248352
### Reverse visit
249-
It is possible to revert the order of visitation of your visitor by setting its attribute `is_reversed` of your visitor (works with any kind of visitor), example :
353+
It is possible to revert the order of visitation of any visitor by setting its attribute `is_reversed` of the visitor (works with any type of visitor inheriting from AbstractVisitor).
354+
example :
250355
```python
251356
# we assume a graph called `execution_graph` that represent the following graph:
252-
# id_1 ---> id_2 ---> id_3 ---> id4 ---> id3bis ---> id5
357+
# id1 ---> id2 ---> id3 ---> id4 ---> id3bis ---> id5
253358

254359
v = FindFirstVisitor(lambda node: node.id.startswith("id3"))
255360

@@ -263,6 +368,8 @@ assert v.found()
263368
assert v.result.id == "id3bis"
264369
```
265370

371+
---
372+
266373
## Installation
267374

268375
* Via pip

freexgraph/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@
2525
Module to manipulate an execution graph
2626
"""
2727

28-
from freexgraph._version import __version__
29-
30-
from freexgraph.freexgraph import FreExGraph, FreExNode, AnyVisitor
28+
from freexgraph.freexgraph import GraphNode, FreExGraph, FreExNode, AnyVisitor
3129
from freexgraph.visitor import AbstractVisitor, VisitorComposer
3230

3331
import freexgraph.standard_visitor
32+
33+
34+
def version() -> str:
35+
from freexgraph._version import __version__
36+
37+
return __version__

freexgraph/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@
2121
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222
# SOFTWARE.
2323

24-
__version__ = "0.1.0"
24+
__version__ = "1.0.0"

freexgraph/freexgraph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def add_node(self, node: AnyFreExNode) -> None:
218218
"""
219219
assert (
220220
node.fork_id is not None or ":" not in node.id
221-
), f"Node cannot contains a ':' in its name {node.id}"
221+
), f"Node cannot contains a ':' in its id {node.id}"
222222
assert not self._graph.has_node(
223223
node.id
224224
), f"{node.id} is already in the execution graph"

0 commit comments

Comments
 (0)