classmap-hanthink 0.1.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.
- classmap_hanthink-0.1.0/.gitignore +7 -0
- classmap_hanthink-0.1.0/CHANGELOG.md +16 -0
- classmap_hanthink-0.1.0/LICENSE +7 -0
- classmap_hanthink-0.1.0/PKG-INFO +108 -0
- classmap_hanthink-0.1.0/README.md +94 -0
- classmap_hanthink-0.1.0/examples/sample_classes.py +22 -0
- classmap_hanthink-0.1.0/pyproject.toml +27 -0
- classmap_hanthink-0.1.0/src/classmap_hanthink/__init__.py +16 -0
- classmap_hanthink-0.1.0/src/classmap_hanthink/cli.py +47 -0
- classmap_hanthink-0.1.0/src/classmap_hanthink/model.py +19 -0
- classmap_hanthink-0.1.0/src/classmap_hanthink/render.py +99 -0
- classmap_hanthink-0.1.0/src/classmap_hanthink/scanner.py +85 -0
- classmap_hanthink-0.1.0/src/classmap_hanthink/tree.py +35 -0
- classmap_hanthink-0.1.0/tests/test_render.py +26 -0
- classmap_hanthink-0.1.0/tests/test_scanner.py +29 -0
- classmap_hanthink-0.1.0/tests/test_tree.py +25 -0
- classmap_hanthink-0.1.0/uv.lock +156 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-06-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added `ClassInfo` data model.
|
|
8
|
+
- Added Python AST-based scanner.
|
|
9
|
+
- Added recursive file and folder scanning.
|
|
10
|
+
- Added inheritance map builder.
|
|
11
|
+
- Added text tree renderer.
|
|
12
|
+
- Added independent class renderer.
|
|
13
|
+
- Added Mermaid class diagram renderer.
|
|
14
|
+
- Added CLI command: `classmap`.
|
|
15
|
+
- Added pytest test suite.
|
|
16
|
+
- Added example Python file.
|
|
@@ -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,94 @@
|
|
|
1
|
+
# classmap-hanthink
|
|
2
|
+
|
|
3
|
+
`classmap-hanthink` is a small Python developer tool that scans Python files and shows class inheritance relationships as a readable tree.
|
|
4
|
+
|
|
5
|
+
Simple explanation:
|
|
6
|
+
|
|
7
|
+
> It helps you see which class is the parent and which classes are children.
|
|
8
|
+
|
|
9
|
+
Technical explanation:
|
|
10
|
+
|
|
11
|
+
> 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.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install classmap-hanthink
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## CLI Usage
|
|
20
|
+
|
|
21
|
+
Scan the current folder:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
classmap .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Scan a specific file:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
classmap examples/sample_classes.py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Show explanation text:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
classmap . --explain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Show Mermaid class diagram output:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
classmap . --mermaid
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Python Usage
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from classmap_hanthink import scan_path, build_inheritance_map, render_text_tree
|
|
49
|
+
|
|
50
|
+
classes = scan_path("examples/sample_classes.py")
|
|
51
|
+
tree = build_inheritance_map(classes)
|
|
52
|
+
print(render_text_tree(classes, tree))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Example Output
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
classmap report
|
|
59
|
+
|
|
60
|
+
Target: examples/sample_classes.py
|
|
61
|
+
Classes found: 6
|
|
62
|
+
|
|
63
|
+
Inheritance Tree
|
|
64
|
+
|
|
65
|
+
Animal [examples/sample_classes.py:1]
|
|
66
|
+
├── Cat [examples/sample_classes.py:13]
|
|
67
|
+
└── Dog [examples/sample_classes.py:5]
|
|
68
|
+
└── Puppy [examples/sample_classes.py:9]
|
|
69
|
+
|
|
70
|
+
Independent Classes
|
|
71
|
+
|
|
72
|
+
- Config [examples/sample_classes.py:17]
|
|
73
|
+
- ExternalChild [examples/sample_classes.py:21]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv sync --group dev
|
|
80
|
+
uv run pytest tests/ -v
|
|
81
|
+
uv build
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Release Roadmap
|
|
85
|
+
|
|
86
|
+
### v0.1.0
|
|
87
|
+
|
|
88
|
+
- Scan Python files and folders
|
|
89
|
+
- Extract class names, base classes, file paths, and line numbers
|
|
90
|
+
- Build parent-to-child inheritance relationships
|
|
91
|
+
- Render text tree output
|
|
92
|
+
- Render optional explanation output
|
|
93
|
+
- Render optional Mermaid classDiagram output
|
|
94
|
+
- Provide CLI command: `classmap`
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "classmap-hanthink"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [{ name = "Han-think" }]
|
|
9
|
+
description = "Scan Python class inheritance relationships and render them as readable maps."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
dependencies = []
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/Han-think/classmap-hanthink"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
classmap = "classmap_hanthink.cli:main"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = ["pytest>=8.0"]
|
|
@@ -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,26 @@
|
|
|
1
|
+
from classmap_hanthink.model import ClassInfo
|
|
2
|
+
from classmap_hanthink.tree import build_inheritance_map
|
|
3
|
+
from classmap_hanthink.render import render_text_tree, render_mermaid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_render_text_tree():
|
|
7
|
+
classes = [
|
|
8
|
+
ClassInfo("Animal", file="sample.py", line=1),
|
|
9
|
+
ClassInfo("Dog", ("Animal",), file="sample.py", line=4),
|
|
10
|
+
]
|
|
11
|
+
tree = build_inheritance_map(classes)
|
|
12
|
+
|
|
13
|
+
output = render_text_tree(classes, tree)
|
|
14
|
+
|
|
15
|
+
assert "Animal [sample.py:1]" in output
|
|
16
|
+
assert "Dog [sample.py:4]" in output
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_render_mermaid():
|
|
20
|
+
classes = [ClassInfo("Animal"), ClassInfo("Dog", ("Animal",))]
|
|
21
|
+
tree = build_inheritance_map(classes)
|
|
22
|
+
|
|
23
|
+
output = render_mermaid(classes, tree)
|
|
24
|
+
|
|
25
|
+
assert "classDiagram" in output
|
|
26
|
+
assert "Animal <|-- Dog" in output
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from classmap_hanthink import scan_file, scan_path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_scan_file_extracts_classes(tmp_path: Path):
|
|
6
|
+
sample = tmp_path / "sample.py"
|
|
7
|
+
sample.write_text(
|
|
8
|
+
"class Animal:\n"
|
|
9
|
+
" pass\n\n"
|
|
10
|
+
"class Dog(Animal):\n"
|
|
11
|
+
" pass\n",
|
|
12
|
+
encoding="utf-8",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
classes = scan_file(sample)
|
|
16
|
+
|
|
17
|
+
assert [item.name for item in classes] == ["Animal", "Dog"]
|
|
18
|
+
assert classes[1].bases == ("Animal",)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_scan_path_recursively(tmp_path: Path):
|
|
22
|
+
package = tmp_path / "pkg"
|
|
23
|
+
package.mkdir()
|
|
24
|
+
(package / "a.py").write_text("class A:\n pass\n", encoding="utf-8")
|
|
25
|
+
(package / "b.py").write_text("class B(A):\n pass\n", encoding="utf-8")
|
|
26
|
+
|
|
27
|
+
classes = scan_path(package)
|
|
28
|
+
|
|
29
|
+
assert {item.name for item in classes} == {"A", "B"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from classmap_hanthink.model import ClassInfo
|
|
2
|
+
from classmap_hanthink.tree import build_inheritance_map, find_independent_classes, find_roots
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_build_inheritance_map():
|
|
6
|
+
classes = [
|
|
7
|
+
ClassInfo("Animal"),
|
|
8
|
+
ClassInfo("Dog", ("Animal",)),
|
|
9
|
+
ClassInfo("Puppy", ("Dog",)),
|
|
10
|
+
ClassInfo("Cat", ("Animal",)),
|
|
11
|
+
ClassInfo("Config"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
tree = build_inheritance_map(classes)
|
|
15
|
+
|
|
16
|
+
assert tree["Animal"] == ["Cat", "Dog"]
|
|
17
|
+
assert tree["Dog"] == ["Puppy"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_find_roots_and_independent_classes():
|
|
21
|
+
classes = [ClassInfo("Animal"), ClassInfo("Dog", ("Animal",)), ClassInfo("Config")]
|
|
22
|
+
tree = build_inheritance_map(classes)
|
|
23
|
+
|
|
24
|
+
assert find_roots(classes, tree) == ["Animal"]
|
|
25
|
+
assert find_independent_classes(classes, tree) == ["Config"]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.10"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "classmap-hanthink"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
source = { editable = "." }
|
|
9
|
+
|
|
10
|
+
[package.dev-dependencies]
|
|
11
|
+
dev = [
|
|
12
|
+
{ name = "pytest" },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[package.metadata]
|
|
16
|
+
|
|
17
|
+
[package.metadata.requires-dev]
|
|
18
|
+
dev = [{ name = "pytest", specifier = ">=8.0" }]
|
|
19
|
+
|
|
20
|
+
[[package]]
|
|
21
|
+
name = "colorama"
|
|
22
|
+
version = "0.4.6"
|
|
23
|
+
source = { registry = "https://pypi.org/simple" }
|
|
24
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
25
|
+
wheels = [
|
|
26
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[[package]]
|
|
30
|
+
name = "exceptiongroup"
|
|
31
|
+
version = "1.3.1"
|
|
32
|
+
source = { registry = "https://pypi.org/simple" }
|
|
33
|
+
dependencies = [
|
|
34
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
35
|
+
]
|
|
36
|
+
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
37
|
+
wheels = [
|
|
38
|
+
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[[package]]
|
|
42
|
+
name = "iniconfig"
|
|
43
|
+
version = "2.3.0"
|
|
44
|
+
source = { registry = "https://pypi.org/simple" }
|
|
45
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
46
|
+
wheels = [
|
|
47
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[[package]]
|
|
51
|
+
name = "packaging"
|
|
52
|
+
version = "26.2"
|
|
53
|
+
source = { registry = "https://pypi.org/simple" }
|
|
54
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
|
55
|
+
wheels = [
|
|
56
|
+
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[[package]]
|
|
60
|
+
name = "pluggy"
|
|
61
|
+
version = "1.6.0"
|
|
62
|
+
source = { registry = "https://pypi.org/simple" }
|
|
63
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
64
|
+
wheels = [
|
|
65
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[[package]]
|
|
69
|
+
name = "pygments"
|
|
70
|
+
version = "2.20.0"
|
|
71
|
+
source = { registry = "https://pypi.org/simple" }
|
|
72
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
73
|
+
wheels = [
|
|
74
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[[package]]
|
|
78
|
+
name = "pytest"
|
|
79
|
+
version = "9.0.3"
|
|
80
|
+
source = { registry = "https://pypi.org/simple" }
|
|
81
|
+
dependencies = [
|
|
82
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
83
|
+
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
84
|
+
{ name = "iniconfig" },
|
|
85
|
+
{ name = "packaging" },
|
|
86
|
+
{ name = "pluggy" },
|
|
87
|
+
{ name = "pygments" },
|
|
88
|
+
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
89
|
+
]
|
|
90
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
|
91
|
+
wheels = [
|
|
92
|
+
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[[package]]
|
|
96
|
+
name = "tomli"
|
|
97
|
+
version = "2.4.1"
|
|
98
|
+
source = { registry = "https://pypi.org/simple" }
|
|
99
|
+
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
|
100
|
+
wheels = [
|
|
101
|
+
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
|
102
|
+
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
|
103
|
+
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
|
104
|
+
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
|
105
|
+
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
|
106
|
+
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
|
107
|
+
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
|
108
|
+
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
|
109
|
+
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
|
110
|
+
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
|
111
|
+
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
|
112
|
+
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
|
113
|
+
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
|
114
|
+
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
|
115
|
+
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
|
116
|
+
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
|
117
|
+
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
|
118
|
+
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
|
119
|
+
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
|
120
|
+
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
|
121
|
+
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
|
122
|
+
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
|
123
|
+
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
|
124
|
+
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
|
125
|
+
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
|
126
|
+
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
|
127
|
+
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
|
128
|
+
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
|
129
|
+
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
|
130
|
+
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
|
131
|
+
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
|
132
|
+
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
|
133
|
+
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
|
134
|
+
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
|
135
|
+
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
|
136
|
+
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
|
137
|
+
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
|
138
|
+
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
|
139
|
+
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
|
140
|
+
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
|
141
|
+
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
|
142
|
+
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
|
143
|
+
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
|
144
|
+
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
|
145
|
+
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
|
146
|
+
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
[[package]]
|
|
150
|
+
name = "typing-extensions"
|
|
151
|
+
version = "4.15.0"
|
|
152
|
+
source = { registry = "https://pypi.org/simple" }
|
|
153
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
154
|
+
wheels = [
|
|
155
|
+
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
156
|
+
]
|