codebread 1.0.3__py3-none-any.whl → 1.0.4__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.
- codebread/__init__.py +34 -1
- codebread/server.py +47 -21
- {codebread-1.0.3.dist-info → codebread-1.0.4.dist-info}/METADATA +30 -5
- {codebread-1.0.3.dist-info → codebread-1.0.4.dist-info}/RECORD +8 -8
- {codebread-1.0.3.dist-info → codebread-1.0.4.dist-info}/WHEEL +0 -0
- {codebread-1.0.3.dist-info → codebread-1.0.4.dist-info}/entry_points.txt +0 -0
- {codebread-1.0.3.dist-info → codebread-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {codebread-1.0.3.dist-info → codebread-1.0.4.dist-info}/top_level.txt +0 -0
codebread/__init__.py
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
1
|
"""CodeBread — slice open a codebase and see its internal structure."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _read_version() -> str:
|
|
8
|
+
"""Single source of truth: pyproject.toml's [project] version.
|
|
9
|
+
|
|
10
|
+
Installed packages get it from the wheel's own metadata (always in
|
|
11
|
+
sync with what was actually published). Running from a source
|
|
12
|
+
checkout without installing falls back to reading pyproject.toml
|
|
13
|
+
directly — no parser dependency needed for one `version = "..."`
|
|
14
|
+
line, keeping the zero-dependency promise intact.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
18
|
+
try:
|
|
19
|
+
return version("codebread")
|
|
20
|
+
except PackageNotFoundError:
|
|
21
|
+
pass
|
|
22
|
+
except ImportError:
|
|
23
|
+
pass
|
|
24
|
+
try:
|
|
25
|
+
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
26
|
+
with open(os.path.join(root, "pyproject.toml"), encoding="utf-8") as f:
|
|
27
|
+
for line in f:
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if line.startswith("version"):
|
|
30
|
+
return line.split("=", 1)[1].strip().strip("\"'")
|
|
31
|
+
except OSError:
|
|
32
|
+
pass
|
|
33
|
+
return "0.0.0+unknown"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__version__ = _read_version()
|
codebread/server.py
CHANGED
|
@@ -7,7 +7,7 @@ import socket
|
|
|
7
7
|
import threading
|
|
8
8
|
import webbrowser
|
|
9
9
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
10
|
-
from typing import Dict
|
|
10
|
+
from typing import Dict, Optional
|
|
11
11
|
|
|
12
12
|
WEB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web")
|
|
13
13
|
WEB_ROOT = os.path.realpath(WEB_DIR)
|
|
@@ -21,7 +21,30 @@ MIME = {".html": "text/html; charset=utf-8",
|
|
|
21
21
|
".ico": "image/x-icon"}
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def
|
|
24
|
+
def safe_web_path(rel: str) -> Optional[str]:
|
|
25
|
+
"""Resolve a request path against WEB_DIR and return it only if the
|
|
26
|
+
*resolved* result is still inside WEB_DIR, else None.
|
|
27
|
+
|
|
28
|
+
String-prefix checks on `rel` alone aren't enough: on Windows,
|
|
29
|
+
os.path.join(WEB_DIR, "C:/some/file") silently discards WEB_DIR because
|
|
30
|
+
the second argument is drive-absolute, which would otherwise let a
|
|
31
|
+
request read any file on disk. Kept as a standalone function so the
|
|
32
|
+
traversal guard has a direct regression test, not just cli/browser use.
|
|
33
|
+
"""
|
|
34
|
+
full = os.path.realpath(os.path.join(WEB_DIR, rel))
|
|
35
|
+
try:
|
|
36
|
+
inside = os.path.commonpath([full, WEB_ROOT]) == WEB_ROOT
|
|
37
|
+
except ValueError:
|
|
38
|
+
inside = False # e.g. different drives on Windows
|
|
39
|
+
return full if inside else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _free_port(preferred: int) -> Optional[int]:
|
|
43
|
+
"""Find a bindable port near `preferred`, or None if none was free.
|
|
44
|
+
`preferred == 0` means "let the OS pick" and always succeeds — must
|
|
45
|
+
return that as a distinct value from "no port found", since 0 is a
|
|
46
|
+
falsy int and `if not port:` would otherwise treat a successful
|
|
47
|
+
OS-assigned bind as failure."""
|
|
25
48
|
for port in [preferred] + list(range(preferred + 1, preferred + 30)):
|
|
26
49
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
27
50
|
try:
|
|
@@ -29,12 +52,10 @@ def _free_port(preferred: int) -> int:
|
|
|
29
52
|
return port
|
|
30
53
|
except OSError:
|
|
31
54
|
continue
|
|
32
|
-
return
|
|
55
|
+
return None
|
|
33
56
|
|
|
34
57
|
|
|
35
|
-
def
|
|
36
|
-
data_bytes = json.dumps(graph, ensure_ascii=False).encode("utf-8")
|
|
37
|
-
|
|
58
|
+
def _make_handler(data_bytes: bytes):
|
|
38
59
|
class Handler(BaseHTTPRequestHandler):
|
|
39
60
|
def do_GET(self):
|
|
40
61
|
path = self.path.split("?")[0]
|
|
@@ -46,17 +67,8 @@ def serve(graph: Dict, port: int = 8137, open_browser: bool = True) -> None:
|
|
|
46
67
|
self._file(path.lstrip("/"))
|
|
47
68
|
|
|
48
69
|
def _file(self, rel: str):
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# Windows, os.path.join(WEB_DIR, "C:/some/file") silently
|
|
52
|
-
# discards WEB_DIR because the second argument is drive-absolute,
|
|
53
|
-
# which would otherwise let a request read any file on disk.
|
|
54
|
-
full = os.path.realpath(os.path.join(WEB_DIR, rel))
|
|
55
|
-
try:
|
|
56
|
-
inside = os.path.commonpath([full, WEB_ROOT]) == WEB_ROOT
|
|
57
|
-
except ValueError:
|
|
58
|
-
inside = False # e.g. different drives on Windows
|
|
59
|
-
if not inside:
|
|
70
|
+
full = safe_web_path(rel)
|
|
71
|
+
if full is None:
|
|
60
72
|
self.send_error(403)
|
|
61
73
|
return
|
|
62
74
|
if not os.path.isfile(full):
|
|
@@ -78,12 +90,26 @@ def serve(graph: Dict, port: int = 8137, open_browser: bool = True) -> None:
|
|
|
78
90
|
def log_message(self, fmt, *args): # keep the console clean
|
|
79
91
|
pass
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
return Handler
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def build_server(graph: Dict, port: int = 8137) -> Optional[ThreadingHTTPServer]:
|
|
97
|
+
"""Bind a ready-to-serve ThreadingHTTPServer, or None if no port was
|
|
98
|
+
free. Split out from `serve()` so tests (and other embedders) can start
|
|
99
|
+
and cleanly `.shutdown()` a server instead of blocking forever."""
|
|
100
|
+
data_bytes = json.dumps(graph, ensure_ascii=False).encode("utf-8")
|
|
101
|
+
bound_port = _free_port(port)
|
|
102
|
+
if bound_port is None:
|
|
103
|
+
return None
|
|
104
|
+
return ThreadingHTTPServer(("127.0.0.1", bound_port), _make_handler(data_bytes))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def serve(graph: Dict, port: int = 8137, open_browser: bool = True) -> None:
|
|
108
|
+
server = build_server(graph, port)
|
|
109
|
+
if server is None:
|
|
83
110
|
print("[codebread] No free port found near 8137 — aborting serve.")
|
|
84
111
|
return
|
|
85
|
-
|
|
86
|
-
url = f"http://127.0.0.1:{port}/"
|
|
112
|
+
url = f"http://127.0.0.1:{server.server_port}/"
|
|
87
113
|
print(f"[codebread] Serving at {url} (Ctrl+C to stop)")
|
|
88
114
|
if open_browser:
|
|
89
115
|
threading.Timer(0.4, lambda: webbrowser.open(url)).start()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebread
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Interactive codebase analyzer & visualizer — slice open a project and see how it's wired together.
|
|
5
5
|
Author: Danial Irsyad
|
|
6
6
|
License: MIT
|
|
@@ -10,6 +10,8 @@ Description-Content-Type: text/markdown
|
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Provides-Extra: full
|
|
12
12
|
Requires-Dist: pathspec>=0.11; extra == "full"
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
13
15
|
Dynamic: license-file
|
|
14
16
|
|
|
15
17
|
<p align="center">
|
|
@@ -22,7 +24,8 @@ Dynamic: license-file
|
|
|
22
24
|
</p>
|
|
23
25
|
|
|
24
26
|
<p align="center">
|
|
25
|
-
<img alt="
|
|
27
|
+
<a href="https://pypi.org/project/codebread/"><img alt="PyPI" src="https://img.shields.io/pypi/v/codebread?color=2dd4bf&label=version"></a>
|
|
28
|
+
<a href="https://github.com/honow48-tech/CodeBread/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/honow48-tech/CodeBread/actions/workflows/ci.yml/badge.svg"></a>
|
|
26
29
|
<img alt="python" src="https://img.shields.io/badge/python-3.9%2B-0284c7">
|
|
27
30
|
<img alt="deps" src="https://img.shields.io/badge/required%20dependencies-none-2dd4bf">
|
|
28
31
|
<img alt="license" src="https://img.shields.io/badge/license-MIT-8b5cf6">
|
|
@@ -238,12 +241,30 @@ codebread/
|
|
|
238
241
|
server.py ← stdlib local web server
|
|
239
242
|
export.py ← JSON + single-file HTML export
|
|
240
243
|
web/ ← the UI (vanilla JS + SVG, zero dependencies)
|
|
244
|
+
tests/ ← pytest suite + per-language fixture corpus
|
|
241
245
|
assets/ ← logo (.svg source + .png for the README —
|
|
242
246
|
GitHub blocks same-repo SVG <img> embeds)
|
|
243
247
|
```
|
|
244
248
|
|
|
245
249
|
---
|
|
246
250
|
|
|
251
|
+
## Testing
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pip install -e ".[full,test]"
|
|
255
|
+
pytest -q
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The suite covers every parser (a small fixture file per language under
|
|
259
|
+
`tests/fixtures/`), the connection/graph builder (call resolution,
|
|
260
|
+
API↔route matching, DB edges, orphan + cycle detection), the scanner
|
|
261
|
+
(`.gitignore` handling, language detection), the local server (including a
|
|
262
|
+
regression test for the path-traversal fix), and an end-to-end analyze
|
|
263
|
+
pass — including a regression test that secrets in scanned `.env` files
|
|
264
|
+
never leak into an exported graph. CI (`.github/workflows/ci.yml`) runs it
|
|
265
|
+
on every push/PR across the minimum and latest supported Python versions,
|
|
266
|
+
and the PyPI release workflow won't publish if it fails.
|
|
267
|
+
|
|
247
268
|
## Roadmap
|
|
248
269
|
|
|
249
270
|
Already done as of v1.0:
|
|
@@ -251,13 +272,13 @@ Already done as of v1.0:
|
|
|
251
272
|
- [x] Orphaned-function and circular-dependency detection
|
|
252
273
|
- [x] Diff mode between two scans
|
|
253
274
|
- [x] Focus mode, orbit layout, right-click actions, minimap, breadcrumb
|
|
275
|
+
- [x] Automated test suite + CI
|
|
254
276
|
|
|
255
277
|
Not yet, but planned:
|
|
256
278
|
|
|
257
279
|
- [ ] "Explain this function" — a plain-language AI summary button
|
|
258
280
|
- [ ] More precise multi-language parsing (currently structural regex for
|
|
259
281
|
everything but Python)
|
|
260
|
-
- [ ] An automated test suite
|
|
261
282
|
|
|
262
283
|
Have an idea? Open an issue.
|
|
263
284
|
|
|
@@ -271,8 +292,12 @@ but contributions are genuinely welcome:
|
|
|
271
292
|
or UI polish. Keep the zero-dependency philosophy: the core tool should
|
|
272
293
|
keep running on nothing but the Python standard library, and the UI
|
|
273
294
|
should keep running on vanilla JS with no build step.
|
|
274
|
-
-
|
|
275
|
-
|
|
295
|
+
- Run `pytest -q` before opening a PR (see [Testing](#testing) above); add
|
|
296
|
+
a fixture + test case under `tests/` for new parser behavior where you
|
|
297
|
+
can.
|
|
298
|
+
|
|
299
|
+
Security issue instead? See [SECURITY.md](SECURITY.md) rather than a
|
|
300
|
+
public issue.
|
|
276
301
|
|
|
277
302
|
## License
|
|
278
303
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
codebread/__init__.py,sha256=
|
|
1
|
+
codebread/__init__.py,sha256=mwaD5s8k8q32ofSXrl6_tMjBJk0U1U2lrxwP3PPNtCs,1215
|
|
2
2
|
codebread/analyzer.py,sha256=tCsV0Cwi1l-If5zbNLbMtsuKW3o-rl8mekNG-B7MXpY,3234
|
|
3
3
|
codebread/classifier.py,sha256=jehD-XQ80CP_O4x9YRi4JQqzd4AaHxKpzI83wLG1yvM,4067
|
|
4
4
|
codebread/cli.py,sha256=AMvwk29cOJDjsI-I07pCGbqogdtq1Y6HWnT52FHYyLs,4072
|
|
@@ -8,7 +8,7 @@ codebread/export.py,sha256=rKYmzuSEXYDrgcItnhdyXI_lo0LN2PSoMe0hYElzU88,1512
|
|
|
8
8
|
codebread/languages.py,sha256=SKHy9v7QPreWZG6FbuX3B0-kq1jqVn-rmwNqJ9ESFqo,4494
|
|
9
9
|
codebread/models.py,sha256=Sw2PJW63dKyl5RpWwfY2ZvCJLYttmjoQg4O5H5DtLgM,4214
|
|
10
10
|
codebread/scanner.py,sha256=bEXSaoENmJ-C_oabj2IF1aWs9-LaIqMc80xXQvo7-eg,8020
|
|
11
|
-
codebread/server.py,sha256=
|
|
11
|
+
codebread/server.py,sha256=Uh7L2n0pumpt1UNSlC1mvcjg6yBl1LAxTmbz9cmsilA,4597
|
|
12
12
|
codebread/parsers/__init__.py,sha256=ovsvmXGClshiUlDdx6mkBmRu8__OZIqYos6-1eLb9eM,2285
|
|
13
13
|
codebread/parsers/generic_parser.py,sha256=lz-wLsmQYERoDRM27lhEP3cvNYbomHfA--VI_3LmJ5E,11966
|
|
14
14
|
codebread/parsers/javascript_parser.py,sha256=lVIlydQ3--Owd78816XIM77NntfQ97DeLzYvPrjd5GE,15916
|
|
@@ -16,9 +16,9 @@ codebread/parsers/python_parser.py,sha256=QnhCLx01bKYLYqav33cI27qbEQNPUiXnS5uN7c
|
|
|
16
16
|
codebread/web/app.js,sha256=bsm7cpji0mJrnpr0TPNhsL6w2bQw4RMwA8lS_T57G1s,66567
|
|
17
17
|
codebread/web/index.html,sha256=6YqjC_wO3pfqyG_11uIhvRM16Wagu2l8eSOs1FNc-UY,8693
|
|
18
18
|
codebread/web/style.css,sha256=fiq1bC-I1bC6-6MxRn0blQwGOjO_fdClPrhf5W7zqPw,23729
|
|
19
|
-
codebread-1.0.
|
|
20
|
-
codebread-1.0.
|
|
21
|
-
codebread-1.0.
|
|
22
|
-
codebread-1.0.
|
|
23
|
-
codebread-1.0.
|
|
24
|
-
codebread-1.0.
|
|
19
|
+
codebread-1.0.4.dist-info/licenses/LICENSE,sha256=okINupIvMgiuOlvMVpsM9DT5jb6QQmKBd--yfZq4bjc,1070
|
|
20
|
+
codebread-1.0.4.dist-info/METADATA,sha256=KY847kSXgZNds5HE-a9UJdGXazfY1nhrWW661_1lIjg,14262
|
|
21
|
+
codebread-1.0.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
22
|
+
codebread-1.0.4.dist-info/entry_points.txt,sha256=ItG61aUP1YTvA_zA-jlUshBc5BPpJDpCyVnsyaxNXKA,49
|
|
23
|
+
codebread-1.0.4.dist-info/top_level.txt,sha256=KctFB7UQb4tN9YotaJD95GuHWIQuoO5VqyLdo_gEswQ,10
|
|
24
|
+
codebread-1.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|