py-flow-mapper 0.1.0b7.dev0__tar.gz → 0.1.0b9.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {py_flow_mapper-0.1.0b7.dev0/src/py_flow_mapper.egg-info → py_flow_mapper-0.1.0b9.dev0}/PKG-INFO +1 -1
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/setup.py +1 -1
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/__init__.py +1 -1
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/analyzer.py +129 -15
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/mermaid_generator.py +120 -42
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0/src/py_flow_mapper.egg-info}/PKG-INFO +1 -1
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/LICENSE +0 -0
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/README.md +0 -0
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/setup.cfg +0 -0
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/cli.py +0 -0
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/utils.py +0 -0
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper.egg-info/SOURCES.txt +0 -0
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper.egg-info/dependency_links.txt +0 -0
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper.egg-info/entry_points.txt +0 -0
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper.egg-info/requires.txt +0 -0
- {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper.egg-info/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="py-flow-mapper",
|
|
8
|
-
version="0.1.
|
|
8
|
+
version="0.1.0b9.dev",
|
|
9
9
|
author="Arun Koundinya Parasa",
|
|
10
10
|
author_email="parasa.arunkoundinya@gmail.com",
|
|
11
11
|
description="Python project analyzer and visualization tool",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
PyFlowMapper - A Python project analyzer and visualization tool.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__version__ = "0.1.
|
|
5
|
+
__version__ = "0.1.0b9.dev"
|
|
6
6
|
__author__ = "Arun Koundinya Parasa"
|
|
7
7
|
__description__ = "Analyze Python projects and generate dependency graphs"
|
|
8
8
|
__github__ = "https://github.com/ArunKoundinya/py-flow-mapper"
|
|
@@ -32,6 +32,7 @@ class FunctionInfo:
|
|
|
32
32
|
docstring: Optional[str] = None
|
|
33
33
|
return_assignments: Dict[str, List[str]] = field(default_factory=dict) # {var_name: [called_functions]}
|
|
34
34
|
call_arguments: Dict[str, List[str]] = field(default_factory=dict)
|
|
35
|
+
call_edges: List[Tuple[str, str, str]] = field(default_factory=list)
|
|
35
36
|
|
|
36
37
|
def __post_init__(self):
|
|
37
38
|
if self.decorators is None:
|
|
@@ -118,7 +119,7 @@ class ProjectAnalyzer:
|
|
|
118
119
|
"site-packages", "node_modules", "bower_components",
|
|
119
120
|
"docs", "doc", "site", "_site", "mkdocs",
|
|
120
121
|
".ipynb_checkpoints", "notebooks", "examples", "experiments", "scratch",
|
|
121
|
-
"
|
|
122
|
+
"datasets", "models", "checkpoints", "outputs", "results",
|
|
122
123
|
"logs", "tmp", "temp", "cache",
|
|
123
124
|
".idea", ".vscode", ".DS_Store", "__MACOSX",
|
|
124
125
|
".github", ".gitlab", ".circleci",
|
|
@@ -291,7 +292,8 @@ class ProjectAnalyzer:
|
|
|
291
292
|
'call_arguments': getattr(func, "call_arguments", {}),
|
|
292
293
|
'module': func.module,
|
|
293
294
|
'file_path': func.file_path,
|
|
294
|
-
'lineno': func.lineno
|
|
295
|
+
'lineno': func.lineno,
|
|
296
|
+
'call_edges': getattr(func, "call_edges", []),
|
|
295
297
|
}
|
|
296
298
|
|
|
297
299
|
# Data flow edges
|
|
@@ -304,6 +306,16 @@ class ProjectAnalyzer:
|
|
|
304
306
|
}
|
|
305
307
|
for edge in self.data_flow_edges
|
|
306
308
|
]
|
|
309
|
+
|
|
310
|
+
call_edges = []
|
|
311
|
+
seen = set()
|
|
312
|
+
for _, func in self.function_map.items():
|
|
313
|
+
for src, tgt, mod in getattr(func, "call_edges", []):
|
|
314
|
+
k = (src, tgt, mod)
|
|
315
|
+
if k in seen:
|
|
316
|
+
continue
|
|
317
|
+
seen.add(k)
|
|
318
|
+
call_edges.append({"source": src, "target": tgt, "module": mod})
|
|
307
319
|
|
|
308
320
|
metadata = {
|
|
309
321
|
'project': {
|
|
@@ -321,6 +333,7 @@ class ProjectAnalyzer:
|
|
|
321
333
|
},
|
|
322
334
|
'function_map': function_map,
|
|
323
335
|
'data_flow_edges': data_flow_edges,
|
|
336
|
+
'call_edges': call_edges,
|
|
324
337
|
'call_graph': self._generate_call_graph_data(),
|
|
325
338
|
'import_graph': self._generate_import_graph_data()
|
|
326
339
|
}
|
|
@@ -429,6 +442,7 @@ class ModuleVisitor(ast.NodeVisitor):
|
|
|
429
442
|
decorators=decorators,
|
|
430
443
|
docstring=docstring,
|
|
431
444
|
call_arguments=flow_analyzer.call_arguments,
|
|
445
|
+
call_edges=flow_analyzer.call_edges,
|
|
432
446
|
)
|
|
433
447
|
|
|
434
448
|
# Store return assignments for data flow analysis
|
|
@@ -518,43 +532,143 @@ class DataFlowAnalyzer(ast.NodeVisitor):
|
|
|
518
532
|
self.return_assignments: Dict[str, List[str]] = {} # var_name -> [called_functions]
|
|
519
533
|
self.current_assignment = None
|
|
520
534
|
self.call_arguments: Dict[str, List[str]] = {}
|
|
535
|
+
self.call_edges: List[Tuple[str, str, str]] = []
|
|
521
536
|
|
|
522
537
|
def visit_Assign(self, node):
|
|
523
538
|
"""Track assignments of function return values."""
|
|
524
|
-
|
|
525
|
-
if isinstance(node.targets[0], ast.Name):
|
|
539
|
+
if node.targets and isinstance(node.targets[0], ast.Name):
|
|
526
540
|
var_name = node.targets[0].id
|
|
527
|
-
|
|
528
|
-
# Check if the value is a function call
|
|
541
|
+
|
|
529
542
|
if isinstance(node.value, ast.Call):
|
|
530
543
|
call_info = self._extract_call_info(node.value)
|
|
531
544
|
if call_info:
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
545
|
+
func_name = call_info["func_name"]
|
|
546
|
+
|
|
547
|
+
if func_name not in self.calls:
|
|
548
|
+
self.calls.append(func_name)
|
|
549
|
+
self.return_assignments.setdefault(var_name, []).append(func_name)
|
|
550
|
+
|
|
538
551
|
elif isinstance(node.value, ast.Name):
|
|
539
|
-
# Track variable assignments for data flow
|
|
540
552
|
pass
|
|
541
|
-
|
|
553
|
+
|
|
542
554
|
self.generic_visit(node)
|
|
543
555
|
|
|
544
556
|
def visit_Call(self, node):
|
|
545
557
|
call_info = self._extract_call_info(node)
|
|
546
558
|
if call_info:
|
|
547
559
|
func_name = call_info['func_name']
|
|
548
|
-
|
|
560
|
+
## Avoid Duplicates
|
|
561
|
+
if func_name not in self.calls:
|
|
562
|
+
self.calls.append(func_name)
|
|
549
563
|
|
|
550
564
|
arg_vars = []
|
|
551
565
|
for arg in node.args:
|
|
552
566
|
if isinstance(arg, ast.Name):
|
|
553
567
|
arg_vars.append(arg.id)
|
|
554
568
|
|
|
569
|
+
elif isinstance(arg, ast.Constant):
|
|
570
|
+
## Added handling for string constants
|
|
571
|
+
val = repr(arg.value) if isinstance(arg.value, str) else str(arg.value)
|
|
572
|
+
arg_vars.append(val)
|
|
573
|
+
|
|
555
574
|
if arg_vars:
|
|
575
|
+
# Record positional arguments
|
|
556
576
|
self.call_arguments.setdefault(func_name, []).extend(arg_vars)
|
|
557
577
|
|
|
578
|
+
## Adding New Block for Keyword Arguments
|
|
579
|
+
for kwarg in node.keywords:
|
|
580
|
+
# Keyword arguments
|
|
581
|
+
if kwarg.arg is not None:
|
|
582
|
+
# Regular kwarg case
|
|
583
|
+
key = kwarg.arg
|
|
584
|
+
if isinstance(kwarg.value, ast.Name):
|
|
585
|
+
val = kwarg.value.id
|
|
586
|
+
|
|
587
|
+
elif isinstance(kwarg.value, ast.Constant):
|
|
588
|
+
val = repr(kwarg.value.value) if isinstance(kwarg.value.value, str) else str(kwarg.value.value)
|
|
589
|
+
|
|
590
|
+
elif isinstance(kwarg.value, ast.List):
|
|
591
|
+
items = []
|
|
592
|
+
mappings = [] # <-- NEW: for ColumnTransformer-style mapping
|
|
593
|
+
payloads = []
|
|
594
|
+
if kwarg.value.elts and all(isinstance(e, ast.Tuple) and len(e.elts) >= 2 for e in kwarg.value.elts):
|
|
595
|
+
for e in kwarg.value.elts:
|
|
596
|
+
payload = e.elts[1]
|
|
597
|
+
if isinstance(payload, ast.Name):
|
|
598
|
+
payloads.append(payload.id)
|
|
599
|
+
|
|
600
|
+
# If we successfully extracted payload names, store them and skip pretty string for this kwarg
|
|
601
|
+
if payloads:
|
|
602
|
+
self.call_arguments.setdefault(func_name, []).extend(payloads)
|
|
603
|
+
continue
|
|
604
|
+
|
|
605
|
+
for elt in kwarg.value.elts:
|
|
606
|
+
if isinstance(elt, ast.Tuple) and len(elt.elts) >= 3:
|
|
607
|
+
transformer_node = elt.elts[1]
|
|
608
|
+
cols_node = elt.elts[2]
|
|
609
|
+
|
|
610
|
+
transformer_name = None
|
|
611
|
+
cols_name = None
|
|
612
|
+
|
|
613
|
+
if isinstance(transformer_node, ast.Call):
|
|
614
|
+
ci = self._extract_call_info(transformer_node)
|
|
615
|
+
transformer_name = ci["func_name"] if ci else "<Call>"
|
|
616
|
+
|
|
617
|
+
if isinstance(cols_node, ast.Name):
|
|
618
|
+
cols_name = cols_node.id
|
|
619
|
+
elif isinstance(cols_node, ast.Constant):
|
|
620
|
+
cols_name = repr(cols_node.value) if isinstance(cols_node.value, str) else str(cols_node.value)
|
|
621
|
+
|
|
622
|
+
if transformer_name and cols_name:
|
|
623
|
+
mappings.append({transformer_name: cols_name})
|
|
624
|
+
self.call_edges.append((func_name, transformer_name, self.module_name))
|
|
625
|
+
|
|
626
|
+
# ---- your existing pretty rendering ----
|
|
627
|
+
if isinstance(elt, ast.Tuple):
|
|
628
|
+
parts = []
|
|
629
|
+
for t in elt.elts:
|
|
630
|
+
if isinstance(t, ast.Name):
|
|
631
|
+
parts.append(t.id)
|
|
632
|
+
elif isinstance(t, ast.Constant):
|
|
633
|
+
parts.append(repr(t.value) if isinstance(t.value, str) else str(t.value))
|
|
634
|
+
elif isinstance(t, ast.Call):
|
|
635
|
+
ci = self._extract_call_info(t)
|
|
636
|
+
parts.append(ci["func_name"] if ci else "<Call>")
|
|
637
|
+
else:
|
|
638
|
+
parts.append(f"<{t.__class__.__name__}>")
|
|
639
|
+
|
|
640
|
+
items.append("(" + ", ".join(parts) + ")")
|
|
641
|
+
elif isinstance(elt, ast.Name):
|
|
642
|
+
items.append(elt.id)
|
|
643
|
+
elif isinstance(elt, ast.Constant):
|
|
644
|
+
items.append(repr(elt.value) if isinstance(elt.value, str) else str(elt.value))
|
|
645
|
+
else:
|
|
646
|
+
items.append(f"<{elt.__class__.__name__}>")
|
|
647
|
+
|
|
648
|
+
val = "[" + ", ".join(items) + "]"
|
|
649
|
+
|
|
650
|
+
# Store BOTH mapping dict(s) and the pretty string, under the same outer function call
|
|
651
|
+
if mappings:
|
|
652
|
+
self.call_arguments.setdefault(func_name, []).extend(mappings)
|
|
653
|
+
self.call_arguments.setdefault(func_name, []).append(f"{key}={val}")
|
|
654
|
+
continue
|
|
655
|
+
|
|
656
|
+
else:
|
|
657
|
+
val = f"<{kwarg.value.__class__.__name__}>"
|
|
658
|
+
|
|
659
|
+
self.call_arguments.setdefault(func_name, []).append(f"{key}={val}")
|
|
660
|
+
|
|
661
|
+
else:
|
|
662
|
+
# **kwargs case
|
|
663
|
+
if isinstance(kwarg.value, ast.Name):
|
|
664
|
+
self.call_arguments.setdefault(func_name, []).append(f"**{kwarg.value.id}")
|
|
665
|
+
elif isinstance(kwarg.value, ast.Constant):
|
|
666
|
+
val = repr(kwarg.value.value) if isinstance(kwarg.value.value, str) else str(kwarg.value.value)
|
|
667
|
+
self.call_arguments.setdefault(func_name, []).append(f"**={val}")
|
|
668
|
+
|
|
669
|
+
else:
|
|
670
|
+
self.call_arguments.setdefault(func_name, []).append(f"**=<${kwarg.value.__class__.__name__}>")
|
|
671
|
+
|
|
558
672
|
self.generic_visit(node)
|
|
559
673
|
|
|
560
674
|
|
{py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/mermaid_generator.py
RENAMED
|
@@ -71,23 +71,16 @@ class MermaidGenerator:
|
|
|
71
71
|
return ""
|
|
72
72
|
return ",".join(sorted(set(cleaned)))
|
|
73
73
|
|
|
74
|
-
current_module_ctx = ""
|
|
75
74
|
|
|
76
|
-
def module_import_mapping() -> dict:
|
|
77
|
-
return (modules.get(
|
|
75
|
+
def module_import_mapping(mod: str) -> dict:
|
|
76
|
+
return (modules.get(mod, {}) or {}).get("import_mapping", {}) or {}
|
|
78
77
|
|
|
79
|
-
def external_root_name(call_name: str) -> str:
|
|
78
|
+
def external_root_name(call_name: str, current_module: str) -> str:
|
|
80
79
|
if not call_name:
|
|
81
80
|
return ""
|
|
82
|
-
|
|
83
81
|
root = call_name.split(".")[0]
|
|
84
|
-
|
|
85
|
-
# module-specific import mapping (from analyzer)
|
|
86
|
-
imp_map = module_import_mapping() or {}
|
|
87
|
-
|
|
88
|
-
# merge common aliases (module imports override defaults)
|
|
82
|
+
imp_map = module_import_mapping(current_module)
|
|
89
83
|
merged_map = {**COMMON_ALIAS_MAP, **imp_map}
|
|
90
|
-
|
|
91
84
|
return merged_map.get(root, root)
|
|
92
85
|
|
|
93
86
|
# Keep the graph clean: ignore common builtins + attribute-noise
|
|
@@ -99,52 +92,74 @@ class MermaidGenerator:
|
|
|
99
92
|
"Path",
|
|
100
93
|
}
|
|
101
94
|
|
|
102
|
-
def keep_external(call_name: str) -> bool:
|
|
95
|
+
def keep_external(call_name: str, current_module: str) -> bool:
|
|
103
96
|
if not call_name:
|
|
104
97
|
return False
|
|
105
98
|
|
|
106
99
|
base = short_label(call_name)
|
|
107
100
|
|
|
108
|
-
|
|
109
|
-
root = external_root_name(call_name)
|
|
101
|
+
root = external_root_name(call_name, current_module)
|
|
110
102
|
if root and root in self.force_external:
|
|
111
103
|
return True
|
|
112
104
|
if base in self.force_external:
|
|
113
105
|
return True
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
imp_map = module_import_mapping() or {}
|
|
107
|
+
imp_map = module_import_mapping(current_module)
|
|
117
108
|
if base in imp_map:
|
|
118
109
|
mapped = imp_map[base]
|
|
119
110
|
mapped_module = ".".join(mapped.split(".")[:-1])
|
|
120
|
-
|
|
121
111
|
if mapped_module in modules:
|
|
122
112
|
return False
|
|
123
|
-
|
|
124
113
|
return True
|
|
125
114
|
|
|
126
|
-
# internal class => NOT external
|
|
127
115
|
if base in internal_classes:
|
|
128
116
|
return False
|
|
129
117
|
|
|
130
118
|
if base in NOISY_EXTERNAL:
|
|
131
119
|
return False
|
|
132
120
|
|
|
133
|
-
# dotted calls like obj.method: too noisy at this level
|
|
134
121
|
if "." in call_name:
|
|
135
122
|
return False
|
|
136
123
|
|
|
137
|
-
# keep CamelCase (tool-ish classes / constructors)
|
|
138
124
|
return is_camel_case(base)
|
|
139
125
|
|
|
140
126
|
def resolve_internal(call: str, current_module: str) -> str:
|
|
141
|
-
|
|
127
|
+
call = call or ""
|
|
128
|
+
|
|
129
|
+
# 1) existing resolver
|
|
142
130
|
target = self._find_function_full_name(call, current_module)
|
|
143
131
|
if target and target in internal_funcs:
|
|
144
132
|
return target
|
|
145
133
|
|
|
146
|
-
|
|
147
|
-
|
|
134
|
+
imp_map = module_import_mapping(current_module)
|
|
135
|
+
|
|
136
|
+
# 2) If call is a bare name imported via "from X import name"
|
|
137
|
+
# import_mapping might contain: name -> "pkg.mod.name"
|
|
138
|
+
if call in imp_map:
|
|
139
|
+
mapped = imp_map[call]
|
|
140
|
+
if mapped in internal_funcs:
|
|
141
|
+
return mapped
|
|
142
|
+
# if mapped is "pkg.mod.name", try resolving it (or its tail)
|
|
143
|
+
target = self._find_function_full_name(mapped, current_module)
|
|
144
|
+
if target and target in internal_funcs:
|
|
145
|
+
return target
|
|
146
|
+
|
|
147
|
+
# 3) If call is "alias.something", rewrite alias using import_mapping
|
|
148
|
+
# import_mapping might contain: alias -> "pkg.mod"
|
|
149
|
+
if "." in call:
|
|
150
|
+
root, rest = call.split(".", 1)
|
|
151
|
+
if root in imp_map:
|
|
152
|
+
mapped_root = imp_map[root]
|
|
153
|
+
# If mapping is to a specific object like "pkg.mod.func",
|
|
154
|
+
# treat it as root too (best-effort)
|
|
155
|
+
candidate = f"{mapped_root}.{rest}"
|
|
156
|
+
if candidate in internal_funcs:
|
|
157
|
+
return candidate
|
|
158
|
+
target = self._find_function_full_name(candidate, current_module)
|
|
159
|
+
if target and target in internal_funcs:
|
|
160
|
+
return target
|
|
161
|
+
|
|
162
|
+
# 4) your existing "obj.method" heuristic
|
|
148
163
|
method = call.split(".")[-1]
|
|
149
164
|
matches = [k for k in internal_funcs if k.endswith("." + method)]
|
|
150
165
|
if len(matches) == 1:
|
|
@@ -159,7 +174,7 @@ class MermaidGenerator:
|
|
|
159
174
|
|
|
160
175
|
def is_fileish_arg(arg: str) -> bool:
|
|
161
176
|
a = (arg or "").lower()
|
|
162
|
-
return any(k in a for k in ("meta", "json", "yaml", "yml", "config", "
|
|
177
|
+
return any(k in a for k in ("meta", "json", "yaml", "yml", "config", "file", ".json", ".yaml", ".yml", ".cfg", ".ini", ".txt", ".csv"))
|
|
163
178
|
|
|
164
179
|
def ordered_functions_from_entry(entry_key: str) -> list[str]:
|
|
165
180
|
"""DFS from entry, preserving call order from metadata."""
|
|
@@ -174,7 +189,7 @@ class MermaidGenerator:
|
|
|
174
189
|
|
|
175
190
|
info = function_map.get(fn_key, {})
|
|
176
191
|
current_module = info.get("module", "") or ""
|
|
177
|
-
current_module_ctx = current_module
|
|
192
|
+
#current_module_ctx = current_module
|
|
178
193
|
for c in (info.get("calls") or []):
|
|
179
194
|
target = self._find_function_full_name(c, current_module)
|
|
180
195
|
if target and target in function_map:
|
|
@@ -210,15 +225,15 @@ class MermaidGenerator:
|
|
|
210
225
|
|
|
211
226
|
for _, info in function_map.items():
|
|
212
227
|
current_module = info.get("module", "") or ""
|
|
213
|
-
current_module_ctx = current_module
|
|
228
|
+
#current_module_ctx = current_module
|
|
214
229
|
|
|
215
230
|
# prefer call_arguments keys
|
|
216
231
|
call_args = info.get("call_arguments", {}) or {}
|
|
217
232
|
for callee in call_args.keys():
|
|
218
233
|
if resolve_internal(callee, current_module):
|
|
219
234
|
continue
|
|
220
|
-
if keep_external(callee):
|
|
221
|
-
external_nodes.add(external_root_name(callee) or callee)
|
|
235
|
+
if keep_external(callee, current_module):
|
|
236
|
+
external_nodes.add(external_root_name(callee, current_module) or callee)
|
|
222
237
|
|
|
223
238
|
# scan raw calls for Done + missed externals
|
|
224
239
|
for callee in (info.get("calls") or []):
|
|
@@ -227,8 +242,8 @@ class MermaidGenerator:
|
|
|
227
242
|
|
|
228
243
|
if resolve_internal(callee, current_module):
|
|
229
244
|
continue
|
|
230
|
-
if keep_external(callee):
|
|
231
|
-
external_nodes.add(external_root_name(callee) or callee)
|
|
245
|
+
if keep_external(callee, current_module):
|
|
246
|
+
external_nodes.add(external_root_name(callee, current_module) or callee)
|
|
232
247
|
|
|
233
248
|
if external_nodes or uses_done:
|
|
234
249
|
lines.append(" subgraph External [External]")
|
|
@@ -238,6 +253,37 @@ class MermaidGenerator:
|
|
|
238
253
|
lines.append(" Done((Done))")
|
|
239
254
|
lines.append(" end")
|
|
240
255
|
|
|
256
|
+
# ---- External composition edges (e.g., ColumnTransformer -> OneHotEncoder) ----
|
|
257
|
+
extra_call_edges = self.metadata.get("call_edges", []) or []
|
|
258
|
+
extra_seen = set()
|
|
259
|
+
|
|
260
|
+
for e in extra_call_edges:
|
|
261
|
+
src_call = e.get("source") or ""
|
|
262
|
+
tgt_call = e.get("target") or ""
|
|
263
|
+
mod = e.get("module") or ""
|
|
264
|
+
|
|
265
|
+
if not src_call or not tgt_call:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# Keep only external-resolved calls in this module context
|
|
269
|
+
if not keep_external(src_call, mod):
|
|
270
|
+
continue
|
|
271
|
+
if not keep_external(tgt_call, mod):
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
src_ext = external_root_name(src_call, mod) or src_call
|
|
275
|
+
tgt_ext = external_root_name(tgt_call, mod) or tgt_call
|
|
276
|
+
|
|
277
|
+
# Only draw if the target/source nodes exist in the External subgraph set
|
|
278
|
+
if src_ext not in external_nodes or tgt_ext not in external_nodes:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
k = (src_ext, tgt_ext)
|
|
282
|
+
if k in extra_seen:
|
|
283
|
+
continue
|
|
284
|
+
extra_seen.add(k)
|
|
285
|
+
|
|
286
|
+
lines.append(f" {nid('ext:' + src_ext)} --> {nid('ext:' + tgt_ext)}")
|
|
241
287
|
# ---------- call edges (internal -> internal/external), ordered from entry ----------
|
|
242
288
|
caller_order = ordered_functions_from_entry("main.main")
|
|
243
289
|
|
|
@@ -247,16 +293,48 @@ class MermaidGenerator:
|
|
|
247
293
|
|
|
248
294
|
info = function_map[caller]
|
|
249
295
|
current_module = info.get("module", "") or ""
|
|
250
|
-
current_module_ctx = current_module
|
|
296
|
+
#current_module_ctx = current_module
|
|
251
297
|
src = nid(caller)
|
|
252
298
|
|
|
253
299
|
call_args = info.get("call_arguments", {}) or {}
|
|
254
300
|
seen = set()
|
|
255
301
|
|
|
302
|
+
extra_call_edges = self.metadata.get("call_edges", []) or []
|
|
303
|
+
|
|
304
|
+
caller_external_roots = set()
|
|
305
|
+
for c in (info.get("calls") or []):
|
|
306
|
+
if keep_external(c, current_module) and not resolve_internal(c, current_module):
|
|
307
|
+
caller_external_roots.add(external_root_name(c, current_module) or c)
|
|
308
|
+
|
|
309
|
+
suppressed_external_roots = set()
|
|
310
|
+
for e in extra_call_edges:
|
|
311
|
+
src_call = e.get("source") or ""
|
|
312
|
+
tgt_call = e.get("target") or ""
|
|
313
|
+
mod = e.get("module") or ""
|
|
314
|
+
|
|
315
|
+
if not src_call or not tgt_call:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
# apply only when edge was discovered in same module context
|
|
319
|
+
if mod and mod != current_module:
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
src_root = external_root_name(src_call, current_module) or src_call
|
|
323
|
+
tgt_root = external_root_name(tgt_call, current_module) or tgt_call
|
|
324
|
+
|
|
325
|
+
if src_root in caller_external_roots:
|
|
326
|
+
suppressed_external_roots.add(tgt_root)
|
|
327
|
+
|
|
328
|
+
|
|
256
329
|
for callee in (info.get("calls") or []):
|
|
257
330
|
if not callee:
|
|
258
331
|
continue
|
|
259
332
|
|
|
333
|
+
if keep_external(callee, current_module) and not resolve_internal(callee, current_module):
|
|
334
|
+
callee_root = external_root_name(callee, current_module) or callee
|
|
335
|
+
if callee_root in suppressed_external_roots:
|
|
336
|
+
continue
|
|
337
|
+
|
|
260
338
|
target_internal = resolve_internal(callee, current_module)
|
|
261
339
|
|
|
262
340
|
# label (if any)
|
|
@@ -274,8 +352,8 @@ class MermaidGenerator:
|
|
|
274
352
|
else:
|
|
275
353
|
lines.append(f" {src} --> {nid(target_internal)}")
|
|
276
354
|
else:
|
|
277
|
-
if keep_external(callee):
|
|
278
|
-
ext = external_root_name(callee) or callee
|
|
355
|
+
if keep_external(callee, current_module):
|
|
356
|
+
ext = external_root_name(callee, current_module) or callee
|
|
279
357
|
edge_key = (src, nid("ext:" + callee), label)
|
|
280
358
|
if edge_key in seen:
|
|
281
359
|
continue
|
|
@@ -294,7 +372,7 @@ class MermaidGenerator:
|
|
|
294
372
|
info = function_map[fn]
|
|
295
373
|
dst = nid(fn)
|
|
296
374
|
current_module = info.get("module", "") or ""
|
|
297
|
-
current_module_ctx = current_module
|
|
375
|
+
#current_module_ctx = current_module
|
|
298
376
|
|
|
299
377
|
return_assignments = info.get("return_assignments", {}) or {}
|
|
300
378
|
for var_name, producers in return_assignments.items():
|
|
@@ -303,20 +381,20 @@ class MermaidGenerator:
|
|
|
303
381
|
if internal_p:
|
|
304
382
|
lines.append(f" {nid(internal_p)} -.->|{var_name}| {dst}")
|
|
305
383
|
else:
|
|
306
|
-
if keep_external(p):
|
|
307
|
-
ext = external_root_name(p) or p
|
|
384
|
+
if keep_external(p, current_module):
|
|
385
|
+
ext = external_root_name(p,current_module) or p
|
|
308
386
|
lines.append(f" {nid('ext:' + ext)} -.->|{var_name}| {dst}")
|
|
309
387
|
|
|
310
388
|
# ---------- pipeline heuristic: external tool A -> external tool B ----------
|
|
311
389
|
for _, info in function_map.items():
|
|
312
390
|
current_module = info.get("module", "") or ""
|
|
313
|
-
current_module_ctx = current_module
|
|
391
|
+
#current_module_ctx = current_module
|
|
314
392
|
calls = info.get("calls") or []
|
|
315
393
|
call_args = info.get("call_arguments") or {}
|
|
316
394
|
|
|
317
395
|
meaningful = []
|
|
318
396
|
for c in calls:
|
|
319
|
-
if c and keep_external(c) and not resolve_internal(c, current_module):
|
|
397
|
+
if c and keep_external(c,current_module) and not resolve_internal(c, current_module):
|
|
320
398
|
if c not in meaningful:
|
|
321
399
|
meaningful.append(c)
|
|
322
400
|
|
|
@@ -325,7 +403,7 @@ class MermaidGenerator:
|
|
|
325
403
|
|
|
326
404
|
fileish_callee = None
|
|
327
405
|
for callee, args in call_args.items():
|
|
328
|
-
if keep_external(callee) and any(is_fileish_arg(a) for a in (args or []) if isinstance(a, str)):
|
|
406
|
+
if keep_external(callee, current_module) and any(is_fileish_arg(a) for a in (args or []) if isinstance(a, str)):
|
|
329
407
|
fileish_callee = callee
|
|
330
408
|
break
|
|
331
409
|
|
|
@@ -338,8 +416,8 @@ class MermaidGenerator:
|
|
|
338
416
|
|
|
339
417
|
vars_passed = call_args.get(fileish_callee, []) or []
|
|
340
418
|
label = normalize_vars(vars_passed) or "value"
|
|
341
|
-
prod_ext = external_root_name(producer) or producer
|
|
342
|
-
file_ext = external_root_name(fileish_callee) or fileish_callee
|
|
419
|
+
prod_ext = external_root_name(producer,current_module) or producer
|
|
420
|
+
file_ext = external_root_name(fileish_callee,current_module) or fileish_callee
|
|
343
421
|
lines.append(f" {nid('ext:' + prod_ext)} -->|{label}| {nid('ext:' + file_ext)}")
|
|
344
422
|
|
|
345
423
|
# ---------- Done edge ----------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|