classmap-hanthink 0.1.0__tar.gz → 0.2.0__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 (20) hide show
  1. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/CHANGELOG.md +10 -0
  2. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/PKG-INFO +19 -1
  3. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/README.md +18 -0
  4. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/pyproject.toml +1 -1
  5. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/src/classmap_hanthink/__init__.py +2 -1
  6. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/src/classmap_hanthink/cli.py +7 -5
  7. classmap_hanthink-0.2.0/src/classmap_hanthink/render.py +144 -0
  8. classmap_hanthink-0.2.0/src/classmap_hanthink/tree.py +72 -0
  9. classmap_hanthink-0.2.0/tests/test_external.py +36 -0
  10. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/uv.lock +1 -1
  11. classmap_hanthink-0.1.0/src/classmap_hanthink/render.py +0 -99
  12. classmap_hanthink-0.1.0/src/classmap_hanthink/tree.py +0 -35
  13. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/.gitignore +0 -0
  14. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/LICENSE +0 -0
  15. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/examples/sample_classes.py +0 -0
  16. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/src/classmap_hanthink/model.py +0 -0
  17. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/src/classmap_hanthink/scanner.py +0 -0
  18. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/tests/test_render.py +0 -0
  19. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/tests/test_scanner.py +0 -0
  20. {classmap_hanthink-0.1.0 → classmap_hanthink-0.2.0}/tests/test_tree.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0] - 2026-06-04
4
+
5
+ ### Added
6
+
7
+ - Added `--show-external` CLI option.
8
+ - Added external and built-in base class detection.
9
+ - Added `External Base Classes` output section.
10
+ - Added external base support for Mermaid and explanation output.
11
+
12
+
3
13
  ## [0.1.0] - 2026-06-04
4
14
 
5
15
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classmap-hanthink
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Scan Python class inheritance relationships and render them as readable maps.
5
5
  Project-URL: Homepage, https://github.com/Han-think/classmap-hanthink
6
6
  Author: Han-think
@@ -106,3 +106,21 @@ uv build
106
106
  - Render optional explanation output
107
107
  - Render optional Mermaid classDiagram output
108
108
  - Provide CLI command: `classmap`
109
+
110
+
111
+ ## External Base Classes
112
+
113
+ Use `--show-external` to show classes that inherit from external or built-in base classes.
114
+
115
+ ```bash
116
+ classmap examples/sample_classes.py --show-external
117
+ ```
118
+
119
+ Example output:
120
+
121
+ ```text
122
+ External Base Classes
123
+
124
+ dict
125
+ └── ExternalChild [examples/sample_classes.py:21]
126
+ ```
@@ -92,3 +92,21 @@ uv build
92
92
  - Render optional explanation output
93
93
  - Render optional Mermaid classDiagram output
94
94
  - Provide CLI command: `classmap`
