py-flow-mapper 0.1.0b3.dev0__tar.gz → 0.1.0b5.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.0b3.dev0/src/py_flow_mapper.egg-info → py_flow_mapper-0.1.0b5.dev0}/PKG-INFO +1 -1
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/setup.py +1 -1
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper/__init__.py +1 -1
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper/analyzer.py +27 -14
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper/mermaid_generator.py +24 -38
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper/utils.py +30 -11
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0/src/py_flow_mapper.egg-info}/PKG-INFO +1 -1
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/LICENSE +0 -0
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/README.md +0 -0
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/setup.cfg +0 -0
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper/cli.py +0 -0
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper.egg-info/SOURCES.txt +0 -0
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper.egg-info/dependency_links.txt +0 -0
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper.egg-info/entry_points.txt +0 -0
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper.egg-info/requires.txt +0 -0
- {py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.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.0b5.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.0b5.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"
|
|
@@ -107,8 +107,24 @@ class ProjectAnalyzer:
|
|
|
107
107
|
"""Find all Python files in the project, excluding virtual environments and other unwanted directories."""
|
|
108
108
|
python_files = []
|
|
109
109
|
|
|
110
|
-
# Prefixes of directories to exclude
|
|
111
|
-
exclude_prefixes = (
|
|
110
|
+
# Prefixes of directories to exclude
|
|
111
|
+
exclude_prefixes = (
|
|
112
|
+
"venv", ".venv", "env", ".env", "virtualenv", ".virtualenv",
|
|
113
|
+
"__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
114
|
+
".coverage", "htmlcov", ".hypothesis",
|
|
115
|
+
".git", ".hg", ".svn",
|
|
116
|
+
"build", "dist", ".eggs", "egg-info", "*.egg-info",
|
|
117
|
+
".tox", ".nox", ".pdm-build",
|
|
118
|
+
"site-packages", "node_modules", "bower_components",
|
|
119
|
+
"docs", "doc", "site", "_site", "mkdocs",
|
|
120
|
+
".ipynb_checkpoints", "notebooks", "examples", "experiments", "scratch",
|
|
121
|
+
"data", "datasets", "models", "checkpoints", "outputs", "results",
|
|
122
|
+
"logs", "tmp", "temp", "cache",
|
|
123
|
+
".idea", ".vscode", ".DS_Store", "__MACOSX",
|
|
124
|
+
".github", ".gitlab", ".circleci",
|
|
125
|
+
"tests", "test", "testing",
|
|
126
|
+
)
|
|
127
|
+
|
|
112
128
|
|
|
113
129
|
for root, dirs, files in os.walk(self.base_path):
|
|
114
130
|
# Filter out directories that start with any of the exclude prefixes
|
|
@@ -505,24 +521,21 @@ class DataFlowAnalyzer(ast.NodeVisitor):
|
|
|
505
521
|
|
|
506
522
|
def visit_Assign(self, node):
|
|
507
523
|
"""Track assignments of function return values."""
|
|
508
|
-
|
|
509
|
-
if isinstance(node.targets[0], ast.Name):
|
|
524
|
+
if node.targets and isinstance(node.targets[0], ast.Name):
|
|
510
525
|
var_name = node.targets[0].id
|
|
511
|
-
|
|
512
|
-
# Check if the value is a function call
|
|
526
|
+
|
|
513
527
|
if isinstance(node.value, ast.Call):
|
|
514
528
|
call_info = self._extract_call_info(node.value)
|
|
515
529
|
if call_info:
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
530
|
+
func_name = call_info["func_name"]
|
|
531
|
+
|
|
532
|
+
if func_name not in self.calls:
|
|
533
|
+
self.calls.append(func_name)
|
|
534
|
+
self.return_assignments.setdefault(var_name, []).append(func_name)
|
|
535
|
+
|
|
522
536
|
elif isinstance(node.value, ast.Name):
|
|
523
|
-
# Track variable assignments for data flow
|
|
524
537
|
pass
|
|
525
|
-
|
|
538
|
+
|
|
526
539
|
self.generic_visit(node)
|
|
527
540
|
|
|
528
541
|
def visit_Call(self, node):
|
{py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.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,42 +92,35 @@ 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:
|
|
@@ -174,7 +160,7 @@ class MermaidGenerator:
|
|
|
174
160
|
|
|
175
161
|
info = function_map.get(fn_key, {})
|
|
176
162
|
current_module = info.get("module", "") or ""
|
|
177
|
-
current_module_ctx = current_module
|
|
163
|
+
#current_module_ctx = current_module
|
|
178
164
|
for c in (info.get("calls") or []):
|
|
179
165
|
target = self._find_function_full_name(c, current_module)
|
|
180
166
|
if target and target in function_map:
|
|
@@ -210,15 +196,15 @@ class MermaidGenerator:
|
|
|
210
196
|
|
|
211
197
|
for _, info in function_map.items():
|
|
212
198
|
current_module = info.get("module", "") or ""
|
|
213
|
-
current_module_ctx = current_module
|
|
199
|
+
#current_module_ctx = current_module
|
|
214
200
|
|
|
215
201
|
# prefer call_arguments keys
|
|
216
202
|
call_args = info.get("call_arguments", {}) or {}
|
|
217
203
|
for callee in call_args.keys():
|
|
218
204
|
if resolve_internal(callee, current_module):
|
|
219
205
|
continue
|
|
220
|
-
if keep_external(callee):
|
|
221
|
-
external_nodes.add(external_root_name(callee) or callee)
|
|
206
|
+
if keep_external(callee, current_module):
|
|
207
|
+
external_nodes.add(external_root_name(callee, current_module) or callee)
|
|
222
208
|
|
|
223
209
|
# scan raw calls for Done + missed externals
|
|
224
210
|
for callee in (info.get("calls") or []):
|
|
@@ -227,8 +213,8 @@ class MermaidGenerator:
|
|
|
227
213
|
|
|
228
214
|
if resolve_internal(callee, current_module):
|
|
229
215
|
continue
|
|
230
|
-
if keep_external(callee):
|
|
231
|
-
external_nodes.add(external_root_name(callee) or callee)
|
|
216
|
+
if keep_external(callee, current_module):
|
|
217
|
+
external_nodes.add(external_root_name(callee, current_module) or callee)
|
|
232
218
|
|
|
233
219
|
if external_nodes or uses_done:
|
|
234
220
|
lines.append(" subgraph External [External]")
|
|
@@ -247,7 +233,7 @@ class MermaidGenerator:
|
|
|
247
233
|
|
|
248
234
|
info = function_map[caller]
|
|
249
235
|
current_module = info.get("module", "") or ""
|
|
250
|
-
current_module_ctx = current_module
|
|
236
|
+
#current_module_ctx = current_module
|
|
251
237
|
src = nid(caller)
|
|
252
238
|
|
|
253
239
|
call_args = info.get("call_arguments", {}) or {}
|
|
@@ -274,8 +260,8 @@ class MermaidGenerator:
|
|
|
274
260
|
else:
|
|
275
261
|
lines.append(f" {src} --> {nid(target_internal)}")
|
|
276
262
|
else:
|
|
277
|
-
if keep_external(callee):
|
|
278
|
-
ext = external_root_name(callee) or callee
|
|
263
|
+
if keep_external(callee, current_module):
|
|
264
|
+
ext = external_root_name(callee, current_module) or callee
|
|
279
265
|
edge_key = (src, nid("ext:" + callee), label)
|
|
280
266
|
if edge_key in seen:
|
|
281
267
|
continue
|
|
@@ -294,7 +280,7 @@ class MermaidGenerator:
|
|
|
294
280
|
info = function_map[fn]
|
|
295
281
|
dst = nid(fn)
|
|
296
282
|
current_module = info.get("module", "") or ""
|
|
297
|
-
current_module_ctx = current_module
|
|
283
|
+
#current_module_ctx = current_module
|
|
298
284
|
|
|
299
285
|
return_assignments = info.get("return_assignments", {}) or {}
|
|
300
286
|
for var_name, producers in return_assignments.items():
|
|
@@ -303,20 +289,20 @@ class MermaidGenerator:
|
|
|
303
289
|
if internal_p:
|
|
304
290
|
lines.append(f" {nid(internal_p)} -.->|{var_name}| {dst}")
|
|
305
291
|
else:
|
|
306
|
-
if keep_external(p):
|
|
307
|
-
ext = external_root_name(p) or p
|
|
292
|
+
if keep_external(p, current_module):
|
|
293
|
+
ext = external_root_name(p,current_module) or p
|
|
308
294
|
lines.append(f" {nid('ext:' + ext)} -.->|{var_name}| {dst}")
|
|
309
295
|
|
|
310
296
|
# ---------- pipeline heuristic: external tool A -> external tool B ----------
|
|
311
297
|
for _, info in function_map.items():
|
|
312
298
|
current_module = info.get("module", "") or ""
|
|
313
|
-
current_module_ctx = current_module
|
|
299
|
+
#current_module_ctx = current_module
|
|
314
300
|
calls = info.get("calls") or []
|
|
315
301
|
call_args = info.get("call_arguments") or {}
|
|
316
302
|
|
|
317
303
|
meaningful = []
|
|
318
304
|
for c in calls:
|
|
319
|
-
if c and keep_external(c) and not resolve_internal(c, current_module):
|
|
305
|
+
if c and keep_external(c,current_module) and not resolve_internal(c, current_module):
|
|
320
306
|
if c not in meaningful:
|
|
321
307
|
meaningful.append(c)
|
|
322
308
|
|
|
@@ -325,7 +311,7 @@ class MermaidGenerator:
|
|
|
325
311
|
|
|
326
312
|
fileish_callee = None
|
|
327
313
|
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)):
|
|
314
|
+
if keep_external(callee, current_module) and any(is_fileish_arg(a) for a in (args or []) if isinstance(a, str)):
|
|
329
315
|
fileish_callee = callee
|
|
330
316
|
break
|
|
331
317
|
|
|
@@ -338,8 +324,8 @@ class MermaidGenerator:
|
|
|
338
324
|
|
|
339
325
|
vars_passed = call_args.get(fileish_callee, []) or []
|
|
340
326
|
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
|
|
327
|
+
prod_ext = external_root_name(producer,current_module) or producer
|
|
328
|
+
file_ext = external_root_name(fileish_callee,current_module) or fileish_callee
|
|
343
329
|
lines.append(f" {nid('ext:' + prod_ext)} -->|{label}| {nid('ext:' + file_ext)}")
|
|
344
330
|
|
|
345
331
|
# ---------- Done edge ----------
|
|
@@ -120,17 +120,36 @@ def load_json(file_path: Path) -> Any:
|
|
|
120
120
|
def get_project_structure(base_path: Path, exclude_dirs: List[str] = None) -> Dict[str, Any]:
|
|
121
121
|
"""Get the project directory structure."""
|
|
122
122
|
if exclude_dirs is None:
|
|
123
|
-
exclude_dirs = [
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
exclude_dirs = [
|
|
124
|
+
"venv", ".venv", "env", ".env", "virtualenv",
|
|
125
|
+
"__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
126
|
+
".coverage", "htmlcov",
|
|
127
|
+
".git", ".hg", ".svn",
|
|
128
|
+
".idea", ".vscode", ".DS_Store", "__MACOSX",
|
|
129
|
+
"build", "dist", ".eggs", ".tox", ".nox",
|
|
130
|
+
"node_modules", "site-packages",
|
|
131
|
+
"docs", "doc", "notebooks", ".ipynb_checkpoints",
|
|
132
|
+
"models", "outputs", "results",
|
|
133
|
+
"logs", "tmp", "temp",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
exclude_set = set(exclude_dirs)
|
|
137
|
+
structure: Dict[str, Any] = {}
|
|
138
|
+
|
|
139
|
+
for item in sorted(base_path.iterdir(), key=lambda p: p.name):
|
|
140
|
+
name = item.name
|
|
141
|
+
|
|
142
|
+
# Unified exclusion logic (works for both files and dirs)
|
|
143
|
+
if (
|
|
144
|
+
name in exclude_set
|
|
145
|
+
or name.endswith(".egg-info") # e.g., mypkg.egg-info
|
|
146
|
+
or any(name.startswith(excl) for excl in exclude_set) # e.g., venv311, build_temp
|
|
147
|
+
):
|
|
129
148
|
continue
|
|
130
|
-
|
|
149
|
+
|
|
131
150
|
if item.is_dir():
|
|
132
|
-
structure[
|
|
151
|
+
structure[name] = get_project_structure(item, exclude_dirs)
|
|
133
152
|
else:
|
|
134
|
-
structure[
|
|
135
|
-
|
|
136
|
-
return structure
|
|
153
|
+
structure[name] = "file"
|
|
154
|
+
|
|
155
|
+
return structure
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_flow_mapper-0.1.0b3.dev0 → py_flow_mapper-0.1.0b5.dev0}/src/py_flow_mapper.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|