py-flow-mapper 0.1.0b2.dev0__tar.gz → 0.1.0b3.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.0b2.dev0/src/py_flow_mapper.egg-info → py_flow_mapper-0.1.0b3.dev0}/PKG-INFO +1 -1
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/setup.py +1 -1
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper/__init__.py +1 -1
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper/analyzer.py +13 -7
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper/cli.py +6 -2
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper/mermaid_generator.py +68 -15
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0/src/py_flow_mapper.egg-info}/PKG-INFO +1 -1
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/LICENSE +0 -0
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/README.md +0 -0
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/setup.cfg +0 -0
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper/utils.py +0 -0
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper.egg-info/SOURCES.txt +0 -0
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper.egg-info/dependency_links.txt +0 -0
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper.egg-info/entry_points.txt +0 -0
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper.egg-info/requires.txt +0 -0
- {py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.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.0b3.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.0b3.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"
|
|
@@ -368,20 +368,21 @@ class ModuleVisitor(ast.NodeVisitor):
|
|
|
368
368
|
def visit_ImportFrom(self, node):
|
|
369
369
|
"""Process from ... import statements."""
|
|
370
370
|
module_name = node.module or ""
|
|
371
|
-
|
|
371
|
+
|
|
372
372
|
for alias in node.names:
|
|
373
373
|
full_import = f"{module_name}.{alias.name}" if module_name else alias.name
|
|
374
374
|
self.imports.append(full_import)
|
|
375
|
-
|
|
375
|
+
|
|
376
|
+
alias_name = alias.asname or alias.name
|
|
377
|
+
self.import_mapping[alias_name] = full_import
|
|
378
|
+
|
|
376
379
|
if self._is_internal_import(module_name):
|
|
377
380
|
self.internal_imports.append(full_import)
|
|
378
|
-
# Map the imported function/class
|
|
379
|
-
alias_name = alias.asname or alias.name
|
|
380
|
-
self.import_mapping[alias_name] = full_import
|
|
381
381
|
else:
|
|
382
382
|
self.external_imports.append(full_import)
|
|
383
|
-
|
|
383
|
+
|
|
384
384
|
self.generic_visit(node)
|
|
385
|
+
|
|
385
386
|
|
|
386
387
|
def visit_FunctionDef(self, node):
|
|
387
388
|
"""Process function definitions with data flow analysis."""
|
|
@@ -560,7 +561,12 @@ class DataFlowAnalyzer(ast.NodeVisitor):
|
|
|
560
561
|
return {'func_name': node.func.id, 'full_name': None}
|
|
561
562
|
elif isinstance(node.func, ast.Attribute):
|
|
562
563
|
# Handle module.function calls
|
|
563
|
-
|
|
564
|
+
|
|
565
|
+
# https://github.com/ArunKoundinya/py-flow-mapper/issues/1 : forced external
|
|
566
|
+
#method_name = node.func.attr
|
|
567
|
+
|
|
568
|
+
method_name = self._get_attribute_name(node.func)
|
|
569
|
+
|
|
564
570
|
return {'func_name': method_name, 'full_name': None}
|
|
565
571
|
|
|
566
572
|
return None
|
|
@@ -24,7 +24,11 @@ def main():
|
|
|
24
24
|
# Generate diagrams command
|
|
25
25
|
diagram_parser = subparsers.add_parser('diagram', help='Generate diagrams from metadata')
|
|
26
26
|
diagram_parser.add_argument('metadata', type=str, help='Path to metadata JSON file')
|
|
27
|
-
|
|
27
|
+
# https://github.com/ArunKoundinya/py-flow-mapper/issues/1 : forced external
|
|
28
|
+
diagram_parser.add_argument( "--include-external", type=str, default="",
|
|
29
|
+
help="Comma-separated list of external libraries to include in diagrams"
|
|
30
|
+
)
|
|
31
|
+
|
|
28
32
|
# Structure command
|
|
29
33
|
struct_parser = subparsers.add_parser('structure', help='Show project structure')
|
|
30
34
|
struct_parser.add_argument('path', type=str, help='Path to the project directory')
|
|
@@ -96,7 +100,7 @@ def generate_diagrams(args):
|
|
|
96
100
|
|
|
97
101
|
print(f"Generating diagrams from: {metadata_path}")
|
|
98
102
|
|
|
99
|
-
generator = MermaidGenerator(metadata_path)
|
|
103
|
+
generator = MermaidGenerator(metadata_path,include_external=args.include_external)
|
|
100
104
|
|
|
101
105
|
try:
|
|
102
106
|
master_path = generator.generate_all_diagrams()
|
{py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper/mermaid_generator.py
RENAMED
|
@@ -2,12 +2,25 @@ import json
|
|
|
2
2
|
from typing import Dict, List, Any
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
COMMON_ALIAS_MAP = {
|
|
6
|
+
"pd": "pandas",
|
|
7
|
+
"np": "numpy",
|
|
8
|
+
"plt": "matplotlib",
|
|
9
|
+
"sns": "seaborn",
|
|
10
|
+
"sk": "sklearn",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
5
14
|
class MermaidGenerator:
|
|
6
15
|
"""Generate Mermaid diagrams from project metadata with data flow."""
|
|
7
16
|
|
|
8
|
-
def __init__(self, metadata_path: Path):
|
|
17
|
+
def __init__(self, metadata_path: Path,include_external: str = ""):
|
|
9
18
|
self.metadata = self._load_metadata(metadata_path)
|
|
10
19
|
self.output_dir = metadata_path.parent
|
|
20
|
+
# https://github.com/ArunKoundinya/py-flow-mapper/issues/1 : forced external
|
|
21
|
+
self.force_external = {
|
|
22
|
+
x.strip() for x in include_external.split(",") if x.strip()
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
def _load_metadata(self, metadata_path: Path) -> Dict[str, Any]:
|
|
13
26
|
"""Load metadata from JSON file."""
|
|
@@ -25,7 +38,7 @@ class MermaidGenerator:
|
|
|
25
38
|
- Adds a generic pipeline edge between two external tool-like nodes when file-ish arguments suggest a handoff.
|
|
26
39
|
- Adds a Done node when output-generation-like calls are detected.
|
|
27
40
|
"""
|
|
28
|
-
lines = ["```mermaid", "graph
|
|
41
|
+
lines = ["```mermaid", "graph LR"]
|
|
29
42
|
|
|
30
43
|
function_map = self.metadata.get("function_map", {}) or {}
|
|
31
44
|
internal_funcs = set(function_map.keys())
|
|
@@ -57,6 +70,25 @@ class MermaidGenerator:
|
|
|
57
70
|
if not cleaned:
|
|
58
71
|
return ""
|
|
59
72
|
return ",".join(sorted(set(cleaned)))
|
|
73
|
+
|
|
74
|
+
current_module_ctx = ""
|
|
75
|
+
|
|
76
|
+
def module_import_mapping() -> dict:
|
|
77
|
+
return (modules.get(current_module_ctx, {}) or {}).get("import_mapping", {}) or {}
|
|
78
|
+
|
|
79
|
+
def external_root_name(call_name: str) -> str:
|
|
80
|
+
if not call_name:
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
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)
|
|
89
|
+
merged_map = {**COMMON_ALIAS_MAP, **imp_map}
|
|
90
|
+
|
|
91
|
+
return merged_map.get(root, root)
|
|
60
92
|
|
|
61
93
|
# Keep the graph clean: ignore common builtins + attribute-noise
|
|
62
94
|
NOISY_EXTERNAL = {
|
|
@@ -68,18 +100,29 @@ class MermaidGenerator:
|
|
|
68
100
|
}
|
|
69
101
|
|
|
70
102
|
def keep_external(call_name: str) -> bool:
|
|
71
|
-
"""
|
|
72
|
-
Meaningful external nodes only:
|
|
73
|
-
- drop noise builtins
|
|
74
|
-
- drop internal classes
|
|
75
|
-
- drop dotted calls (obj.method) to avoid clutter
|
|
76
|
-
- keep CamelCase (tool-ish classes / constructors)
|
|
77
|
-
"""
|
|
78
103
|
if not call_name:
|
|
79
104
|
return False
|
|
80
105
|
|
|
81
106
|
base = short_label(call_name)
|
|
82
107
|
|
|
108
|
+
# forced external always wins
|
|
109
|
+
root = external_root_name(call_name)
|
|
110
|
+
if root and root in self.force_external:
|
|
111
|
+
return True
|
|
112
|
+
if base in self.force_external:
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
# ✅ NEW: module-specific import mapping
|
|
116
|
+
imp_map = module_import_mapping() or {}
|
|
117
|
+
if base in imp_map:
|
|
118
|
+
mapped = imp_map[base]
|
|
119
|
+
mapped_module = ".".join(mapped.split(".")[:-1])
|
|
120
|
+
|
|
121
|
+
if mapped_module in modules:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
return True
|
|
125
|
+
|
|
83
126
|
# internal class => NOT external
|
|
84
127
|
if base in internal_classes:
|
|
85
128
|
return False
|
|
@@ -91,6 +134,7 @@ class MermaidGenerator:
|
|
|
91
134
|
if "." in call_name:
|
|
92
135
|
return False
|
|
93
136
|
|
|
137
|
+
# keep CamelCase (tool-ish classes / constructors)
|
|
94
138
|
return is_camel_case(base)
|
|
95
139
|
|
|
96
140
|
def resolve_internal(call: str, current_module: str) -> str:
|
|
@@ -130,6 +174,7 @@ class MermaidGenerator:
|
|
|
130
174
|
|
|
131
175
|
info = function_map.get(fn_key, {})
|
|
132
176
|
current_module = info.get("module", "") or ""
|
|
177
|
+
current_module_ctx = current_module
|
|
133
178
|
for c in (info.get("calls") or []):
|
|
134
179
|
target = self._find_function_full_name(c, current_module)
|
|
135
180
|
if target and target in function_map:
|
|
@@ -165,6 +210,7 @@ class MermaidGenerator:
|
|
|
165
210
|
|
|
166
211
|
for _, info in function_map.items():
|
|
167
212
|
current_module = info.get("module", "") or ""
|
|
213
|
+
current_module_ctx = current_module
|
|
168
214
|
|
|
169
215
|
# prefer call_arguments keys
|
|
170
216
|
call_args = info.get("call_arguments", {}) or {}
|
|
@@ -172,7 +218,7 @@ class MermaidGenerator:
|
|
|
172
218
|
if resolve_internal(callee, current_module):
|
|
173
219
|
continue
|
|
174
220
|
if keep_external(callee):
|
|
175
|
-
external_nodes.add(callee)
|
|
221
|
+
external_nodes.add(external_root_name(callee) or callee)
|
|
176
222
|
|
|
177
223
|
# scan raw calls for Done + missed externals
|
|
178
224
|
for callee in (info.get("calls") or []):
|
|
@@ -182,7 +228,7 @@ class MermaidGenerator:
|
|
|
182
228
|
if resolve_internal(callee, current_module):
|
|
183
229
|
continue
|
|
184
230
|
if keep_external(callee):
|
|
185
|
-
external_nodes.add(callee)
|
|
231
|
+
external_nodes.add(external_root_name(callee) or callee)
|
|
186
232
|
|
|
187
233
|
if external_nodes or uses_done:
|
|
188
234
|
lines.append(" subgraph External [External]")
|
|
@@ -201,6 +247,7 @@ class MermaidGenerator:
|
|
|
201
247
|
|
|
202
248
|
info = function_map[caller]
|
|
203
249
|
current_module = info.get("module", "") or ""
|
|
250
|
+
current_module_ctx = current_module
|
|
204
251
|
src = nid(caller)
|
|
205
252
|
|
|
206
253
|
call_args = info.get("call_arguments", {}) or {}
|
|
@@ -228,15 +275,16 @@ class MermaidGenerator:
|
|
|
228
275
|
lines.append(f" {src} --> {nid(target_internal)}")
|
|
229
276
|
else:
|
|
230
277
|
if keep_external(callee):
|
|
278
|
+
ext = external_root_name(callee) or callee
|
|
231
279
|
edge_key = (src, nid("ext:" + callee), label)
|
|
232
280
|
if edge_key in seen:
|
|
233
281
|
continue
|
|
234
282
|
seen.add(edge_key)
|
|
235
283
|
|
|
236
284
|
if label:
|
|
237
|
-
lines.append(f" {src} --> |{label}| {nid('ext:' +
|
|
285
|
+
lines.append(f" {src} --> |{label}| {nid('ext:' + ext)}")
|
|
238
286
|
else:
|
|
239
|
-
lines.append(f" {src} --> {nid('ext:' +
|
|
287
|
+
lines.append(f" {src} --> {nid('ext:' + ext)}")
|
|
240
288
|
|
|
241
289
|
# ---------- data flow edges (return_assignments) ----------
|
|
242
290
|
for fn in caller_order:
|
|
@@ -246,6 +294,7 @@ class MermaidGenerator:
|
|
|
246
294
|
info = function_map[fn]
|
|
247
295
|
dst = nid(fn)
|
|
248
296
|
current_module = info.get("module", "") or ""
|
|
297
|
+
current_module_ctx = current_module
|
|
249
298
|
|
|
250
299
|
return_assignments = info.get("return_assignments", {}) or {}
|
|
251
300
|
for var_name, producers in return_assignments.items():
|
|
@@ -255,11 +304,13 @@ class MermaidGenerator:
|
|
|
255
304
|
lines.append(f" {nid(internal_p)} -.->|{var_name}| {dst}")
|
|
256
305
|
else:
|
|
257
306
|
if keep_external(p):
|
|
258
|
-
|
|
307
|
+
ext = external_root_name(p) or p
|
|
308
|
+
lines.append(f" {nid('ext:' + ext)} -.->|{var_name}| {dst}")
|
|
259
309
|
|
|
260
310
|
# ---------- pipeline heuristic: external tool A -> external tool B ----------
|
|
261
311
|
for _, info in function_map.items():
|
|
262
312
|
current_module = info.get("module", "") or ""
|
|
313
|
+
current_module_ctx = current_module
|
|
263
314
|
calls = info.get("calls") or []
|
|
264
315
|
call_args = info.get("call_arguments") or {}
|
|
265
316
|
|
|
@@ -287,7 +338,9 @@ class MermaidGenerator:
|
|
|
287
338
|
|
|
288
339
|
vars_passed = call_args.get(fileish_callee, []) or []
|
|
289
340
|
label = normalize_vars(vars_passed) or "value"
|
|
290
|
-
|
|
341
|
+
prod_ext = external_root_name(producer) or producer
|
|
342
|
+
file_ext = external_root_name(fileish_callee) or fileish_callee
|
|
343
|
+
lines.append(f" {nid('ext:' + prod_ext)} -->|{label}| {nid('ext:' + file_ext)}")
|
|
291
344
|
|
|
292
345
|
# ---------- Done edge ----------
|
|
293
346
|
if uses_done:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_flow_mapper-0.1.0b2.dev0 → py_flow_mapper-0.1.0b3.dev0}/src/py_flow_mapper.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|