95
+
96
+
97
+ ## External Base Classes
98
+
99
+ Use `--show-external` to show classes that inherit from external or built-in base classes.
100
+
101
+ ```bash
102
+ classmap examples/sample_classes.py --show-external
103
+ ```
104
+
105
+ Example output:
106
+
107
+ ```text
108
+ External Base Classes
109
+
110
+ dict
111
+ └── ExternalChild [examples/sample_classes.py:21]
112
+ ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "classmap-hanthink"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  authors = [{ name = "Han-think" }]
9
9
  description = "Scan Python class inheritance relationships and render them as readable maps."
10
10
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  from .model import ClassInfo
2
2
  from .scanner import scan_file, scan_path
3
- from .tree import build_inheritance_map, find_roots, find_independent_classes
3
+ from .tree import build_inheritance_map, build_external_base_map, find_roots, find_independent_classes
4
4
  from .render import render_text_tree, render_explanation, render_mermaid
5
5
 
6
6
  __all__ = [
@@ -8,6 +8,7 @@ __all__ = [
8
8
  "scan_file",
9
9
  "scan_path",
10
10
  "build_inheritance_map",
11
+ "build_external_base_map",
11
12
  "find_roots",
12
13
  "find_independent_classes",
13
14
  "render_text_tree",
@@ -2,7 +2,7 @@ import argparse
2
2
  from pathlib import Path
3
3
 
4
4
  from .scanner import scan_path
5
- from .tree import build_inheritance_map
5
+ from .tree import build_external_base_map, build_inheritance_map
6
6
  from .render import render_text_tree, render_explanation, render_mermaid
7
7
 
8
8
 
@@ -14,18 +14,20 @@ def main(argv: list[str] | None = None) -> int:
14
14
  parser.add_argument("target", nargs="?", default=".", help="Python file or folder to scan.")
15
15
  parser.add_argument("--explain", action="store_true", help="Show beginner-friendly explanation.")
16
16
  parser.add_argument("--mermaid", action="store_true", help="Render Mermaid classDiagram output.")
17
+ parser.add_argument("--show-external", action="store_true", help="Show external or built-in base classes.")
17
18
 
18
19
  args = parser.parse_args(argv)
19
20
 
20
21
  target = Path(args.target)
21
22
  classes = scan_path(target)
22
23
  tree = build_inheritance_map(classes)
24
+ external_map = build_external_base_map(classes)
23
25
 
24
26
  if args.mermaid:
25
- print(render_mermaid(classes, tree))
27
+ print(render_mermaid(classes, tree, external_map, show_external=args.show_external))
26
28
  if args.explain:
27
29
  print()
28
- print(render_explanation(classes, tree))
30
+ print(render_explanation(classes, tree, external_map, show_external=args.show_external))
29
31
  return 0
30
32
 
31
33
  print("classmap report")
@@ -34,11 +36,11 @@ def main(argv: list[str] | None = None) -> int:
34
36
  print(f"Resolved path: {target.resolve()}")
35
37
  print(f"Classes found: {len(classes)}")
36
38
  print()
37
- print(render_text_tree(classes, tree))
39
+ print(render_text_tree(classes, tree, external_map, show_external=args.show_external))
38
40
 
39
41
  if args.explain:
40
42
  print()
41
- print(render_explanation(classes, tree))
43
+ print(render_explanation(classes, tree, external_map, show_external=args.show_external))
42
44
 
43
45
  return 0
44
46
 
@@ -0,0 +1,144 @@
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(_render_node(child, tree, locations, prefix=prefix, is_last=index == len(children) - 1, is_root=False))
25
+
26
+ return lines
27
+
28
+
29
+ def render_external_bases(external_map: dict[str, list[str]], locations: dict[str, str] | None = None) -> str:
30
+ """Render external or built-in base class relationships."""
31
+ if not external_map:
32
+ return ""
33
+
34
+ locations = locations or {}
35
+ lines = ["External Base Classes", ""]
36
+
37
+ for base, children in sorted(external_map.items()):
38
+ lines.append(base)
39
+ for index, child in enumerate(children):
40
+ connector = "└── " if index == len(children) - 1 else "├── "
41
+ lines.append(f"{connector}{child} [{locations.get(child, 'unknown')}]")
42
+ lines.append("")
43
+
44
+ return "\n".join(lines).rstrip()
45
+
46
+
47
+ def render_text_tree(
48
+ classes: list[ClassInfo],
49
+ tree: dict[str, list[str]],
50
+ external_map: dict[str, list[str]] | None = None,
51
+ *,
52
+ show_external: bool = False,
53
+ ) -> str:
54
+ """Render the inheritance map as a readable text tree."""
55
+ if not classes:
56
+ return "No classes found."
57
+
58
+ locations = _location_map(classes)
59
+ roots = find_roots(classes, tree)
60
+ independent = find_independent_classes(classes, tree, external_map, exclude_external=show_external)
61
+
62
+ lines: list[str] = ["Inheritance Tree", ""]
63
+
64
+ if roots:
65
+ for root in roots:
66
+ lines.extend(_render_node(root, tree, locations))
67
+ lines.append("")
68
+ else:
69
+ lines.append("No inheritance relationships found.")
70
+ lines.append("")
71
+
72
+ if show_external and external_map:
73
+ lines.append(render_external_bases(external_map, locations))
74
+ lines.append("")
75
+
76
+ if independent:
77
+ lines.append("Independent Classes")
78
+ lines.append("")
79
+ for name in independent:
80
+ lines.append(f"- {name} [{locations.get(name, 'unknown')}]")
81
+ lines.append("")
82
+
83
+ return "\n".join(lines).rstrip()
84
+
85
+
86
+ def render_explanation(
87
+ classes: list[ClassInfo],
88
+ tree: dict[str, list[str]],
89
+ external_map: dict[str, list[str]] | None = None,
90
+ *,
91
+ show_external: bool = False,
92
+ ) -> str:
93
+ """Render beginner-friendly explanation text."""
94
+ if not classes:
95
+ return "No classes were found in the target."
96
+
97
+ lines = ["Explanation", ""]
98
+
99
+ for parent, children in sorted(tree.items()):
100
+ joined = ", ".join(children)
101
+ lines.append(f"- {parent} has {len(children)} child class(es): {joined}")
102
+
103
+ if show_external and external_map:
104
+ for base, children in sorted(external_map.items()):
105
+ joined = ", ".join(children)
106
+ lines.append(f"- {base} is an external or built-in base class for: {joined}")
107
+
108
+ independent = find_independent_classes(classes, tree, external_map, exclude_external=show_external)
109
+ for name in independent:
110
+ lines.append(f"- {name} is independent.")
111
+
112
+ if len(lines) == 2:
113
+ lines.append("- Classes were found, but no inheritance relationships were detected.")
114
+
115
+ return "\n".join(lines)
116
+
117
+
118
+ def render_mermaid(
119
+ classes: list[ClassInfo],
120
+ tree: dict[str, list[str]],
121
+ external_map: dict[str, list[str]] | None = None,
122
+ *,
123
+ show_external: bool = False,
124
+ ) -> str:
125
+ """Render the inheritance map as Mermaid classDiagram syntax."""
126
+ lines = ["classDiagram"]
127
+ added_edge = False
128
+
129
+ for parent, children in sorted(tree.items()):
130
+ for child in children:
131
+ lines.append(f" {parent} <|-- {child}")
132
+ added_edge = True
133
+
134
+ if show_external and external_map:
135
+ for base, children in sorted(external_map.items()):
136
+ for child in children:
137
+ lines.append(f" {base} <|-- {child}")
138
+ added_edge = True
139
+
140
+ if not added_edge:
141
+ for item in classes:
142
+ lines.append(f" class {item.name}")
143
+
144
+ return "\n".join(lines)
@@ -0,0 +1,72 @@
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
+ """Build a parent-to-children inheritance map using only scanned classes."""
8
+ known_names = {item.name for item in classes}
9
+ children: dict[str, list[str]] = defaultdict(list)
10
+
11
+ for item in classes:
12
+ for base in item.bases:
13
+ short_base = base.split(".")[-1]
14
+ if short_base in known_names:
15
+ children[short_base].append(item.name)
16
+
17
+ return {parent: sorted(child_list) for parent, child_list in children.items()}
18
+
19
+
20
+ def build_external_base_map(classes: list[ClassInfo]) -> dict[str, list[str]]:
21
+ """Build an external-base-to-children map.
22
+
23
+ External base classes are base classes that appear in class definitions
24
+ but are not defined inside the scanned target.
25
+ """
26
+ known_names = {item.name for item in classes}
27
+ external: dict[str, list[str]] = defaultdict(list)
28
+
29
+ for item in classes:
30
+ for base in item.bases:
31
+ short_base = base.split(".")[-1]
32
+ if short_base not in known_names:
33
+ external[base].append(item.name)
34
+
35
+ return {base: sorted(child_list) for base, child_list in sorted(external.items())}
36
+
37
+
38
+ def find_roots(classes: list[ClassInfo], tree: dict[str, list[str]]) -> list[str]:
39
+ """Find root classes that have internal children but no internal parent."""
40
+ child_names = {child for children in tree.values() for child in children}
41
+ roots = [item.name for item in classes if item.name in tree and item.name not in child_names]
42
+ return sorted(set(roots))
43
+
44
+
45
+ def find_independent_classes(
46
+ classes: list[ClassInfo],
47
+ tree: dict[str, list[str]],
48
+ external_map: dict[str, list[str]] | None = None,
49
+ *,
50
+ exclude_external: bool = False,
51
+ ) -> list[str]:
52
+ """Find classes that are not connected to known inheritance relationships."""
53
+ known_names = {item.name for item in classes}
54
+ child_names = {child for children in tree.values() for child in children}
55
+ parent_names = set(tree.keys())
56
+ external_children = set()
57
+
58
+ if exclude_external and external_map:
59
+ external_children = {child for children in external_map.values() for child in children}
60
+
61
+ independent = []
62
+
63
+ for item in classes:
64
+ has_known_parent = any(base.split(".")[-1] in known_names for base in item.bases)
65
+ has_child = item.name in parent_names
66
+ is_child = item.name in child_names
67
+ has_external_parent = item.name in external_children
68
+
69
+ if not has_known_parent and not has_child and not is_child and not has_external_parent:
70
+ independent.append(item.name)
71
+
72
+ return sorted(independent)
@@ -0,0 +1,36 @@
1
+ from classmap_hanthink.model import ClassInfo
2
+ from classmap_hanthink.tree import build_external_base_map, build_inheritance_map, find_independent_classes
3
+ from classmap_hanthink.render import render_text_tree, render_mermaid
4
+
5
+
6
+ def test_build_external_base_map():
7
+ classes = [ClassInfo("Animal"), ClassInfo("Dog", ("Animal",)), ClassInfo("ExternalChild", ("dict",))]
8
+ external = build_external_base_map(classes)
9
+ assert external == {"dict": ["ExternalChild"]}
10
+
11
+
12
+ def test_external_child_removed_from_independent_when_enabled():
13
+ classes = [ClassInfo("Config"), ClassInfo("ExternalChild", ("dict",))]
14
+ tree = build_inheritance_map(classes)
15
+ external = build_external_base_map(classes)
16
+ independent = find_independent_classes(classes, tree, external, exclude_external=True)
17
+ assert independent == ["Config"]
18
+
19
+
20
+ def test_render_text_tree_show_external():
21
+ classes = [ClassInfo("ExternalChild", ("dict",), file="sample.py", line=1)]
22
+ tree = build_inheritance_map(classes)
23
+ external = build_external_base_map(classes)
24
+ output = render_text_tree(classes, tree, external, show_external=True)
25
+ assert "External Base Classes" in output
26
+ assert "dict" in output
27
+ assert "ExternalChild [sample.py:1]" in output
28
+ assert "Independent Classes" not in output
29
+
30
+
31
+ def test_render_mermaid_show_external():
32
+ classes = [ClassInfo("ExternalChild", ("dict",))]
33
+ tree = build_inheritance_map(classes)
34
+ external = build_external_base_map(classes)
35
+ output = render_mermaid(classes, tree, external, show_external=True)
36
+ assert "dict <|-- ExternalChild" in output
@@ -4,7 +4,7 @@ requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
6
6
  name = "classmap-hanthink"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  source = { editable = "." }
9
9
 
10
10
  [package.dev-dependencies]
@@ -1,99 +0,0 @@
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)
@@ -1,35 +0,0 @@
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)