Skip to content

Commit

Permalink
Merge pull request #15 from cdfmlr/14-ConditionNode_alignment_support
Browse files Browse the repository at this point in the history
condition node alignment support
  • Loading branch information
cdfmlr authored Feb 14, 2022
2 parents e669472 + 38b9828 commit 94a4150
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 32 deletions.
68 changes: 59 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ To flowchartlize your python codes in `example.py`,run:
$ python3 -m pyflowchart example.py
```

PyFlowchart will output the generated flowchart.js DSL. Go http://flowchart.js.org or use editors like [Typora](https://support.typora.io/Draw-Diagrams-With-Markdown/#flowcharts) to turns the output code into a rendered diagram.
PyFlowchart will output the generated flowchart.js DSL. Go to http://flowchart.js.org or use editors like [Typora](https://support.typora.io/Draw-Diagrams-With-Markdown/#flowcharts) to turn the output code into a rendered diagram.

To specify a function (or a method in a class) to flowchartlize:

Expand All @@ -48,7 +48,7 @@ PyFlowchart supports [flowchart.js](https://github.com/adrai/flowchart.js#node-t
- SubroutineNode
- EndNode

Nodes can be connected by `connect()` method (`connect_{yes|no}` for ConditionNode).
Nodes can be connected by `connect()` method (`connect_{yes|no}` for ConditionNode). An optional second parameter to `connect()` is used to specify the connect_direction.

Get a Flowchart with your start node and call its `flowchart()` method to generate flowchart.js flowchart DSL:

Expand All @@ -62,14 +62,11 @@ io = InputOutputNode(InputOutputNode.OUTPUT, 'something...')
sub = SubroutineNode('A Subroutine')
e = EndNode('a_pyflow_test')

# define the direction the connection will leave the node from
sub.set_connect_direction("right")

st.connect(op)
op.connect(cond)
cond.connect_yes(io)
cond.connect_no(sub)
sub.connect(op)
sub.connect(op, "right") # sub->op line starts from the right of sub
io.connect(e)

fc = Flowchart(st)
Expand Down Expand Up @@ -102,6 +99,40 @@ Then you can visit http://flowchart.js.org and translate the generated textual r

P.S. Many Markdown editors (for example, Typora) support this flowchart syntax, too (reference: [Typora doc about flowchart](https://support.typora.io/Draw-Diagrams-With-Markdown/#flowcharts)). And if you prefer CLI, see [francoislaberge/diagrams](https://github.com/francoislaberge/diagrams/#flowchart).

### Set Params to Nodes

Since v0.2.0, we support a `Node.set_param(key, value)` method to generate flowchart like this:

```
element(param1=value1,param2=value2)=>start: Start
```

(See also [adrai/flowchart.js#node-specific-specifiers-by-type](https://github.com/adrai/flowchart.js#node-specific-specifiers-by-type))

And for convenience, there are grammar sugars to set param `align-next=no` for ConditionNodes:

```python
cond = ConditionNode("a cond node")
cond.no_align_next()
# or do this at __init__:
cond = ConditionNode("a cond node", align_next=False)
```

This usually works with a connect_direction customization:

```python
cond.connect_yes(op, "right")
```

The generated flowchart will look like:

```
cond(align-next=no)=>condition: Yes or No?
...
cond(yes,right)->op
```

## Python to Flowchart

PyFlowchart can also translate your Python Codes into Flowcharts.
Expand Down Expand Up @@ -146,20 +177,21 @@ Or, in Python
As mentioned above, we use `Flowchart.from_code` to translate Python codes into Flowcharts. The `from_code` is defined as:

```python
Flowchart.from_code(code, field='', inner=True, simplify=True)
Flowchart.from_code(code, field="", inner=True, simplify=True, conds_align=False)
```

PyFlowchart CLI is a 1:1 interface for this function:

```sh
python3 -m pyflowchart [-f FIELD] [-i] [--no-simplify] code_file
python3 -m pyflowchart [-f FIELD] [-i] [--no-simplify] [--conds-align] code_file
```

Let's talk about those three args:

- `field`: str: Specify a field of code to generate a flowchart
- `inner`: bool: `True` to parse the body of field; whereas `False` to parse the body as a single object.
- `simplify`: bool: for If & Loop statements: simplify the one-line-body or not
- `conds_align`: bool: improve the flowchart of *consecutive If statements* converted from python code. (Beta)

### field

Expand Down Expand Up @@ -255,9 +287,26 @@ print(flowchart.flowchart())

![no simplify result](docs/imgs/no-simplify.png)

### conds-align (Beta)

Improve the flowchart of *consecutive If statements* converted from python code with the new feature of `v0.2.0`.

```python
# example-conds-align.py
if cond1:
op1
if cond2:
op2
if cond3:
op3
op_end
```

![conds-align-result](docs/imgs/conds-align.png)

## Beautify Flowcharts

Sometimes, the generated flowchart is awful. In that case, you are encouraged to modify the generated flowchart code by yourself OR consider making your python source code at bottom more clear if it's exceedingly complex.
Sometimes, the generated flowchart is awful. In those cases, you are encouraged to modify the generated flowchart code by yourself OR consider making your python source code at bottom more clear if it's exceedingly complex.

## TODOs

Expand All @@ -274,6 +323,7 @@ Depends on `node.js` and `flowchart.js`.
Well, I guess a **GUI** for PyFlowchart may be remarkable. Pasting your code into it, the flowchart DSL will be generated just in time, and the flowchart will be shown aside.

- [ ] ~~The Chinese README your buddies waiting for!~~ 希望有同学帮助贡献个中文 README 呀。
- [ ] Tests automation.

----

Expand Down
Binary file added docs/imgs/conds-align.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/pyflowchart_illustrations.key
Binary file not shown.
5 changes: 4 additions & 1 deletion pyflowchart.iml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
Expand Down
11 changes: 8 additions & 3 deletions pyflowchart/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,17 @@ def detect_decode(file_content: bytes) -> str:
return content


def main(code_file, field, inner, simplify):
def main(code_file, field, inner, simplify, conds_align):
# read file content: binary
file_content: bytes = code_file.read()
# detect encoding and decode file content by detected encoding
code = detect_decode(file_content)

flowchart = Flowchart.from_code(code, field=field, inner=inner, simplify=simplify)
flowchart = Flowchart.from_code(code,
field=field,
inner=inner,
simplify=simplify,
conds_align=conds_align)
print(flowchart.flowchart())


Expand All @@ -69,10 +73,11 @@ def main(code_file, field, inner, simplify):
parser.add_argument('-f', '--field', default="", type=str, help="field to draw flowchart. (e.g. Class.method)")
parser.add_argument('-i', '--inner', action="store_true", help="parse the body of field")
parser.add_argument('--no-simplify', action="store_false", help="do not simplify the one-line-body If/Loop")
parser.add_argument('--conds-align', action="store_true", help="align consecutive If statements")

args = parser.parse_args()

if not args.field: # field="", parse the whole file (ast Module), should use the body
args.inner = True

main(args.code_file, args.field, args.inner, args.no_simplify)
main(args.code_file, args.field, args.inner, args.no_simplify, args.conds_align)
47 changes: 40 additions & 7 deletions pyflowchart/ast_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ def parse_func_body(self, **kwargs) -> Tuple[Node, List[Node]]:
class LoopCondition(AstConditionNode):
"""a AstConditionNode special for Loop"""

def connect(self, sub_node) -> None:
def connect(self, sub_node, direction='') -> None:
if direction:
self.set_connect_direction(direction)
self.connect_no(sub_node)

def is_one_line_body(self) -> bool:
Expand Down Expand Up @@ -362,6 +364,8 @@ def __init__(self, ast_if: _ast.If, **kwargs):

if kwargs.get("simplify", True):
self.simplify()
if kwargs.get("conds_align", False) and self.cond_node.is_no_else():
self.cond_node.connection_yes.set_connect_direction("right")

def parse_if_body(self, **kwargs) -> None:
"""
Expand Down Expand Up @@ -399,11 +403,11 @@ def parse_else_body(self, **kwargs) -> None:
self.append_tails(virtual_no)

def simplify(self) -> None:
"""
simplify following case:
"""simplify the one-line body case:
if expr:
one_line_body
# no else
before:
... -> If (self, NodesGroup) -> IfCondition('if expr') -> CommonOperation('one_line_body') -> ...
after:
Expand All @@ -425,6 +429,25 @@ def simplify(self) -> None:
except AttributeError as e:
print(e)

def align(self):
"""ConditionNode alignment support #14
if cond1:
op1
if cond2:
op2
if cond3:
op3
op_end
Simplify: add param `align-next=no` to cond1~3, which improves the generated flowchart.
See:
- https://github.com/cdfmlr/pyflowchart/issues/14
- https://github.com/adrai/flowchart.js/issues/221#issuecomment-846919013
- https://github.com/adrai/flowchart.js/issues/115
"""
self.cond_node.no_align_next()


####################
# Common, Call #
Expand Down Expand Up @@ -466,7 +489,7 @@ def __init__(self, ast_break_continue: _ast.stmt, **kwargs): # Break & Continue
AstNode.__init__(self, ast_break_continue, **kwargs)
SubroutineNode.__init__(self, self.ast_to_source())

def connect(self, sub_node) -> None:
def connect(self, sub_node, direction='') -> None:
# a BreakContinueSubroutine should connect to nothing
pass

Expand Down Expand Up @@ -551,7 +574,7 @@ def __init__(self, ast_return: _ast.Return, **kwargs):
# """
# return NodesGroup.fc_connection(self)
#
def connect(self, sub_node) -> None:
def connect(self, sub_node, direction='') -> None:
"""
Return should not be connected with anything
"""
Expand Down Expand Up @@ -605,8 +628,11 @@ def parse(ast_list: List[_ast.AST], **kwargs) -> ParseProcessGraph:
Args:
ast_list: a list of _ast.AST object
**kwargs:
- simplify: for If & Loop: simplify the one line body case
Keyword Args:
* simplify: for If & Loop: simplify the one line body cases
* conds_align: for If: allow the align-next option set for the condition nodes.
See https://github.com/cdfmlr/pyflowchart/issues/14
Returns:
ParseGraph
Expand Down Expand Up @@ -637,6 +663,13 @@ def parse(ast_list: List[_ast.AST], **kwargs) -> ParseProcessGraph:
tail_node = node
else:
tail_node.connect(node)

# ConditionNode alignment support (Issue#14)
# XXX: It's ugly to handle it here. But I have no idea, for this moment, to make it ELEGANT.
if isinstance(tail_node, If) and isinstance(node, If) and \
kwargs.get("conds_align", False):
tail_node.align()

tail_node = node

process.set_head(head_node)
Expand Down
5 changes: 3 additions & 2 deletions pyflowchart/flowchart.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def flowchart(self) -> str:
return self.fc_definition() + '\n' + self.fc_connection()

@staticmethod
def from_code(code: str, field: str = "", inner=True, simplify=True):
def from_code(code: str, field: str = "", inner=True, simplify=True, conds_align=False):
"""
Get a Flowchart instance from a str of Python code.
Expand All @@ -50,6 +50,7 @@ def from_code(code: str, field: str = "", inner=True, simplify=True):
field: str, path to field (function) you want to draw flowchart
inner: bool, True: parse the body of field; Field: parse the body as an object
simplify: bool, for If & Loop statements: simplify the one-line-body or not.
conds_align: bool, for consecutive If statements: conditionNode alignment support (Issue#14) or not
Returns:
A Flowchart instance parsed from given code.
Expand Down Expand Up @@ -98,7 +99,7 @@ def g(self):
assert field_ast.body, f"{field}: nothing to parse. Check given code and field please."

f = field_ast.body if inner else [field_ast]
p = parse(f, simplify=simplify)
p = parse(f, simplify=simplify, conds_align=conds_align)
return Flowchart(p.head)

@staticmethod
Expand Down
Loading

0 comments on commit 94a4150

Please sign in to comment.