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.
Files changed (16) hide show
  1. {py_flow_mapper-0.1.0b7.dev0/src/py_flow_mapper.egg-info → py_flow_mapper-0.1.0b9.dev0}/PKG-INFO +1 -1
  2. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/setup.py +1 -1
  3. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/__init__.py +1 -1
  4. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/analyzer.py +129 -15
  5. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/mermaid_generator.py +120 -42
  6. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0/src/py_flow_mapper.egg-info}/PKG-INFO +1 -1
  7. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/LICENSE +0 -0
  8. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/README.md +0 -0
  9. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/setup.cfg +0 -0
  10. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/cli.py +0 -0
  11. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper/utils.py +0 -0
  12. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper.egg-info/SOURCES.txt +0 -0
  13. {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
  14. {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
  15. {py_flow_mapper-0.1.0b7.dev0 → py_flow_mapper-0.1.0b9.dev0}/src/py_flow_mapper.egg-info/requires.txt +0 -0
  16. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-flow-mapper
3
- Version: 0.1.0b7.dev0
3
+ Version: 0.1.0b9.dev0
4
4
  Summary: Python project analyzer and visualization tool
5
5
  Home-page: https://github.com/ArunKoundinya/py-flow-mapper
6
6
  Author: Arun Koundinya Parasa
@@ -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.0b7.dev",
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.0b7.dev"
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
- "data", "datasets", "models", "checkpoints", "outputs", "results",
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
- # Get the variable name being assigned to
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
- self.calls.append(call_info['func_name'])
533
- if var_name not in self.return_assignments:
534
- self.return_assignments[var_name] = []
535
- self.return_assignments[var_name].append(call_info['func_name'])
536
-
537
- # Check if the value is a Name (could be a variable holding a return value)
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
- self.calls.append(func_name)
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
 
@@ -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(current_module_ctx, {}) or {}).get("import_mapping", {}) or {}
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
- # forced external always wins
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
- # NEW: module-specific import mapping
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
- # First try your normal resolver
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
- # NEW: if call is like "obj.method", try to map by method name
147
- if "." in (call or ""):
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", "path", "file"))
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 ----------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-flow-mapper
3
- Version: 0.1.0b7.dev0
3
+ Version: 0.1.0b9.dev0
4
4
  Summary: Python project analyzer and visualization tool
5
5
  Home-page: https://github.com/ArunKoundinya/py-flow-mapper
6
6
  Author: Arun Koundinya Parasa