2727logger = logging .getLogger (__name__ )
2828
2929
30- # ---------------------------------------------------------------------------
31- # Visualization helper (unrelated to pipeline node types)
32- # ---------------------------------------------------------------------------
33-
34-
35- class VizGraphNode :
36- def __init__ (self , label : str , id : int , kernel_type : str ):
37- self .label = label
38- self .id = id
39- self .kernel_type = kernel_type
40-
41- def __hash__ (self ):
42- return hash ((self .id , self .kernel_type ))
43-
44- def __eq__ (self , other ):
45- if not isinstance (other , VizGraphNode ):
46- return NotImplemented
47- return (self .id , self .kernel_type ) == (
48- other .id ,
49- other .kernel_type ,
50- )
51-
52-
5330# ---------------------------------------------------------------------------
5431# Pipeline
5532# ---------------------------------------------------------------------------
@@ -533,6 +510,19 @@ def _compute_pipeline_snapshot_hash(self) -> str:
533510 combined = "\n " .join (node_lines + edge_lines )
534511 return hashlib .sha256 (combined .encode ()).hexdigest ()[:16 ]
535512
513+ def show_graph (self , ** kwargs ) -> str | None :
514+ """Render the pipeline's node graph.
515+
516+ Args:
517+ **kwargs: Forwarded to ``render_graph``.
518+
519+ Raises:
520+ RuntimeError: If the pipeline has not been compiled yet.
521+ """
522+ if self ._node_graph is None :
523+ raise RuntimeError ("Pipeline must be compiled before showing the graph." )
524+ return render_graph (self ._node_graph , ** kwargs )
525+
536526 def flush (self ) -> None :
537527 """Flush all databases."""
538528 self ._pipeline_database .flush ()
@@ -806,8 +796,7 @@ def load(cls, path: str | Path, mode: str = "full") -> "Pipeline":
806796 all_upstreams_usable = (
807797 all (
808798 hasattr (n , "load_status" )
809- and n .load_status
810- in (LoadStatus .FULL , LoadStatus .READ_ONLY )
799+ and n .load_status in (LoadStatus .FULL , LoadStatus .READ_ONLY )
811800 for n in upstream_nodes
812801 )
813802 if upstream_nodes
@@ -1141,7 +1130,7 @@ class GraphRenderer:
11411130 "style" : "filled" ,
11421131 "typefontcolor" : "lightgray" , # Light text for dark background
11431132 },
1144- "pod " : {
1133+ "function " : {
11451134 "fillcolor" : "#090271" , # darker navy blue
11461135 "shape" : "cylinder" ,
11471136 "fontcolor" : "white" ,
@@ -1153,26 +1142,22 @@ class GraphRenderer:
11531142 def __init__ (self ):
11541143 pass
11551144
1156- def _sanitize_node_id (self , node_id : Any ) -> str :
1145+ def _sanitize_node_id (self , node_id : GraphNode ) -> str :
11571146 return f"node_{ hash (node_id )} "
11581147
1159- def _create_default_html_label (self , node , node_attrs ) -> str :
1160- """
1161- Create HTML for the label (text) section of the node
1148+ def _create_default_html_label (
1149+ self , node : GraphNode , node_attrs : dict [str , str ]
1150+ ) -> str :
1151+ """Create HTML for the label (text) section of the node.
11621152
11631153 Format:
1164- kernel_type (11pt, small text)
1154+ node_type (11pt, small text)
11651155 main_label (14pt, normal text)
11661156 """
1157+ main_label = str (node .label )
1158+ node_type = node .node_type
11671159
1168- main_label = str (node .label ) if hasattr (node , "label" ) else str (node )
1169- kernel_type = str (node .kernel_type ) if hasattr (node , "kernel_type" ) else ""
1170-
1171- if not kernel_type :
1172- # No kernel_type, just return main label
1173- return f'<FONT POINT-SIZE="{ self .DEFAULT_STYLES ["main_font_size" ]} ">{ main_label } </FONT>'
1174-
1175- # Create HTML label: small kernel_type above, main label below
1160+ # Create HTML label: small node_type above, main label below
11761161 main_size = self .DEFAULT_STYLES ["main_font_size" ]
11771162 type_size = self .DEFAULT_STYLES ["type_font_size" ]
11781163 font_name = self .DEFAULT_STYLES ["font_name" ]
@@ -1182,30 +1167,30 @@ def _create_default_html_label(self, node, node_attrs) -> str:
11821167
11831168 html_label = f'''<
11841169 <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
1185- <TR><TD ALIGN="CENTER"><FONT POINT-SIZE="{ type_size } " COLOR="{ type_font_color } " FACE="{ font_name } , bold">{ kernel_type } </FONT></TD></TR>
1170+ <TR><TD ALIGN="CENTER"><FONT POINT-SIZE="{ type_size } " COLOR="{ type_font_color } " FACE="{ font_name } , bold">{ node_type } </FONT></TD></TR>
11861171 <TR><TD ALIGN="CENTER"><FONT POINT-SIZE="{ main_size } ">{ main_label } </FONT></TD></TR>
11871172 </TABLE>
11881173 >'''
11891174
11901175 return html_label
11911176
11921177 def _get_node_label (
1193- self , node_id : Any , label_lut : dict [Any , str ] | None = None
1178+ self ,
1179+ node : GraphNode ,
1180+ label_lut : dict [GraphNode , str ] | None = None ,
11941181 ) -> str :
1195- if label_lut and node_id in label_lut :
1196- return label_lut [node_id ]
1197- return str (node_id )
1182+ if label_lut and node in label_lut :
1183+ return label_lut [node ]
1184+ return str (node . label )
11981185
11991186 def _get_node_attributes (
1200- self , node_id : Any , style_rules : dict | None = None
1187+ self ,
1188+ node : GraphNode ,
1189+ style_rules : dict [str , dict [str , str ]] | None = None ,
12011190 ) -> dict [str , str ]:
1202- """
1203- Get styling attributes for a specific node based on its properties
1204- """
1205- # Use provided rules or defaults
1191+ """Get styling attributes for a specific node based on its node_type."""
12061192 rules = style_rules or self .DEFAULT_STYLE_RULES
12071193
1208- # Default attributes
12091194 default_attrs = {
12101195 "fillcolor" : self .DEFAULT_STYLES ["node_color" ],
12111196 "shape" : self .DEFAULT_STYLES ["node_shape" ],
@@ -1216,13 +1201,8 @@ def _get_node_attributes(
12161201 "typefontcolor" : self .DEFAULT_STYLES ["type_font_color" ],
12171202 }
12181203
1219- # Check if node has kernel_type attribute
1220- if hasattr (node_id , "kernel_type" ):
1221- kernel_type = node_id .kernel_type
1222- if kernel_type in rules :
1223- # Override defaults with rule-specific attributes
1224- rule_attrs = rules [kernel_type ].copy ()
1225- default_attrs .update (rule_attrs )
1204+ if node .node_type in rules :
1205+ default_attrs .update (rules [node .node_type ])
12261206
12271207 return default_attrs
12281208
@@ -1238,11 +1218,10 @@ def _merge_styles(self, **override_styles) -> dict:
12381218 def generate_dot (
12391219 self ,
12401220 graph : "nx.DiGraph" ,
1241- label_lut : dict [Any , str ] | None = None ,
1242- style_rules : dict | None = None ,
1221+ label_lut : dict [GraphNode , str ] | None = None ,
1222+ style_rules : dict [ str , dict [ str , str ]] | None = None ,
12431223 ** style_overrides ,
12441224 ) -> str :
1245- # Get final styles (defaults + overrides)
12461225 styles = self ._merge_styles (** style_overrides )
12471226
12481227 import graphviz
@@ -1252,28 +1231,24 @@ def generate_dot(
12521231 # Apply global styles
12531232 dot .attr (rankdir = styles ["rankdir" ], dpi = str (styles ["dpi" ]))
12541233 dot .attr (fontname = styles ["font_name" ])
1255- if styles .get ("font_size " ):
1256- dot .attr (fontsize = styles ["fontsize" ])
1234+ if styles .get ("fontsize " ):
1235+ dot .attr (fontsize = str ( styles ["fontsize" ]) )
12571236 if styles ["font_path" ]:
12581237 dot .attr (fontpath = styles ["font_path" ])
12591238
12601239 # Set default edge attributes
12611240 dot .attr ("edge" , color = styles ["edge_color" ])
12621241
1263- # Add nodes with default attribute specific styling
1264- for node_id in graph .nodes ():
1265- sanitized_id = self ._sanitize_node_id (node_id )
1242+ # Add nodes with styling based on node_type
1243+ for node in graph .nodes ():
1244+ sanitized_id = self ._sanitize_node_id (node )
1245+ node_attrs = self ._get_node_attributes (node , style_rules )
12661246
1267- node_attrs = self ._get_node_attributes (node_id , style_rules )
1268-
1269- if label_lut and node_id in label_lut :
1270- # Use custom label if provided
1271- label = label_lut [node_id ]
1247+ if label_lut and node in label_lut :
1248+ label = label_lut [node ]
12721249 else :
1273- # Use default HTML label with kernel_type above main label
1274- label = self ._create_default_html_label (node_id , node_attrs )
1250+ label = self ._create_default_html_label (node , node_attrs )
12751251
1276- # Add nodes with its specific attributes
12771252 dot .node (sanitized_id , label = label , ** node_attrs )
12781253
12791254 # Add edges
@@ -1287,13 +1262,13 @@ def generate_dot(
12871262 def render_graph (
12881263 self ,
12891264 graph : "nx.DiGraph" ,
1290- label_lut : dict [Any , str ] | None = None ,
1265+ label_lut : dict [GraphNode , str ] | None = None ,
12911266 show : bool = True ,
12921267 output_path : str | None = None ,
12931268 raw_output : bool = False ,
12941269 figsize : tuple = (12 , 8 ),
12951270 dpi : int = 150 ,
1296- style_rules : dict | None = None ,
1271+ style_rules : dict [ str , dict [ str , str ]] | None = None ,
12971272 ** style_overrides ,
12981273 ) -> str | None :
12991274 # Always generate DOT first
@@ -1321,14 +1296,14 @@ def render_graph(
13211296 dot .attr ("edge" , color = styles ["edge_color" ])
13221297
13231298 # Add nodes with specific styling
1324- for node_id in graph .nodes ():
1325- sanitized_id = self ._sanitize_node_id (node_id )
1326- node_attrs = self ._get_node_attributes (node_id , style_rules )
1299+ for node in graph .nodes ():
1300+ sanitized_id = self ._sanitize_node_id (node )
1301+ node_attrs = self ._get_node_attributes (node , style_rules )
13271302
1328- if label_lut and node_id in label_lut :
1329- label = label_lut [node_id ]
1303+ if label_lut and node in label_lut :
1304+ label = label_lut [node ]
13301305 else :
1331- label = self ._create_default_html_label (node_id , node_attrs )
1306+ label = self ._create_default_html_label (node , node_attrs )
13321307
13331308 dot .node (sanitized_id , label = label , ** node_attrs )
13341309
@@ -1367,30 +1342,28 @@ def render_graph(
13671342# =====================
13681343def render_graph (
13691344 graph : "nx.DiGraph" ,
1370- label_lut : dict [Any , str ] | None = None ,
1371- style_rules : dict | None = None ,
1345+ label_lut : dict [GraphNode , str ] | None = None ,
1346+ style_rules : dict [ str , dict [ str , str ]] | None = None ,
13721347 ** kwargs ,
13731348) -> str | None :
1374- """
1375- Convenience function with conditional node styling
1349+ """Convenience function with conditional node styling.
13761350
13771351 Args:
1378- graph: NetworkX DiGraph
1379- label_lut: Optional node labels
1380- style_rules: Dict mapping node attributes to styling rules
1381- **kwargs: Other styling arguments
1352+ graph: NetworkX DiGraph whose nodes are GraphNode instances.
1353+ label_lut: Optional mapping from node to custom display label.
1354+ style_rules: Mapping from node_type to graphviz attribute overrides.
1355+ **kwargs: Other styling arguments forwarded to GraphRenderer.
13821356 """
13831357 renderer = GraphRenderer ()
13841358 return renderer .render_graph (graph , label_lut , style_rules = style_rules , ** kwargs )
13851359
13861360
13871361def render_graph_dark_theme (
1388- graph : "nx.DiGraph" , label_lut : dict [Any , str ] | None = None , ** kwargs
1362+ graph : "nx.DiGraph" ,
1363+ label_lut : dict [GraphNode , str ] | None = None ,
1364+ ** kwargs ,
13891365) -> str | None :
1390- """
1391- Render with dark theme - all backgrounds dark, all pod type fonts light
1392- Perfect for dark themed presentations or displays
1393- """
1366+ """Render with dark theme — dark backgrounds, light fonts."""
13941367 renderer = GraphRenderer ()
13951368 return renderer .render_graph (
13961369 graph , label_lut , style_rules = renderer .DARK_THEME_RULES , ** kwargs
@@ -1425,29 +1398,29 @@ def create_custom_rules(
14251398 pod_main_fcolor = "white" ,
14261399 source_type_fcolor = "darkgray" ,
14271400 operator_type_fcolor = "darkgray" ,
1428- kernel_type_fcolor = "lightgray" ,
1429- ):
1430- """Create custom theme rules"""
1401+ node_type_fcolor = "lightgray" ,
1402+ ) -> dict [ str , dict [ str , str ]] :
1403+ """Create custom theme rules. """
14311404 return {
14321405 "source" : {
14331406 "fillcolor" : source_bg ,
14341407 "shape" : "ellipse" ,
14351408 "fontcolor" : source_main_fcolor ,
14361409 "style" : "filled" ,
1437- "type_font_color " : source_type_fcolor ,
1410+ "typefontcolor " : source_type_fcolor ,
14381411 },
14391412 "operator" : {
14401413 "fillcolor" : operator_bg ,
14411414 "shape" : "diamond" ,
14421415 "fontcolor" : operator_main_fcolor ,
14431416 "style" : "filled" ,
1444- "type_font_color " : operator_type_fcolor ,
1417+ "typefontcolor " : operator_type_fcolor ,
14451418 },
14461419 "function" : {
14471420 "fillcolor" : pod_bg ,
14481421 "shape" : "box" ,
14491422 "fontcolor" : pod_main_fcolor ,
14501423 "style" : "filled,rounded" ,
1451- "type_font_color " : kernel_type_fcolor ,
1424+ "typefontcolor " : node_type_fcolor ,
14521425 },
14531426 }
0 commit comments