pymap-cli 0.3.0__py3-none-any.whl
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.
- pymap/__init__.py +37 -0
- pymap/__main__.py +8 -0
- pymap/analysis/__init__.py +1 -0
- pymap/analysis/calls.py +36 -0
- pymap/analysis/cycles.py +43 -0
- pymap/analysis/flow.py +175 -0
- pymap/analysis/symbols.py +175 -0
- pymap/cli.py +143 -0
- pymap/mapper.py +198 -0
- pymap/py.typed +0 -0
- pymap/render.py +170 -0
- pymap/runner.py +93 -0
- pymap/settings.py +176 -0
- pymap/templates/__init__.py +8 -0
- pymap/templates/explorer.html +822 -0
- pymap/templates/index.html +42 -0
- pymap/templates/section.html +7 -0
- pymap/tools/__init__.py +22 -0
- pymap/tools/code2flow.py +56 -0
- pymap/tools/pydeps.py +42 -0
- pymap/tools/pyreverse.py +41 -0
- pymap/tools/tach.py +104 -0
- pymap_cli-0.3.0.dist-info/METADATA +197 -0
- pymap_cli-0.3.0.dist-info/RECORD +27 -0
- pymap_cli-0.3.0.dist-info/WHEEL +4 -0
- pymap_cli-0.3.0.dist-info/entry_points.txt +2 -0
- pymap_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta charset="utf-8">
|
|
3
|
+
<title>Map of __PYMAP_NAME__</title>
|
|
4
|
+
<style>
|
|
5
|
+
:root {
|
|
6
|
+
--ink:#12171c; --paper:#f7f6f3; --rule:#c9c4ba;
|
|
7
|
+
--accent:#1a5e63; --alert:#9c3b28; --muted:#6a6459;
|
|
8
|
+
}
|
|
9
|
+
* { box-sizing:border-box }
|
|
10
|
+
body {
|
|
11
|
+
margin:0; background:var(--paper); color:var(--ink);
|
|
12
|
+
font:15px/1.6 "SF Mono",ui-monospace,"JetBrains Mono",Menlo,monospace;
|
|
13
|
+
}
|
|
14
|
+
header { padding:44px 32px 28px; border-bottom:2px solid var(--ink) }
|
|
15
|
+
h1 { margin:0; font-size:30px; font-weight:600; letter-spacing:-.02em }
|
|
16
|
+
.sub { color:var(--muted); margin-top:6px }
|
|
17
|
+
main { padding:0 32px 64px }
|
|
18
|
+
section { border-bottom:1px solid var(--rule); padding:28px 0 }
|
|
19
|
+
h2 { font-size:17px; margin:0 0 4px; font-weight:600 }
|
|
20
|
+
.what { color:var(--muted); margin:0 0 14px; max-width:62ch }
|
|
21
|
+
.stats { display:flex; gap:26px; margin:14px 0 18px; flex-wrap:wrap }
|
|
22
|
+
.stat b { display:block; font-size:26px; font-weight:600; color:var(--accent) }
|
|
23
|
+
.stat span { color:var(--muted); font-size:13px }
|
|
24
|
+
a.view {
|
|
25
|
+
display:inline-block; padding:9px 15px; border:1.5px solid var(--ink);
|
|
26
|
+
color:var(--ink); text-decoration:none; margin:0 8px 8px 0;
|
|
27
|
+
}
|
|
28
|
+
a.view:hover { background:var(--ink); color:var(--paper) }
|
|
29
|
+
a.view:focus-visible { outline:3px solid var(--accent); outline-offset:2px }
|
|
30
|
+
.absent { color:var(--muted); font-style:italic }
|
|
31
|
+
.cycles { border-left:4px solid var(--alert); padding:12px 16px; background:#fff }
|
|
32
|
+
.cycles b { color:var(--alert) }
|
|
33
|
+
code { background:#ebe8e2; padding:1px 5px }
|
|
34
|
+
ul { margin:8px 0; padding-left:20px }
|
|
35
|
+
</style>
|
|
36
|
+
<header>
|
|
37
|
+
<h1>__PYMAP_NAME__</h1>
|
|
38
|
+
<div class="sub">__PYMAP_FILES__ Python files · __PYMAP_LINES__ lines · analysed locally, nothing is sent anywhere</div>
|
|
39
|
+
</header>
|
|
40
|
+
<main>
|
|
41
|
+
__PYMAP_BODY__
|
|
42
|
+
</main>
|
pymap/tools/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Wrappers around external tools: one module per tool.
|
|
2
|
+
|
|
3
|
+
Each module exposes ``execute(settings)`` and returns raw facts, never HTML --
|
|
4
|
+
layout belongs to :mod:`pymap.render`. A missing tool returns an empty result
|
|
5
|
+
carrying an explanatory ``log``; it never aborts the run.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def count_svg(path: str | Path) -> tuple[int, int]:
|
|
15
|
+
"""(nodes, edges) of an SVG produced by Graphviz."""
|
|
16
|
+
try:
|
|
17
|
+
text = Path(path).read_text(encoding="utf-8", errors="ignore")
|
|
18
|
+
except OSError:
|
|
19
|
+
return 0, 0
|
|
20
|
+
titles = re.findall(r"<title>([^<]+)</title>", text)
|
|
21
|
+
edges = [t for t in titles if "->" in t or "->" in t]
|
|
22
|
+
return len(titles) - len(edges), len(edges)
|
pymap/tools/code2flow.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""code2flow -- call graph between functions.
|
|
2
|
+
|
|
3
|
+
Only the JSON is produced: pymap's own explorer supersedes code2flow's SVG, but
|
|
4
|
+
it feeds on this graph.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pymap import runner
|
|
13
|
+
from pymap.settings import Settings
|
|
14
|
+
|
|
15
|
+
ROLE = "call graph"
|
|
16
|
+
|
|
17
|
+
#: Past this point the file list would exceed the maximum command-line length,
|
|
18
|
+
#: so we fall back to the directory (and therefore to code2flow's own walk).
|
|
19
|
+
MAX_ARGUMENTS = 3000
|
|
20
|
+
|
|
21
|
+
#: code2flow is slow on large codebases; it needs more than the shared timeout.
|
|
22
|
+
MIN_TIMEOUT = 420
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def execute(settings: Settings) -> dict[str, Any]:
|
|
26
|
+
"""Write ``calls.json``. Returns ``{json, functions, calls, log}``."""
|
|
27
|
+
empty: dict[str, Any] = {"json": None, "functions": 0, "calls": 0}
|
|
28
|
+
base = runner.resolve("code2flow")
|
|
29
|
+
if not base:
|
|
30
|
+
return dict(empty, log="code2flow is not installed")
|
|
31
|
+
|
|
32
|
+
files = settings.files()
|
|
33
|
+
if not files:
|
|
34
|
+
return dict(empty, log="no Python file to analyse")
|
|
35
|
+
# Pass the filtered list rather than the directory: code2flow does not know
|
|
36
|
+
# about our exclusions and would walk into .venv/ if pointed at the root.
|
|
37
|
+
sources = (
|
|
38
|
+
[str(path) for path in files] if len(files) <= MAX_ARGUMENTS else [str(settings.target)]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
output = settings.output / "calls.json"
|
|
42
|
+
ok, log = runner.run(
|
|
43
|
+
base + sources + ["-o", str(output)], timeout=max(settings.timeout, MIN_TIMEOUT)
|
|
44
|
+
)
|
|
45
|
+
if not (ok and output.exists()):
|
|
46
|
+
return dict(empty, log=log or "code2flow failed")
|
|
47
|
+
try:
|
|
48
|
+
graph = json.loads(output.read_text(encoding="utf-8"))["graph"]
|
|
49
|
+
except (OSError, ValueError, KeyError):
|
|
50
|
+
return dict(empty, json=output, log="unreadable code2flow JSON")
|
|
51
|
+
return {
|
|
52
|
+
"json": output,
|
|
53
|
+
"functions": len(graph["nodes"]),
|
|
54
|
+
"calls": len(graph["edges"]),
|
|
55
|
+
"log": "",
|
|
56
|
+
}
|
pymap/tools/pydeps.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""pydeps -- who imports whom, at module level."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pymap import runner
|
|
8
|
+
from pymap.settings import Settings
|
|
9
|
+
from pymap.tools import count_svg
|
|
10
|
+
|
|
11
|
+
ROLE = "import graph"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute(settings: Settings) -> dict[str, Any]:
|
|
15
|
+
"""Write ``imports.svg``. Returns ``{svg, modules, imports, log}``."""
|
|
16
|
+
empty: dict[str, Any] = {"svg": None, "modules": 0, "imports": 0}
|
|
17
|
+
base = runner.resolve("pydeps")
|
|
18
|
+
if not base:
|
|
19
|
+
return dict(empty, log="pydeps is not installed")
|
|
20
|
+
if not runner.resolve("dot"):
|
|
21
|
+
return dict(empty, log="graphviz (dot) missing: pydeps cannot render the SVG")
|
|
22
|
+
|
|
23
|
+
svg = settings.output / "imports.svg"
|
|
24
|
+
ok, log = runner.run(
|
|
25
|
+
[
|
|
26
|
+
*base,
|
|
27
|
+
str(settings.target),
|
|
28
|
+
"--noshow",
|
|
29
|
+
"--cluster",
|
|
30
|
+
"--max-bacon",
|
|
31
|
+
"2",
|
|
32
|
+
"-T",
|
|
33
|
+
"svg",
|
|
34
|
+
"-o",
|
|
35
|
+
str(svg),
|
|
36
|
+
],
|
|
37
|
+
timeout=settings.timeout,
|
|
38
|
+
)
|
|
39
|
+
if not (ok and svg.exists()):
|
|
40
|
+
return dict(empty, log=log or "pydeps failed")
|
|
41
|
+
nodes, edges = count_svg(svg)
|
|
42
|
+
return {"svg": svg, "modules": nodes, "imports": edges, "log": ""}
|
pymap/tools/pyreverse.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""pyreverse (from pylint) -- class and package diagrams."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pymap import runner
|
|
9
|
+
from pymap.settings import Settings
|
|
10
|
+
|
|
11
|
+
ROLE = "class diagrams"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute(settings: Settings) -> dict[str, Any]:
|
|
15
|
+
"""Write ``classes_<name>.svg`` and ``packages_<name>.svg``.
|
|
16
|
+
|
|
17
|
+
Returns ``{classes, packages, log}``.
|
|
18
|
+
"""
|
|
19
|
+
empty: dict[str, Any] = {"classes": None, "packages": None}
|
|
20
|
+
base = runner.resolve("pyreverse")
|
|
21
|
+
if not base:
|
|
22
|
+
return dict(empty, log="pyreverse is not installed (ships with pylint)")
|
|
23
|
+
if not runner.resolve("dot"):
|
|
24
|
+
return dict(empty, log="graphviz (dot) missing: pyreverse cannot render the SVG")
|
|
25
|
+
|
|
26
|
+
project = re.sub(r"[^A-Za-z0-9_.-]", "_", settings.name) or "project"
|
|
27
|
+
ok, log = runner.run(
|
|
28
|
+
[*base, "-o", "svg", "-p", project, "-d", str(settings.output), str(settings.target)],
|
|
29
|
+
cwd=str(settings.target.parent),
|
|
30
|
+
timeout=settings.timeout,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
classes = settings.output / f"classes_{project}.svg"
|
|
34
|
+
packages = settings.output / f"packages_{project}.svg"
|
|
35
|
+
if not ok and not classes.exists():
|
|
36
|
+
return dict(empty, log=log or "pyreverse failed")
|
|
37
|
+
return {
|
|
38
|
+
"classes": classes if classes.exists() else None,
|
|
39
|
+
"packages": packages if packages.exists() else None,
|
|
40
|
+
"log": "",
|
|
41
|
+
}
|
pymap/tools/tach.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""tach -- module boundaries and file-by-file dependencies.
|
|
2
|
+
|
|
3
|
+
tach requires a ``tach.toml`` at the root of wherever it runs. Rather than drop
|
|
4
|
+
that file into the analysed project -- and risk clobbering the user's own --
|
|
5
|
+
pymap copies the ``.py`` sources into a temporary directory and works there.
|
|
6
|
+
The target project is never modified.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
from collections.abc import Iterable, Sequence
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from pymap import runner
|
|
19
|
+
from pymap.settings import Settings, is_excluded
|
|
20
|
+
|
|
21
|
+
ROLE = "module boundaries"
|
|
22
|
+
|
|
23
|
+
#: Past this point ``tach sync`` gets very slow, for a graph already unreadable.
|
|
24
|
+
MAX_MODULES = 60
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _copy_sources(target: Path, into: Path, excluded: Sequence[str]) -> None:
|
|
28
|
+
"""Copy the tree keeping only ``.py`` files, excluded branches pruned."""
|
|
29
|
+
|
|
30
|
+
def ignore(directory: str, names: list[str]) -> set[str]:
|
|
31
|
+
rejected = set()
|
|
32
|
+
for name in names:
|
|
33
|
+
if (Path(directory) / name).is_dir():
|
|
34
|
+
if is_excluded(name, excluded):
|
|
35
|
+
rejected.add(name)
|
|
36
|
+
elif not name.endswith(".py"):
|
|
37
|
+
rejected.add(name)
|
|
38
|
+
return rejected
|
|
39
|
+
|
|
40
|
+
shutil.copytree(target, into, ignore=ignore, symlinks=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _module_paths(target: Path, files: Iterable[Path]) -> list[str]:
|
|
44
|
+
"""Dotted module paths, relative to the source root."""
|
|
45
|
+
names = set()
|
|
46
|
+
for path in files:
|
|
47
|
+
if path.name == "__init__.py":
|
|
48
|
+
continue
|
|
49
|
+
relative = path.relative_to(target).with_suffix("").as_posix().replace("/", ".")
|
|
50
|
+
names.add(f"{target.name}.{relative}")
|
|
51
|
+
return sorted(names)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def execute(settings: Settings) -> dict[str, Any]:
|
|
55
|
+
"""Write ``tach.mmd``. Returns ``{mermaid, deps, modules, truncated, log}``."""
|
|
56
|
+
empty: dict[str, Any] = {"mermaid": None, "deps": {}, "modules": 0, "truncated": 0}
|
|
57
|
+
base = runner.resolve("tach")
|
|
58
|
+
if not base:
|
|
59
|
+
return dict(empty, log="tach is not installed")
|
|
60
|
+
|
|
61
|
+
every = _module_paths(settings.target, settings.files())
|
|
62
|
+
if not every:
|
|
63
|
+
return dict(empty, log="no module to declare")
|
|
64
|
+
kept = every[:MAX_MODULES]
|
|
65
|
+
|
|
66
|
+
with tempfile.TemporaryDirectory(prefix="pymap-tach-") as tmp:
|
|
67
|
+
workspace = Path(tmp)
|
|
68
|
+
try:
|
|
69
|
+
_copy_sources(settings.target, workspace / settings.target.name, settings.excluded)
|
|
70
|
+
except OSError as exc:
|
|
71
|
+
return dict(empty, log=f"could not copy sources: {exc}")
|
|
72
|
+
|
|
73
|
+
lines = ['source_roots = ["."]\n']
|
|
74
|
+
lines += [f'[[modules]]\npath = "{m}"\ndepends_on = []\n\n' for m in kept]
|
|
75
|
+
(workspace / "tach.toml").write_text("".join(lines), encoding="utf-8")
|
|
76
|
+
|
|
77
|
+
ok, log = runner.run([*base, "sync"], cwd=str(workspace), timeout=settings.timeout)
|
|
78
|
+
if not ok:
|
|
79
|
+
return dict(empty, log=log or "tach sync failed")
|
|
80
|
+
|
|
81
|
+
mermaid = settings.output / "tach.mmd"
|
|
82
|
+
drawn, _ = runner.run(
|
|
83
|
+
[*base, "show", "--mermaid", "-o", str(mermaid)],
|
|
84
|
+
cwd=str(workspace),
|
|
85
|
+
timeout=settings.timeout,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
deps: dict[str, list[str]] = {}
|
|
89
|
+
mapped, raw = runner.run(
|
|
90
|
+
[*base, "map", "-o", "-"], cwd=str(workspace), timeout=settings.timeout, full=True
|
|
91
|
+
)
|
|
92
|
+
if mapped:
|
|
93
|
+
try:
|
|
94
|
+
deps = json.loads(raw[raw.index("{") : raw.rindex("}") + 1])
|
|
95
|
+
except (ValueError, json.JSONDecodeError):
|
|
96
|
+
deps = {}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"mermaid": mermaid if (drawn and mermaid.exists()) else None,
|
|
100
|
+
"deps": deps,
|
|
101
|
+
"modules": len(kept),
|
|
102
|
+
"truncated": len(every) - len(kept),
|
|
103
|
+
"log": "",
|
|
104
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pymap-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Map a Python codebase in one command.
|
|
5
|
+
Project-URL: Homepage, https://github.com/hermann225-zrouama/pymap-cli
|
|
6
|
+
Project-URL: Issues, https://github.com/hermann225-zrouama/pymap-cli/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/hermann225-zrouama/pymap-cli/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Franck Zrouama
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: architecture,ast,call graph,codebase map,documentation
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: code2flow>=2.5; extra == 'dev'
|
|
26
|
+
Requires-Dist: pydeps>=1.11; extra == 'dev'
|
|
27
|
+
Requires-Dist: pylint>=2.15; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
30
|
+
Requires-Dist: tach>=0.9; extra == 'dev'
|
|
31
|
+
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
|
|
32
|
+
Provides-Extra: graphs
|
|
33
|
+
Requires-Dist: code2flow>=2.5; extra == 'graphs'
|
|
34
|
+
Requires-Dist: pydeps>=1.11; extra == 'graphs'
|
|
35
|
+
Requires-Dist: pylint>=2.15; extra == 'graphs'
|
|
36
|
+
Requires-Dist: tach>=0.9; extra == 'graphs'
|
|
37
|
+
Provides-Extra: toml
|
|
38
|
+
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'toml'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# pymap
|
|
42
|
+
|
|
43
|
+
Map a Python codebase in one command. No service, no upload: everything is
|
|
44
|
+
analysed and rendered locally.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd my-project
|
|
48
|
+
pymap --open
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
pymap guesses which package to analyse, runs whichever graph tools you have
|
|
52
|
+
installed, and assembles the result into a single page — plus a **flow
|
|
53
|
+
explorer** that pymap builds itself: for every function, the order of the calls,
|
|
54
|
+
the conditions, the loops, the error paths, and the data flowing through.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pipx install pymap-cli # isolated, available everywhere
|
|
60
|
+
# or, inside the project you want to map:
|
|
61
|
+
pip install pymap-cli
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The core depends only on the standard library, so installing pymap into a
|
|
65
|
+
project adds **no version constraint** to it. The graph tools are optional and
|
|
66
|
+
live behind an extra:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install "pymap-cli[graphs]" # pydeps, code2flow, pylint, tach
|
|
70
|
+
brew install graphviz # or: apt install graphviz
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Any missing tool is reported at start-up and then skipped; the explorer always
|
|
74
|
+
works.
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pymap # guess the current project's package
|
|
80
|
+
pymap src/mypkg # explicit target
|
|
81
|
+
pymap src/mypkg -o map/ --open # output directory, open when done
|
|
82
|
+
pymap --exclude "generated_*" # skip more paths
|
|
83
|
+
pymap --editor pycharm # code links for another editor
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
How the target is guessed, in order: `[tool.pymap] target` in `pyproject.toml`,
|
|
87
|
+
a `src/<package>/` layout, the package named after the project, then the single
|
|
88
|
+
top-level package. If nothing stands out, pymap says so and waits for a path.
|
|
89
|
+
|
|
90
|
+
### Configuration
|
|
91
|
+
|
|
92
|
+
Optional, in the project's `pyproject.toml` (read on Python ≥ 3.11, or with
|
|
93
|
+
`tomli` installed):
|
|
94
|
+
|
|
95
|
+
```toml
|
|
96
|
+
[tool.pymap]
|
|
97
|
+
target = "src/mypkg"
|
|
98
|
+
output = "pymap-out"
|
|
99
|
+
editor = "vscode" # vscodium, cursor, windsurf, zed, pycharm,
|
|
100
|
+
# idea, sublime, none, or "myeditor://{f}:{l}"
|
|
101
|
+
exclude = ["vendor", "generated_*"]
|
|
102
|
+
timeout = 300 # seconds per external tool
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### As a library
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pymap import Settings, map_codebase
|
|
109
|
+
|
|
110
|
+
report = map_codebase(Settings(target="src/mypkg", output="map/"))
|
|
111
|
+
print(report.coverage, "% documented,", len(report.cycles), "cycles")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## What the map contains
|
|
115
|
+
|
|
116
|
+
| View | Source | What you read there |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| **Walkthrough tree** | pymap (AST) | The execution flow function by function, keyboard-navigable |
|
|
119
|
+
| Imports between modules | pydeps | The layers, and the modules everyone pulls in |
|
|
120
|
+
| Classes and inheritance | pyreverse | Attributes and hierarchies, when the code is object-oriented |
|
|
121
|
+
| Module boundaries | tach | File-by-file dependencies, cycles, Mermaid source |
|
|
122
|
+
|
|
123
|
+
The output directory (`pymap-out/` by default) holds `index.html` — the page to
|
|
124
|
+
open — and the views it links to.
|
|
125
|
+
|
|
126
|
+
### In the explorer
|
|
127
|
+
|
|
128
|
+
<kbd>↓</kbd> <kbd>↑</kbd> walk the steps · <kbd>↵</kbd> step into the
|
|
129
|
+
highlighted call · <kbd>←</kbd> go back up · <kbd>m</kbd> mark seen ·
|
|
130
|
+
<kbd>o</kbd> open in the editor · <kbd>/</kbd> search
|
|
131
|
+
|
|
132
|
+
## How the code is organised
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
src/pymap/
|
|
136
|
+
├── cli.py command line: arguments, pyproject, messages
|
|
137
|
+
├── settings.py what to analyse, where to write, what to skip
|
|
138
|
+
├── mapper.py orchestration + library API
|
|
139
|
+
├── runner.py launching external tools, detecting missing ones
|
|
140
|
+
├── render.py templates → HTML pages
|
|
141
|
+
├── analysis/ what pymap works out on its own, from the AST
|
|
142
|
+
│ ├── symbols.py modules, classes, functions, signatures
|
|
143
|
+
│ ├── flow.py execution tree of a function
|
|
144
|
+
│ ├── calls.py code2flow's call graph
|
|
145
|
+
│ └── cycles.py circular dependencies
|
|
146
|
+
├── tools/ one module per external tool
|
|
147
|
+
│ ├── pydeps.py pyreverse.py code2flow.py tach.py
|
|
148
|
+
└── templates/ index.html, section.html, explorer.html
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Adding a tool: drop a module in `tools/` exposing `execute(settings)` that
|
|
152
|
+
returns raw facts (never HTML), then add one line to `STEPS` and one section to
|
|
153
|
+
`_sections()`, both in `mapper.py`. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
154
|
+
|
|
155
|
+
## Guarantees
|
|
156
|
+
|
|
157
|
+
- **Nothing is executed** from the analysed code: everything goes through the
|
|
158
|
+
standard library's AST.
|
|
159
|
+
- **Nothing is written** into the target project. `tach` requires a `tach.toml`
|
|
160
|
+
at the root of wherever it runs, so pymap copies the sources into a temporary
|
|
161
|
+
directory instead of dropping that file in your tree.
|
|
162
|
+
- Environments and caches (`.venv/`, `node_modules/`, `build/`,
|
|
163
|
+
`__pycache__/`, …) are pruned during the walk, never traversed.
|
|
164
|
+
|
|
165
|
+
## Known limitations
|
|
166
|
+
|
|
167
|
+
- **Files sharing a basename share a namespace.** Symbol keys are
|
|
168
|
+
`<file stem>::<qualified name>`, because that is how code2flow names its
|
|
169
|
+
graph nodes and it is what lets the two data sets be joined. In a project
|
|
170
|
+
with `app/models.py` and `blog/models.py`, their symbols merge in the
|
|
171
|
+
explorer. pymap prints a note when it detects the case.
|
|
172
|
+
- **tach declares at most 60 modules.** Beyond that `tach sync` gets very slow
|
|
173
|
+
for a graph that is already unreadable. The count left out is reported.
|
|
174
|
+
- The call graph is only as good as code2flow's static resolution: calls
|
|
175
|
+
through dynamic dispatch or `getattr` do not appear.
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
git clone https://github.com/hermann225-zrouama/pymap-cli
|
|
181
|
+
cd pymap-cli
|
|
182
|
+
pip install -e ".[dev]"
|
|
183
|
+
pytest
|
|
184
|
+
ruff check src tests && ruff format --check src tests
|
|
185
|
+
pymap # pymap maps itself
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`tests/test_contract.py` pins the key names shared between the Python payload
|
|
189
|
+
and `templates/explorer.html`. A rename on one side without the other produces
|
|
190
|
+
a blank page rather than an error, so that test is what keeps them honest.
|
|
191
|
+
|
|
192
|
+
## Licence
|
|
193
|
+
|
|
194
|
+
MIT — see [LICENSE](LICENSE).
|
|
195
|
+
|
|
196
|
+
The distribution is named `pymap-cli` because `pymap` is already taken on PyPI
|
|
197
|
+
by an IMAP library. The command and the import name are both `pymap`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
pymap/__init__.py,sha256=C1jcrwBVVCpCK6fNFLdbZohI0L99BMVgA5JWDN_w6YQ,1223
|
|
2
|
+
pymap/__main__.py,sha256=LnOY_l14VeZ9NVefVstic4CxBNVaq9H4dUdgz4S3bHk,169
|
|
3
|
+
pymap/cli.py,sha256=O8rGgY8MC_8jYFeuSl4n6qAP7uJjAcnsR-GG2y7k7l0,4718
|
|
4
|
+
pymap/mapper.py,sha256=bFcdsImrGbxSNPzb5vjVVBnngbjgcATMGLsvpcIuDqc,7229
|
|
5
|
+
pymap/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
pymap/render.py,sha256=ctnR5yEtiHkc-Aj0L1MNlf_rM4HIEWZg7q9k5sez9gY,5249
|
|
7
|
+
pymap/runner.py,sha256=TB-8WDQuw6Q2IFqxUhxG0etkswWCOtmxnO-X_Kb3HbA,3302
|
|
8
|
+
pymap/settings.py,sha256=P_cML1kHCBgaGRMaaB89CHUfaC5tdzlo9aya7tUV8uQ,6025
|
|
9
|
+
pymap/analysis/__init__.py,sha256=sQkLkcimHcjMabQoBq74N2p7jl4suuVRUbmMMk-4Egk,80
|
|
10
|
+
pymap/analysis/calls.py,sha256=K_vC7v4e0Cgb_1Xo1kDmlgxpk9omBsJFbk9wfeKHinU,1319
|
|
11
|
+
pymap/analysis/cycles.py,sha256=YuKIYs2LHRnkEt46AnzlzDq3NApf8FONT76LcVyggtE,1342
|
|
12
|
+
pymap/analysis/flow.py,sha256=o__gpjM7qmipfeMa7q5a0eYWiwYKR-dOiEIgA1pqRR0,6453
|
|
13
|
+
pymap/analysis/symbols.py,sha256=wOqHGDHmGuXWy77KQWqDG74HJwgnt76uqUZY1rd3fM4,6884
|
|
14
|
+
pymap/templates/__init__.py,sha256=u1GeOJbGJC3VdLp1WiZN-Xwu52tNK2RElXJ8e0j230A,437
|
|
15
|
+
pymap/templates/explorer.html,sha256=XJLJh7bh-0ucu2ZlY55yDLaHkby7g0ReJJCkSdbRYjQ,37734
|
|
16
|
+
pymap/templates/index.html,sha256=tdtChG7Oipp7S5KWjaebMcpftkNgloTp9rhIcYkgxZo,1760
|
|
17
|
+
pymap/templates/section.html,sha256=bFB1E2mvl89XuzRPZO3K0oNEi8M_V4VPsre9XxEubkU,140
|
|
18
|
+
pymap/tools/__init__.py,sha256=a7_ToNuCq6Rc08Vi1-v919XQPghdfbt1a8OHcJ-jjYc,749
|
|
19
|
+
pymap/tools/code2flow.py,sha256=XxjZRCxaAK38FVKGM0DOTU1Dds4JTiw8G67nmtmWiGo,1889
|
|
20
|
+
pymap/tools/pydeps.py,sha256=NLq1ogy0jkAyhY7hNDUwifkH5ck5i3Q31LfwMkgeQog,1207
|
|
21
|
+
pymap/tools/pyreverse.py,sha256=aOoNnL7E3JC31UA6qbT1mnR4OaFObExHHljKbGjp3_0,1361
|
|
22
|
+
pymap/tools/tach.py,sha256=JknllG_rWIGr-FjPNGDsG7gwmWGyvrdZgX2tVmsssxc,3669
|
|
23
|
+
pymap_cli-0.3.0.dist-info/METADATA,sha256=sBo9Zim92TSOtpDzV3Vz7sGSG5Vs8vip2KAI6TxYF_E,7674
|
|
24
|
+
pymap_cli-0.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
25
|
+
pymap_cli-0.3.0.dist-info/entry_points.txt,sha256=fIJReyb8moS3dUNAxWnMTQan-joX10DqqmZs2Esym3k,41
|
|
26
|
+
pymap_cli-0.3.0.dist-info/licenses/LICENSE,sha256=7BJhC6lQ3aGXwDNyJcE9U2MhD9I0krkxJKrSZAX8RlI,1071
|
|
27
|
+
pymap_cli-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Franck Zrouama
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|