Skip to content

Commit c8a199a

Browse files
authored
Merge pull request #30 from TheTrueSCU/feature/cyclic-graphs
feat: Support Cyclic Graphs and Refactor Hierarchy
2 parents b0b033f + 80cc6d4 commit c8a199a

56 files changed

Lines changed: 2513 additions & 2394 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ The CLI automatically detects formats based on file extensions:
129129
## Quick Start
130130

131131
```python
132-
from graphable.graph import Graph
132+
from graphable.acyclic_graph import AcyclicGraph as Graph
133133
from graphable.graphable import Graphable
134134
from graphable.views.texttree import create_topology_tree_txt
135135

docs/api.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@ API Reference
66
:undoc-members:
77
:show-inheritance:
88

9-
.. automodule:: graphable.graph
9+
.. automodule:: graphable.acyclic_graph
10+
:members:
11+
:undoc-members:
12+
:show-inheritance:
13+
14+
.. automodule:: graphable.cyclic_graph
15+
:members:
16+
:undoc-members:
17+
:show-inheritance:
18+
19+
.. automodule:: graphable.graph_base
1020
:members:
1121
:undoc-members:
1222
:show-inheritance:

docs/usage.rst

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,43 @@ If your graph contains cycles (which prevents it from being a DAG), ``graphable`
180180
# Returns a list of (source, target) tuples to remove
181181
suggested_breaks = graph.suggest_cycle_breaks()
182182
183+
Cyclic Graphs
184+
^^^^^^^^^^^^^
185+
186+
While the default ``Graph`` class (an alias for ``AcyclicGraph``) strictly enforces acyclicity, you can use the ``CyclicGraph`` class to work with structures that contain loops.
187+
188+
.. code-block:: python
189+
190+
from graphable.cyclic_graph import CyclicGraph
191+
from graphable.graphable import Graphable
192+
193+
a = Graphable("A")
194+
b = Graphable("B")
195+
196+
g = CyclicGraph()
197+
g.add_edge(a, b)
198+
g.add_edge(b, a) # This is allowed in CyclicGraph
199+
200+
**Converting to Acyclic**
201+
202+
If you have a ``CyclicGraph`` and need to perform DAG-specific operations (like topological sorts or CPM), you can convert it to an ``AcyclicGraph`` by breaking cycles:
203+
204+
.. code-block:: python
205+
206+
# Returns a new AcyclicGraph with minimal edge breaks
207+
dag = g.to_acyclic()
208+
209+
**Unified I/O for Cyclic Graphs**
210+
211+
Note that the high-level ``Graph.read()`` method will raise a ``GraphCycleError`` if the input file contains cycles. If you expect your data to be cyclic, use ``CyclicGraph.read()`` instead:
212+
213+
.. code-block:: python
214+
215+
from graphable.cyclic_graph import CyclicGraph
216+
217+
# Correctly handles files with cycles
218+
g = CyclicGraph.read("cyclic_structure.json")
219+
183220
Cycle Detection
184221
^^^^^^^^^^^^^^^
185222

@@ -560,14 +597,13 @@ NetworkX Integration
560597

561598
For users who need advanced graph analysis capabilities, ``graphable`` provides seamless integration with the `NetworkX <https://networkx.org/>`_ library.
562599

563-
If you have ``networkx`` installed, you can convert any ``graphable.Graph`` to a ``networkx.DiGraph`` using the ``to_networkx()`` method:
600+
If you have ``networkx`` installed, you can convert any ``graphable.acyclic_graph.AcyclicGraph`` to a ``networkx.DiGraph`` using the ``to_networkx()`` method:
564601

565602
.. code-block:: python
566603
567604
import networkx as nx
568-
from graphable.graph import Graph
605+
from graphable.acyclic_graph import AcyclicGraph as Graph
569606
from graphable.graphable import Graphable
570-
571607
g = Graph()
572608
# ... build your graph ...
573609

examples/advanced_usage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from graphable.graph import Graph
1+
from graphable.acyclic_graph import AcyclicGraph as Graph
22
from graphable.graphable import Graphable
33
from graphable.views.mermaid import MermaidStylingConfig, create_topology_mermaid_mmd
44

examples/basic_usage.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import sys
33
from pathlib import Path
44

5-
from graphable.graph import Graph
5+
from graphable.acyclic_graph import AcyclicGraph as Graph
66
from graphable.graphable import Graphable
77
from graphable.views.asciiflow import create_topology_ascii_flow
88
from graphable.views.csv import create_topology_csv
@@ -149,12 +149,10 @@ def main():
149149

150150
# Slicing
151151
upstream = g.upstream_of(ui)
152-
print(f"Upstream of React: {[n.reference for n in upstream.topological_order()]}")
152+
print(f"Upstream of React: {[n.reference for n in list(upstream)]}")
153153

154154
between = g.subgraph_between(db, ui)
155-
print(
156-
f"Between Postgres and React: {[n.reference for n in between.topological_order()]}"
157-
)
155+
print(f"Between Postgres and React: {[n.reference for n in list(between)]}")
158156

159157
# Transitive Closure
160158
closure = g.transitive_closure()

examples/cyclic_usage.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from graphable.cyclic_graph import CyclicGraph
2+
from graphable.graphable import Graphable
3+
from graphable.views.mermaid import create_topology_mermaid_mmd
4+
5+
6+
def demo_cyclic_graph():
7+
"""
8+
Demonstrate how to work with Cyclic Graphs in graphable.
9+
"""
10+
print("--- Graphable Cyclic Usage Demo ---")
11+
12+
# 1. Create nodes that form a cycle
13+
# A common real-world example: circular feedback in a system
14+
sensor = Graphable("Sensor")
15+
controller = Graphable("Controller")
16+
actuator = Graphable("Actuator")
17+
18+
# 2. Build the Cyclic Graph
19+
g = CyclicGraph()
20+
21+
g.add_edge(sensor, controller, label="reads")
22+
g.add_edge(controller, actuator, label="commands")
23+
g.add_edge(
24+
actuator, sensor, label="affects"
25+
) # Cycle: Sensor -> Controller -> Actuator -> Sensor
26+
27+
print(f"Graph nodes: {[n.reference for n in g]}")
28+
print(f"Graph is cyclic. Number of nodes: {len(g)}")
29+
30+
# 3. Suggesting cycle breaks
31+
print("\n--- 1. Suggesting Cycle Breaks ---")
32+
breaks = g.suggest_cycle_breaks()
33+
for u, v in breaks:
34+
print(f"Suggested break: {u.reference} -> {v.reference}")
35+
36+
# 4. Converting to Acyclic
37+
print("\n--- 2. Converting to Acyclic Graph ---")
38+
dag = g.to_acyclic()
39+
print(f"DAG nodes: {[n.reference for n in dag]}")
40+
41+
# Now we can perform DAG operations like topological sort
42+
print(f"Topological Order: {[n.reference for n in dag.topological_order()]}")
43+
44+
# 5. Visualization
45+
print("\n--- 3. Mermaid Representation ---")
46+
print(g.render(create_topology_mermaid_mmd))
47+
48+
49+
if __name__ == "__main__":
50+
demo_cyclic_graph()

examples/parser_examples.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from graphable.graph import Graph
1+
from graphable.acyclic_graph import AcyclicGraph as Graph
2+
from graphable.cyclic_graph import CyclicGraph
23
from graphable.views.texttree import create_topology_tree_txt
34

45

@@ -55,12 +56,31 @@ def demo_csv_parsing():
5556
print(g.render(create_topology_tree_txt))
5657

5758

59+
def demo_cyclic_parsing():
60+
print("\n--- Cyclic JSON Parsing ---")
61+
# A cycle: A -> B -> A
62+
json_data = """
63+
{
64+
"nodes": [{"id": "A"}, {"id": "B"}],
65+
"edges": [
66+
{"source": "A", "target": "B"},
67+
{"source": "B", "target": "A"}
68+
]
69+
}
70+
"""
71+
# Note: Using CyclicGraph because Graph (AcyclicGraph) would raise an error
72+
g = CyclicGraph.from_json(json_data)
73+
print(f"Loaded {len(g)} nodes from cyclic JSON string.")
74+
print("Nodes: " + ", ".join([n.reference for n in g]))
75+
76+
5877
def main():
5978
print("Graphable Parser Examples\n")
6079
demo_json_parsing()
6180
demo_yaml_parsing()
6281
demo_toml_parsing()
6382
demo_csv_parsing()
83+
demo_cyclic_parsing()
6484

6585

6686
if __name__ == "__main__":

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "graphable"
3-
version = "0.6.1"
4-
description = "A lightweight, type-safe library for building, managing, and visualizing dependency graphs."
3+
version = "0.7.0"
4+
description = "A lightweight, type-safe library for building, managing, and visualizing dependency graphs (both DAG and cyclic)."
55
readme = "README.md"
66
authors = [
77
{ name = "Richard West", email = "[email protected]" }
@@ -10,7 +10,7 @@ requires-python = ">=3.13"
1010
dependencies = [
1111
"defusedxml>=0.7.1",
1212
]
13-
keywords = ["graph", "dependency-graph", "topological-sort", "dag", "orchestration", "parallel-processing", "caching", "mermaid", "graphviz", "d2", "plantuml", "tikz", "cytoscape", "json", "csv", "networkx", "visualization"]
13+
keywords = ["graph", "dependency-graph", "acyclic-graph", "cyclic-graph", "dag", "orchestration", "parallel-processing", "caching", "mermaid", "graphviz", "d2", "plantuml", "tikz", "cytoscape", "json", "csv", "networkx", "visualization"]
1414
classifiers = [
1515
"Development Status :: 4 - Beta",
1616
"Intended Audience :: Developers",

0 commit comments

Comments
 (0)