Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to edit nodes, edges, etc., after adding them #141

Open
posita opened this issue Aug 21, 2021 · 6 comments
Open

Ability to edit nodes, edges, etc., after adding them #141

posita opened this issue Aug 21, 2021 · 6 comments

Comments

@posita
Copy link

posita commented Aug 21, 2021

PyGraphviz allows for this, but I think leans pretty heavily on the underlying Graphviz native library, which is sometimes a pain to install as a dependency. It would be super neato keen if there was a pure Python version of this functionality.

Architecturally, this would likely be a substantial departure from the current approach. I'm assuming one would want to define and maintain an internal representation of the graph(s), and then translate them to the dot format at write-time (rather than at edit time, which is what appears to be done now). One would have to be careful to ensure that the same set of inputs still resulted in the same dot source each time.

Once such functionality existed, one could write a reader as well, which would support a fairly powerful workflow:

  1. Read a dot file from an external source.
  2. Edit it (add/remove nodes or edges, tweak styles/attrs, etc.).
  3. Create a new dot structure reflecting the edited structure, and optionally render it.
@ntala
Copy link

ntala commented Oct 27, 2021

I really need these functionalities too.
Today, I'm going to use weird workarounds playing with print, source and some ugly string modifications between.
Access to nodes and edges already defined would be a very much cleaner interface.

@xflr6
Copy link
Owner

xflr6 commented Oct 30, 2021

Thanks Matt and ntala. Sorry for taking so long to reply.

About dependencies: One of reason for creating this package was to provide a pure Python way to render graphs: A library that does not depend on non-pure Python dependencies. Such dependencies where non-trivial to get working on some of the target platforms. Back then, wheels where not even around. By now many packages provide binary wheels to ship pre-compiled binaries. So I think this might be less of an issue now. Currently, this package does not have any Python dependency. Only a working installation of Graphviz is required (with the binaries added to the the system PATH, which is done automatically by the installation for some plaforms/distributions but might be challenging for some users to get working). I think it's probably still nice to be self-contained (some worth in keeping install_requires=[]). However, having no Python dependencies might be less of a priority and depend on the features we would like to provide (and whether the installation of the dependency candidate is robust on all platforms).

About architecture: In my view, this library works at the level of DOT source lines/statements (Dot.body). My assumption is that users would typically have some graph-like structure ready and then traverse their objects and add nodes and edges to the graph they want to create from their input data structure. Therefore, we avoid building up let's say a 'clone' of the graph structure that is described by the node-statements and edge-statements: The graphviz.Graph object does not record the parent/child or neighbor relations between the nodes of the graph to render. With this design, there is no node object and therefore no way to walk the graph that is implicit in the statements we output for rendering. If we want to have a possibly mutable rich/nested data struture that represents the graph itself (in the formal sense), then IMHO this is out of scope for this library (there are other libraries for that).

Long story short: Better editable graphviz.dot.Dot subclasses SGTM as long as that feature still works on the level of DOT language statements. I agree that editing via string modifications on the Graph.body list or .source can get cumbersome, which is why we mention that in the docs but don't really recommend it. E.g. we could change Dot.body from a list of lines into a list of richer objects representing DOT source statements before escaping/quoting so users would not need to quote/escape for that use case any more. If that works well, I would also consider parsing DOT code into this data structure (sequence of DOT statements, maybe nesting for subgraphs/clusters). However, that would probably mean adding a dependency for parsing so I would discuss that separately at that point in time.

I think a minimal working version would boil down to recording calls to .node() and .edge(), exposing them to the user for modification, and then actually rendering with the updated calls (deferred/lazy calling). This reminds me of stdlib unittest.mock and its mock.call() type: We could acutally use mocks to create new classes that have the same interface as Graph and Digraph but mock and defer all graph-building method-calls and create a new graph with the recorded mock-calls when building/rendering-methods are called. So this way one can e.g. create a graph template (or prototype) and then create different versions from that by applying different edits to it (currently this would require working on raw .body lines).

I have written a quick proof of concept for this: https://gist.github.com/xflr6/1883d79bbe27b3e75a47916d1ab6b8a7.

Maybe you guys can take a look and let me know if something in this direction would work for you.

Here is a short example:

>>> dot = LazyDigraph(filename='round-table.gv', comment='The Round Table')
>>> dot.node('A', 'King Arthur')
>>> dot.node('B', 'Sir Bedevere the Wise')
>>> dot.node('L', 'Sir Lancelot the Brave')
>>> dot.edges(['AB', 'AL'])
>>> dot.edge('B', 'L', constraint='false')

>>> method_name, tail_head, edge_attrs = dot.mock_calls.pop()
>>> assert method_name == 'edge'
>>> assert tail_head == ('B', 'L')
>>> assert edge_attrs == {'constraint': 'false'}
>>> dot.edge(*reversed(tail_head), **edge_attrs)  # reverse the last edge

>>> print(dot.source)
// The Round Table
digraph {
	A [label="King Arthur"]
	B [label="Sir Bedevere the Wise"]
	L [label="Sir Lancelot the Brave"]
	A -> B
	A -> L
	L -> B [constraint=false]

@xflr6
Copy link
Owner

xflr6 commented Nov 14, 2021

Marking this as enhancement for now (assuming the prototype would satisfy the use case).

Let me know if there is interest in this.

@posita
Copy link
Author

posita commented Nov 14, 2021

Now it's my turn to apologize for the delay in response, and thanks for considering this!

It's hard to tell from the prototype, but would this support reading from an existing file to great the digraph that could then be modified using your mock-based approach? If so, I think you're onto something! 👍

In any event, enhancement seems like the right tag.

@xflr6
Copy link
Owner

xflr6 commented Nov 14, 2021

No worries :)

would this support reading from an existing file [...]?

If the approach replacing Dot.body with a list of statement calls with their attributes works well here for modification, I would also consider parsing DOT code into this data structure. However, because that would probably add a dependency for parsing, I don't want to commit to that at this point: Would the feature be much less useful for you without the ability to parse present DOT code?

@xflr6 xflr6 changed the title Feature request: Consider adding ability to edit nodes, edges, etc., after adding them Ability to edit nodes, edges, etc., after adding them Nov 14, 2021
xflr6 added a commit that referenced this issue Nov 25, 2021
@posita
Copy link
Author

posita commented Dec 10, 2021

Would the feature be much less useful for you without the ability to parse present DOT code?

I can't speak for @ntala, but I think my answer is, "yes". (Although I'm not sure I understand correctly.) My use case involves programmatically reading a .dot file generated by an external tool, deleting entries, then regenerating a visualization reflecting my edits. I hope that's responsive?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants