classmap-hanthink 0.1.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.
@@ -0,0 +1,16 @@
1
+ from .model import ClassInfo
2
+ from .scanner import scan_file, scan_path
3
+ from .tree import build_inheritance_map, find_roots, find_independent_classes
4
+ from .render import render_text_tree, render_explanation, render_mermaid
5
+
6
+ __all__ = [
7
+ "ClassInfo",
8
+ "scan_file",
9
+ "scan_path",
10
+ "build_inheritance_map",
11
+ "find_roots",
12
+ "find_independent_classes",
13
+ "render_text_tree",
14
+ "render_explanation",
15
+ "render_mermaid",
16
+ ]
@@ -0,0 +1,47 @@
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from .scanner import scan_path
5
+ from .tree import build_inheritance_map
6
+ from .render import render_text_tree, render_explanation, render_mermaid
7
+
8
+
9
+ def main(argv: list[str] | None = None) -> int:
10
+ parser = argparse.ArgumentParser(
11
+ prog="classmap",
12
+ description="Scan Python classes and show inheritance relationships as a tree.",
13
+ )
14
+ parser.add_argument("target", nargs="?", default=".", help="Python file or folder to scan.")
15
+ parser.add_argument("--explain", action="store_true", help="Show beginner-friendly explanation.")
16
+ parser.add_argument("--mermaid", action="store_true", help="Render Mermaid classDiagram output.")
17
+
18
+ args = parser.parse_args(argv)
19
+
20
+ target = Path(args.target)
21
+ classes = scan_path(target)
22
+ tree = build_inheritance_map(classes)
23
+
24
+ if args.mermaid:
25
+ print(render_mermaid(classes, tree))
26
+ if args.explain:
27
+ print()
28
+ print(render_explanation(classes, tree))
29
+ return 0
30
+
31
+ print("classmap report")
32
+ print()
33
+ print(f"Target: {target}")
34
+ print(f"Resolved path: {target.resolve()}")
35
+ print(f"Classes found: {len(classes)}")
36
+ print()
37
+ print(render_text_tree(classes, tree))
38
+
39
+ if args.explain:
40
+ print()
41
+ print(render_explanation(classes, tree))
42
+
43
+ return 0
44
+
45
+
46
+ if __name__ == "__main__":
47
+ raise SystemExit(main())
@@ -0,0 +1,19 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class ClassInfo:
6
+ """Information about one Python class definition."""
7
+
8
+ name: str
9
+ bases: tuple[str, ...] = field(default_factory=tuple)
10
+ file: str = ""
11
+ line: int = 0
12
+
13
+ @property
14
+ def location(self) -> str:
15
+ if self.file and self.line:
16
+ return f"{self.file}:{self.line}"
17
+ if self.file:
18
+ return self.file
19
+ return "unknown"
@@ -0,0 +1,99 @@
1
+ from .model import ClassInfo
2
+ from .tree import find_independent_classes, find_roots
3
+
4
+
5
+ def _location_map(classes: list[ClassInfo]) -> dict[str, str]:
6
+ return {item.name: item.location for item in classes}
7
+
8
+
9
+ def _render_node(name, tree, locations, prefix="", is_last=True, is_root=True) -> list[str]:
10
+ label = f"{name} [{locations.get(name, 'unknown')}]"
11
+
12
+ if is_root:
13
+ lines = [label]
14
+ else:
15
+ connector = "└── " if is_last else "├── "
16
+ lines = [prefix + connector + label]
17
+
18
+ children = tree.get(name, [])
19
+
20
+ if not is_root:
21
+ prefix = prefix + (" " if is_last else "│ ")
22
+
23
+ for index, child in enumerate(children):
24
+ lines.extend(
25
+ _render_node(
26
+ child,
27
+ tree,
28
+ locations,
29
+ prefix=prefix,
30
+ is_last=index == len(children) - 1,
31
+ is_root=False,
32
+ )
33
+ )
34
+
35
+ return lines
36
+
37
+
38
+ def render_text_tree(classes: list[ClassInfo], tree: dict[str, list[str]]) -> str:
39
+ if not classes:
40
+ return "No classes found."
41
+
42
+ locations = _location_map(classes)
43
+ roots = find_roots(classes, tree)
44
+ independent = find_independent_classes(classes, tree)
45
+
46
+ lines: list[str] = ["Inheritance Tree", ""]
47
+
48
+ if roots:
49
+ for root in roots:
50
+ lines.extend(_render_node(root, tree, locations))
51
+ lines.append("")
52
+ else:
53
+ lines.append("No inheritance relationships found.")
54
+ lines.append("")
55
+
56
+ if independent:
57
+ lines.append("Independent Classes")
58
+ lines.append("")
59
+ for name in independent:
60
+ lines.append(f"- {name} [{locations.get(name, 'unknown')}]")
61
+ lines.append("")
62
+
63
+ return "\n".join(lines).rstrip()
64
+
65
+
66
+ def render_explanation(classes: list[ClassInfo], tree: dict[str, list[str]]) -> str:
67
+ if not classes:
68
+ return "No classes were found in the target."
69
+
70
+ lines = ["Explanation", ""]
71
+
72
+ for parent, children in sorted(tree.items()):
73
+ joined = ", ".join(children)
74
+ lines.append(f"- {parent} has {len(children)} child class(es): {joined}")
75
+
76
+ independent = find_independent_classes(classes, tree)
77
+ for name in independent:
78
+ lines.append(f"- {name} is independent.")
79
+
80
+ if len(lines) == 2:
81
+ lines.append("- Classes were found, but no inheritance relationships were detected.")
82
+
83
+ return "\n".join(lines)
84
+
85
+
86
+ def render_mermaid(classes: list[ClassInfo], tree: dict[str, list[str]]) -> str:
87
+ lines = ["classDiagram"]
88
+ added_edge = False
89
+
90
+ for parent, children in sorted(tree.items()):
91
+ for child in children:
92
+ lines.append(f" {parent} <|-- {child}")
93
+ added_edge = True
94
+
95
+ if not added_edge:
96
+ for item in classes:
97
+ lines.append(f" class {item.name}")
98
+
99
+ return "\n".join(lines)
@@ -0,0 +1,85 @@
1
+ import ast
2
+ from pathlib import Path
3
+
4
+ from .model import ClassInfo
5
+
6
+
7
+ DEFAULT_SKIP_DIRS = {
8
+ ".git", ".venv", "venv", "__pycache__", ".pytest_cache",
9
+ "dist", "build", ".mypy_cache", ".ruff_cache",
10
+ }
11
+
12
+
13
+ def _base_name(base: ast.expr) -> str | None:
14
+ if isinstance(base, ast.Name):
15
+ return base.id
16
+
17
+ if isinstance(base, ast.Attribute):
18
+ parts: list[str] = []
19
+ node: ast.AST = base
20
+ while isinstance(node, ast.Attribute):
21
+ parts.append(node.attr)
22
+ node = node.value
23
+ if isinstance(node, ast.Name):
24
+ parts.append(node.id)
25
+ return ".".join(reversed(parts))
26
+
27
+ if isinstance(base, ast.Subscript):
28
+ return _base_name(base.value)
29
+
30
+ return None
31
+
32
+
33
+ def scan_file(path: str | Path) -> list[ClassInfo]:
34
+ file_path = Path(path)
35
+
36
+ if file_path.suffix != ".py":
37
+ return []
38
+
39
+ source = file_path.read_text(encoding="utf-8")
40
+ tree = ast.parse(source, filename=str(file_path))
41
+
42
+ classes: list[ClassInfo] = []
43
+
44
+ for node in ast.walk(tree):
45
+ if isinstance(node, ast.ClassDef):
46
+ bases = tuple(
47
+ name for base in node.bases
48
+ if (name := _base_name(base)) is not None
49
+ )
50
+ classes.append(
51
+ ClassInfo(
52
+ name=node.name,
53
+ bases=bases,
54
+ file=str(file_path),
55
+ line=node.lineno,
56
+ )
57
+ )
58
+
59
+ return sorted(classes, key=lambda item: (item.file, item.line, item.name))
60
+
61
+
62
+ def _iter_python_files(path: Path) -> list[Path]:
63
+ if path.is_file():
64
+ return [path] if path.suffix == ".py" else []
65
+
66
+ files: list[Path] = []
67
+ for child in path.rglob("*.py"):
68
+ if any(part in DEFAULT_SKIP_DIRS for part in child.parts):
69
+ continue
70
+ files.append(child)
71
+
72
+ return sorted(files)
73
+
74
+
75
+ def scan_path(path: str | Path) -> list[ClassInfo]:
76
+ target = Path(path)
77
+
78
+ if not target.exists():
79
+ raise FileNotFoundError(f"Path does not exist: {target}")
80
+
81
+ classes: list[ClassInfo] = []
82
+ for file_path in _iter_python_files(target):
83
+ classes.extend(scan_file(file_path))
84
+
85
+ return sorted(classes, key=lambda item: (item.file, item.line, item.name))
@@ -0,0 +1,35 @@
1
+ from collections import defaultdict
2
+
3
+ from .model import ClassInfo
4
+
5
+
6
+ def build_inheritance_map(classes: list[ClassInfo]) -> dict[str, list[str]]:
7
+ known_names = {item.name for item in classes}
8
+ children: dict[str, list[str]] = defaultdict(list)
9
+
10
+ for item in classes:
11
+ for base in item.bases:
12
+ short_base = base.split(".")[-1]
13
+ if short_base in known_names:
14
+ children[short_base].append(item.name)
15
+
16
+ return {parent: sorted(child_list) for parent, child_list in children.items()}
17
+
18
+
19
+ def find_roots(classes: list[ClassInfo], tree: dict[str, list[str]]) -> list[str]:
20
+ child_names = {child for children in tree.values() for child in children}
21
+ roots = [item.name for item in classes if item.name in tree and item.name not in child_names]
22
+ return sorted(set(roots))
23
+
24
+
25
+ def find_independent_classes(classes: list[ClassInfo], tree: dict[str, list[str]]) -> list[str]:
26
+ known_names = {item.name for item in classes}
27
+ parent_names = set(tree.keys())
28
+ independent = []
29
+
30
+ for item in classes:
31
+ has_known_parent = any(base.split(".")[-1] in known_names for base in item.bases)
32
+ if not has_known_parent and item.name not in parent_names:
33
+ independent.append(item.name)
34
+
35
+ return sorted(independent)
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: classmap-hanthink
3
+ Version: 0.1.0
4
+ Summary: Scan Python class inheritance relationships and render them as readable maps.
5
+ Project-URL: Homepage, https://github.com/Han-think/classmap-hanthink
6
+ Author: Han-think
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # classmap-hanthink
16
+
17
+ `classmap-hanthink` is a small Python developer tool that scans Python files and shows class inheritance relationships as a readable tree.
18
+
19
+ Simple explanation:
20
+
21
+ > It helps you see which class is the parent and which classes are children.
22
+
23
+ Technical explanation:
24
+
25
+ > It parses Python source code with the standard `ast` module, extracts class definitions and base classes, builds a parent-to-child inheritance map, and renders the result as a text tree.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install classmap-hanthink
31
+ ```
32
+
33
+ ## CLI Usage
34
+
35
+ Scan the current folder:
36
+
37
+ ```bash
38
+ classmap .
39
+ ```
40
+
41
+ Scan a specific file:
42
+
43
+ ```bash
44
+ classmap examples/sample_classes.py
45
+ ```
46
+
47
+ Show explanation text:
48
+
49
+ ```bash
50
+ classmap . --explain
51
+ ```
52
+
53
+ Show Mermaid class diagram output:
54
+
55
+ ```bash
56
+ classmap . --mermaid
57
+ ```
58
+
59
+ ## Python Usage
60
+
61
+ ```python
62
+ from classmap_hanthink import scan_path, build_inheritance_map, render_text_tree
63
+
64
+ classes = scan_path("examples/sample_classes.py")
65
+ tree = build_inheritance_map(classes)
66
+ print(render_text_tree(classes, tree))
67
+ ```
68
+
69
+ ## Example Output
70
+
71
+ ```text
72
+ classmap report
73
+
74
+ Target: examples/sample_classes.py
75
+ Classes found: 6
76
+
77
+ Inheritance Tree
78
+
79
+ Animal [examples/sample_classes.py:1]
80
+ ├── Cat [examples/sample_classes.py:13]
81
+ └── Dog [examples/sample_classes.py:5]
82
+ └── Puppy [examples/sample_classes.py:9]
83
+
84
+ Independent Classes
85
+
86
+ - Config [examples/sample_classes.py:17]
87
+ - ExternalChild [examples/sample_classes.py:21]
88
+ ```
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ uv sync --group dev
94
+ uv run pytest tests/ -v
95
+ uv build
96
+ ```
97
+
98
+ ## Release Roadmap
99
+
100
+ ### v0.1.0
101
+
102
+ - Scan Python files and folders
103
+ - Extract class names, base classes, file paths, and line numbers
104
+ - Build parent-to-child inheritance relationships
105
+ - Render text tree output
106
+ - Render optional explanation output
107
+ - Render optional Mermaid classDiagram output
108
+ - Provide CLI command: `classmap`
@@ -0,0 +1,11 @@
1
+ classmap_hanthink/__init__.py,sha256=dramHDI-wpL7WCy6sdNIqI0uzhj4UlZGx3Wtp67EqFA,439
2
+ classmap_hanthink/cli.py,sha256=-vnCy4bnwaNazSG7-kvgQ0XgD5CDDNFSMgG_envG7wA,1398
3
+ classmap_hanthink/model.py,sha256=K_vV55Q6XpoFD-yVELr-dCGpNolkg23TwTNqSZHjjRM,455
4
+ classmap_hanthink/render.py,sha256=tr54hxLOH9VdpiYUQ9IFHKOAajNxz1_pWjve1jkztFY,2852
5
+ classmap_hanthink/scanner.py,sha256=jf7oVGVEJeNCJ81nWB8oM8P226TE_nfeQ7cqKK3tzhU,2260
6
+ classmap_hanthink/tree.py,sha256=Bun7WM_IDe5LGozWLC75j1ZAtlN8WK0z4RDGwp7LxpQ,1288
7
+ classmap_hanthink-0.1.0.dist-info/METADATA,sha256=slOrzOu-Msg_SRWF3Hl4t8fkz3nmLbroC9PUZcrOiY4,2337
8
+ classmap_hanthink-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
9
+ classmap_hanthink-0.1.0.dist-info/entry_points.txt,sha256=9abC2TA-GJJEVvCV-fmQS77Raj7hRu5RxxACijh0bJ4,56
10
+ classmap_hanthink-0.1.0.dist-info/licenses/LICENSE,sha256=NHEqsMLJRDBrcNXbBpojCsOinRqPsxZjpxgvb0ZP_7w,218
11
+ classmap_hanthink-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ classmap = classmap_hanthink.cli:main
@@ -0,0 +1,7 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Han-think
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files, to deal in the Software
7
+ without restriction.