chartedcode 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.
- chartedcode-0.1.0/LICENSE +21 -0
- chartedcode-0.1.0/NOTICE +18 -0
- chartedcode-0.1.0/PKG-INFO +153 -0
- chartedcode-0.1.0/README.md +123 -0
- chartedcode-0.1.0/pyproject.toml +128 -0
- chartedcode-0.1.0/rust/Cargo.lock +1101 -0
- chartedcode-0.1.0/rust/Cargo.toml +50 -0
- chartedcode-0.1.0/rust/assets/calls.html +403 -0
- chartedcode-0.1.0/rust/assets/classes.html +110 -0
- chartedcode-0.1.0/rust/assets/elk.bundled.js +6663 -0
- chartedcode-0.1.0/rust/assets/elk.bundled.js.sha256 +1 -0
- chartedcode-0.1.0/rust/assets/elkjs-LICENSE.md +264 -0
- chartedcode-0.1.0/rust/assets/flow.html +181 -0
- chartedcode-0.1.0/rust/python/Cargo.toml +24 -0
- chartedcode-0.1.0/rust/python/build.rs +4 -0
- chartedcode-0.1.0/rust/python/src/lib.rs +27 -0
- chartedcode-0.1.0/rust/python-data/scripts/chartedcode-py +4 -0
- chartedcode-0.1.0/rust/src/analysis.rs +202 -0
- chartedcode-0.1.0/rust/src/bundle.rs +129 -0
- chartedcode-0.1.0/rust/src/calls.rs +552 -0
- chartedcode-0.1.0/rust/src/cfg.rs +555 -0
- chartedcode-0.1.0/rust/src/classes.rs +188 -0
- chartedcode-0.1.0/rust/src/cli.rs +243 -0
- chartedcode-0.1.0/rust/src/discover.rs +62 -0
- chartedcode-0.1.0/rust/src/lib.rs +22 -0
- chartedcode-0.1.0/rust/src/main.rs +10 -0
- chartedcode-0.1.0/rust/src/model.rs +292 -0
- chartedcode-0.1.0/rust/src/modules.rs +48 -0
- chartedcode-0.1.0/rust/src/parse.rs +451 -0
- chartedcode-0.1.0/rust/src/render.rs +170 -0
- chartedcode-0.1.0/rust/src/resolve.rs +310 -0
- chartedcode-0.1.0/rust/tests/bundle.rs +272 -0
- chartedcode-0.1.0/rust/tests/calls.rs +422 -0
- chartedcode-0.1.0/rust/tests/cli.rs +194 -0
- chartedcode-0.1.0/rust/tests/discover.rs +120 -0
- chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/__init__.py +1 -0
- chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/_internal.py +5 -0
- chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/app.py +61 -0
- chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/priv.py +27 -0
- chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/rec.py +16 -0
- chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/util.py +29 -0
- chartedcode-0.1.0/rust/tests/fixtures/flow/flow.py +131 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/__init__.py +1 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/base.py +2 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/core.py +2 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/models.py +9 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/other.py +2 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/services.py +35 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/sub/__init__.py +0 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/sub/deep.py +15 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/sub/helper.py +2 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/app/sub/stars.py +5 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/__init__.py +1 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/base.py +2 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/core.py +2 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/models.py +9 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/other.py +2 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/services.py +35 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/tests_dir/test_fakes.py +17 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/uniq_pkg/__init__.py +0 -0
- chartedcode-0.1.0/rust/tests/fixtures/imports/uniq_pkg/ports.py +5 -0
- chartedcode-0.1.0/rust/tests/flow.rs +421 -0
- chartedcode-0.1.0/rust/tests/imports.rs +224 -0
- chartedcode-0.1.0/rust/tests/render.rs +204 -0
- chartedcode-0.1.0/src/chartedcode/__init__.py +1 -0
- chartedcode-0.1.0/src/chartedcode/__main__.py +5 -0
- chartedcode-0.1.0/src/chartedcode/_binary.py +134 -0
- chartedcode-0.1.0/src/chartedcode/_rust.pyi +2 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Martin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
chartedcode-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
chartedcode is licensed under the MIT License (see LICENSE). The diagrams it writes embed
|
|
2
|
+
the following third-party code, under its own terms:
|
|
3
|
+
|
|
4
|
+
## elkjs
|
|
5
|
+
|
|
6
|
+
rust/assets/elk.bundled.js is elkjs 0.11.0, the Eclipse Layout Kernel compiled to JavaScript
|
|
7
|
+
(https://github.com/kieler/elkjs). It is included unmodified, and every HTML diagram that
|
|
8
|
+
chartedcode generates contains a copy of it so that the diagram works offline.
|
|
9
|
+
|
|
10
|
+
elkjs is licensed under the Eclipse Public License 2.0 (EPL-2.0); the full text is in
|
|
11
|
+
rust/assets/elkjs-LICENSE.md. The corresponding source is available from the repository
|
|
12
|
+
above. The SHA-256 of the bundled file is recorded in rust/assets/elk.bundled.js.sha256 and
|
|
13
|
+
checked by the test suite.
|
|
14
|
+
|
|
15
|
+
## Rust crates
|
|
16
|
+
|
|
17
|
+
The executable is built from Rust crates whose licences are listed by `cargo metadata`;
|
|
18
|
+
among them, the Python parser comes from Ruff (https://github.com/astral-sh/ruff, MIT).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chartedcode
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
14
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
License-File: NOTICE
|
|
18
|
+
License-File: rust/assets/elkjs-LICENSE.md
|
|
19
|
+
Summary: Diagrams for Python projects: class, call and control-flow diagrams, drawn as self-contained HTML by a small deterministic Rust binary.
|
|
20
|
+
Keywords: uml,diagram,call-graph,control-flow,class-diagram,static-analysis,documentation
|
|
21
|
+
Home-Page: https://github.com/w-martin/chartedcode
|
|
22
|
+
Author: William Martin
|
|
23
|
+
License-Expression: MIT
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
26
|
+
Project-URL: Homepage, https://github.com/w-martin/chartedcode
|
|
27
|
+
Project-URL: Issue Tracker, https://github.com/w-martin/chartedcode/issues
|
|
28
|
+
Project-URL: Repository, https://github.com/w-martin/chartedcode
|
|
29
|
+
|
|
30
|
+
# chartedcode
|
|
31
|
+
|
|
32
|
+
Diagrams for Python projects: **class**, **call** and **control-flow** diagrams, drawn as self-contained HTML by a small, deterministic Rust program.
|
|
33
|
+
|
|
34
|
+
Point it at a file or a directory. It reads the source (it never runs it), works out how the code fits together, and writes one HTML file per diagram. Each file holds its data, the layout engine and the viewer, so it opens in any browser, works offline, and needs no Graphviz or other renderer installed.
|
|
35
|
+
|
|
36
|
+
```console
|
|
37
|
+
$ chartedcode classes src/ # class diagram
|
|
38
|
+
$ chartedcode calls src/ # call graph
|
|
39
|
+
$ chartedcode flow src/ # control-flow graph of each function
|
|
40
|
+
wrote src-flow.chartedcode.html
|
|
41
|
+
$ chartedcode all src/ -o out/ # all of them, plus the analysis as JSON and a manifest
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```console
|
|
47
|
+
$ pip install chartedcode # or: uvx chartedcode calls src/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The wheel carries the compiled program; there is nothing else to install. Python 3.11 or later.
|
|
51
|
+
|
|
52
|
+
## The diagrams
|
|
53
|
+
|
|
54
|
+
Every relationship says how it was found, and anything the tool could not follow is counted and listed, not guessed at or silently dropped.
|
|
55
|
+
|
|
56
|
+
### `classes`
|
|
57
|
+
|
|
58
|
+
Inheritance, Protocol conformance and field types.
|
|
59
|
+
|
|
60
|
+
- **inherits**: a base class, resolved through imports (including relative imports, re-exports through `__init__.py`, star imports and `TYPE_CHECKING` imports).
|
|
61
|
+
- **implements**: a class has every method name of a `Protocol`. This compares names only, not signatures, so it is shown as weak evidence.
|
|
62
|
+
- **has-a**: a field whose annotation names a class of the project (class-body annotations, and `self.x` assigned from an annotated parameter in `__init__`).
|
|
63
|
+
|
|
64
|
+
### `calls`
|
|
65
|
+
|
|
66
|
+
Which function calls which, and in what order.
|
|
67
|
+
|
|
68
|
+
- **calls** and **constructs** (`Foo()` goes to `Foo.__init__`, or to an implicit constructor when the class defines none).
|
|
69
|
+
- **passes**: a function or method handed to another function as a callback (`runner.run("train", self._trainer.fit)`), which is assumed to be called.
|
|
70
|
+
- **dispatches**: a call to a Protocol method may run any implementer's method of that name.
|
|
71
|
+
- **Call order**: each call is numbered within its caller, in evaluation order (a call's arguments before the call). Numbers appear on the arrows, in a panel listing the focused function's calls, and in an **execution flow** view that chains the calls in order and enters each callee's own calls. Recursion is marked, not drawn, so the execution flow is always acyclic.
|
|
72
|
+
- **Acyclic view**: functions that call each other in a cycle are collapsed into one node. When there are no cycles the page says it is identical to the call graph.
|
|
73
|
+
- Views at function, class or module granularity; focus on one node with its callers and callees to a chosen depth.
|
|
74
|
+
|
|
75
|
+
The colour of an edge says how sure the tool is: resolved by name, import, `self` or `super`; inferred from a declared type; method names only; external.
|
|
76
|
+
|
|
77
|
+
### `flow`
|
|
78
|
+
|
|
79
|
+
The control-flow graph of each function (and of a module's top-level code): basic blocks with their source lines, edges labelled with when they are taken (`true`, `false`, `next item`, `loop`, `break`, `exception`, `return`, ...), unreachable code marked, and a complexity number (decision points plus one). The calls in each block carry the same step numbers as the call graph, and a call that has a flow of its own can be opened with two clicks.
|
|
80
|
+
|
|
81
|
+
Approximations, so the diagram can be read correctly: any statement in a `try` body may raise (one `exception` edge from the body to each handler), context managers do not branch, and boolean operators, conditional expressions and comprehensions do not split a block.
|
|
82
|
+
|
|
83
|
+
## Every diagram at once, for CI
|
|
84
|
+
|
|
85
|
+
```console
|
|
86
|
+
$ chartedcode all src/ -o out/
|
|
87
|
+
wrote 4 files and manifest.json to out/
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`out/` holds `classes.html`, `calls.html`, `flow.html`, `analysis.json` and `manifest.json`. The names are fixed, so whatever collects them does not need to know the project. The manifest lists each file with its size and SHA-256, the tool and schema versions, and some totals (files analysed, classes, functions, call edges, untraced calls, the highest complexity); it has no timestamps, so the same input gives a byte-identical directory. `analysis.json` is the complete analysis, including control flow, whatever `--min-complexity` does to the flow page.
|
|
91
|
+
|
|
92
|
+
## Embedding the pages
|
|
93
|
+
|
|
94
|
+
Each page is one file with no external requests, no cookies and no browser storage, and it renders inside `<iframe sandbox="allow-scripts">`. A host chooses how a page opens through the URL fragment (`page.html#embed=1&theme=dark`). A page reads `embed` and `theme` again if the fragment changes after load; the other options are read once, when the page opens. There is no other way to drive a page from outside.
|
|
95
|
+
|
|
96
|
+
| Option | Pages | Effect |
|
|
97
|
+
| --- | --- | --- |
|
|
98
|
+
| `embed=1` | all | hide the header and legend, so the diagram fills the frame (the status line and, in the call graph, the order-of-calls panel stay) |
|
|
99
|
+
| `theme=light` or `theme=dark` | all | use these colours whatever the system setting is |
|
|
100
|
+
| `kinds=inherits,implements,composes` | classes | which relationships are shown at first |
|
|
101
|
+
| `view=graph\|exec\|acyclic`, `gran=function\|class\|module` | calls | the view and its granularity |
|
|
102
|
+
| `focus=<function id>`, `depth=N` | calls | the function to focus on and how far to follow its calls |
|
|
103
|
+
| `ext=1`, `untraced=1`, `private=1`, `mincalls=N`, `dir=down` | calls | external calls, untraced-call counts, private names, the focus-list filter, the layout direction |
|
|
104
|
+
| `function=<function id>` | flow | the function to show |
|
|
105
|
+
| `minc=N`, `private=1`, `dir=right` | flow | the complexity filter for the function list, private names, the layout direction |
|
|
106
|
+
|
|
107
|
+
Function ids are the `id` fields of `functions` in `analysis.json`, such as `pkg/app::Service.run`.
|
|
108
|
+
|
|
109
|
+
## What it does not follow
|
|
110
|
+
|
|
111
|
+
Static analysis of a dynamic language has limits. The tool does not infer return types, so a call on a variable whose type is not declared (`model.evaluate()` where `model` is whatever a function returned) is **untraced**: counted per function, listed in the page, never drawn as an edge. It also does not follow dispatch through subclass overrides (only Protocols dispatch), property access that runs code, decorators, `getattr` and other dynamic calls, function-local imports or `__all__`. Branches and loops are flattened in the call numbering: a number means "the order written", not a guaranteed run order.
|
|
112
|
+
|
|
113
|
+
## Options
|
|
114
|
+
|
|
115
|
+
| Option | Effect |
|
|
116
|
+
| --- | --- |
|
|
117
|
+
| `-o, --output FILE` | where to write the result; by default `<name>-<view>.chartedcode.html` in the current directory, or standard output for JSON |
|
|
118
|
+
| `--format html\|json` | the page, or the analysis as JSON (`schema_version` 1; always includes private names) |
|
|
119
|
+
| `--include-private` | start with private names (a single leading underscore) shown; the page hides them by default |
|
|
120
|
+
| `--no-gitignore` | also analyse files matched by `.gitignore` (by default they and hidden directories are skipped; the global ignore file is never read, so a checkout gives the same result everywhere) |
|
|
121
|
+
| `--no-source` | `flow` only: leave source text out of the diagram, so blocks show only line numbers |
|
|
122
|
+
| `--min-complexity N` | `flow` only: only draw functions of at least this complexity |
|
|
123
|
+
|
|
124
|
+
The same input always gives byte-identical output.
|
|
125
|
+
|
|
126
|
+
**A `flow` page contains the source text of your functions.** Use `--no-source` before sharing one if that matters.
|
|
127
|
+
|
|
128
|
+
## From Python
|
|
129
|
+
|
|
130
|
+
```console
|
|
131
|
+
$ python -m chartedcode calls src/
|
|
132
|
+
$ chartedcode-py calls src/ # always inside this process
|
|
133
|
+
$ CHARTEDCODE_INPROC=1 python -m chartedcode calls src/
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`python -m chartedcode` starts the executable that came with the package. For environments where the interpreter may run code but no other program may be started, `chartedcode-py` (or `CHARTEDCODE_INPROC=1`) runs the same program inside the interpreter, through an extension module; the output is identical. `chartedcode._binary.run_binary(["calls", "src/", "--format", "json"], capture=True)` runs it from code.
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
```console
|
|
141
|
+
$ uv sync
|
|
142
|
+
$ uv run inv build # build the executable and the extension, and install them
|
|
143
|
+
$ uv run inv all # format, lint, test (Rust and Python), verify licences
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `uv run inv test` builds first. Run bare `pytest` as `uv run --no-sync pytest` after `uv run inv build`: a plain `uv run` re-syncs the project and can put an older cached build over the one just installed. The test session refuses to run against an executable older than the Rust sources.
|
|
147
|
+
- The browser tests drive a headless Chrome and are skipped when none is found (set `CHROME` to its path).
|
|
148
|
+
- Build wheels from the working tree after staging the extension (`uv run inv build` does that), with `maturin build --profile cli`. `uv build` builds the wheel from the source distribution, which does not contain the compiled extension.
|
|
149
|
+
|
|
150
|
+
## Licence
|
|
151
|
+
|
|
152
|
+
MIT (see `LICENSE`). Every page embeds [elkjs](https://github.com/kieler/elkjs) 0.11.0, unmodified, which is licensed under the Eclipse Public License 2.0; see `NOTICE` and `rust/assets/elkjs-LICENSE.md`.
|
|
153
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# chartedcode
|
|
2
|
+
|
|
3
|
+
Diagrams for Python projects: **class**, **call** and **control-flow** diagrams, drawn as self-contained HTML by a small, deterministic Rust program.
|
|
4
|
+
|
|
5
|
+
Point it at a file or a directory. It reads the source (it never runs it), works out how the code fits together, and writes one HTML file per diagram. Each file holds its data, the layout engine and the viewer, so it opens in any browser, works offline, and needs no Graphviz or other renderer installed.
|
|
6
|
+
|
|
7
|
+
```console
|
|
8
|
+
$ chartedcode classes src/ # class diagram
|
|
9
|
+
$ chartedcode calls src/ # call graph
|
|
10
|
+
$ chartedcode flow src/ # control-flow graph of each function
|
|
11
|
+
wrote src-flow.chartedcode.html
|
|
12
|
+
$ chartedcode all src/ -o out/ # all of them, plus the analysis as JSON and a manifest
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```console
|
|
18
|
+
$ pip install chartedcode # or: uvx chartedcode calls src/
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The wheel carries the compiled program; there is nothing else to install. Python 3.11 or later.
|
|
22
|
+
|
|
23
|
+
## The diagrams
|
|
24
|
+
|
|
25
|
+
Every relationship says how it was found, and anything the tool could not follow is counted and listed, not guessed at or silently dropped.
|
|
26
|
+
|
|
27
|
+
### `classes`
|
|
28
|
+
|
|
29
|
+
Inheritance, Protocol conformance and field types.
|
|
30
|
+
|
|
31
|
+
- **inherits**: a base class, resolved through imports (including relative imports, re-exports through `__init__.py`, star imports and `TYPE_CHECKING` imports).
|
|
32
|
+
- **implements**: a class has every method name of a `Protocol`. This compares names only, not signatures, so it is shown as weak evidence.
|
|
33
|
+
- **has-a**: a field whose annotation names a class of the project (class-body annotations, and `self.x` assigned from an annotated parameter in `__init__`).
|
|
34
|
+
|
|
35
|
+
### `calls`
|
|
36
|
+
|
|
37
|
+
Which function calls which, and in what order.
|
|
38
|
+
|
|
39
|
+
- **calls** and **constructs** (`Foo()` goes to `Foo.__init__`, or to an implicit constructor when the class defines none).
|
|
40
|
+
- **passes**: a function or method handed to another function as a callback (`runner.run("train", self._trainer.fit)`), which is assumed to be called.
|
|
41
|
+
- **dispatches**: a call to a Protocol method may run any implementer's method of that name.
|
|
42
|
+
- **Call order**: each call is numbered within its caller, in evaluation order (a call's arguments before the call). Numbers appear on the arrows, in a panel listing the focused function's calls, and in an **execution flow** view that chains the calls in order and enters each callee's own calls. Recursion is marked, not drawn, so the execution flow is always acyclic.
|
|
43
|
+
- **Acyclic view**: functions that call each other in a cycle are collapsed into one node. When there are no cycles the page says it is identical to the call graph.
|
|
44
|
+
- Views at function, class or module granularity; focus on one node with its callers and callees to a chosen depth.
|
|
45
|
+
|
|
46
|
+
The colour of an edge says how sure the tool is: resolved by name, import, `self` or `super`; inferred from a declared type; method names only; external.
|
|
47
|
+
|
|
48
|
+
### `flow`
|
|
49
|
+
|
|
50
|
+
The control-flow graph of each function (and of a module's top-level code): basic blocks with their source lines, edges labelled with when they are taken (`true`, `false`, `next item`, `loop`, `break`, `exception`, `return`, ...), unreachable code marked, and a complexity number (decision points plus one). The calls in each block carry the same step numbers as the call graph, and a call that has a flow of its own can be opened with two clicks.
|
|
51
|
+
|
|
52
|
+
Approximations, so the diagram can be read correctly: any statement in a `try` body may raise (one `exception` edge from the body to each handler), context managers do not branch, and boolean operators, conditional expressions and comprehensions do not split a block.
|
|
53
|
+
|
|
54
|
+
## Every diagram at once, for CI
|
|
55
|
+
|
|
56
|
+
```console
|
|
57
|
+
$ chartedcode all src/ -o out/
|
|
58
|
+
wrote 4 files and manifest.json to out/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`out/` holds `classes.html`, `calls.html`, `flow.html`, `analysis.json` and `manifest.json`. The names are fixed, so whatever collects them does not need to know the project. The manifest lists each file with its size and SHA-256, the tool and schema versions, and some totals (files analysed, classes, functions, call edges, untraced calls, the highest complexity); it has no timestamps, so the same input gives a byte-identical directory. `analysis.json` is the complete analysis, including control flow, whatever `--min-complexity` does to the flow page.
|
|
62
|
+
|
|
63
|
+
## Embedding the pages
|
|
64
|
+
|
|
65
|
+
Each page is one file with no external requests, no cookies and no browser storage, and it renders inside `<iframe sandbox="allow-scripts">`. A host chooses how a page opens through the URL fragment (`page.html#embed=1&theme=dark`). A page reads `embed` and `theme` again if the fragment changes after load; the other options are read once, when the page opens. There is no other way to drive a page from outside.
|
|
66
|
+
|
|
67
|
+
| Option | Pages | Effect |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `embed=1` | all | hide the header and legend, so the diagram fills the frame (the status line and, in the call graph, the order-of-calls panel stay) |
|
|
70
|
+
| `theme=light` or `theme=dark` | all | use these colours whatever the system setting is |
|
|
71
|
+
| `kinds=inherits,implements,composes` | classes | which relationships are shown at first |
|
|
72
|
+
| `view=graph\|exec\|acyclic`, `gran=function\|class\|module` | calls | the view and its granularity |
|
|
73
|
+
| `focus=<function id>`, `depth=N` | calls | the function to focus on and how far to follow its calls |
|
|
74
|
+
| `ext=1`, `untraced=1`, `private=1`, `mincalls=N`, `dir=down` | calls | external calls, untraced-call counts, private names, the focus-list filter, the layout direction |
|
|
75
|
+
| `function=<function id>` | flow | the function to show |
|
|
76
|
+
| `minc=N`, `private=1`, `dir=right` | flow | the complexity filter for the function list, private names, the layout direction |
|
|
77
|
+
|
|
78
|
+
Function ids are the `id` fields of `functions` in `analysis.json`, such as `pkg/app::Service.run`.
|
|
79
|
+
|
|
80
|
+
## What it does not follow
|
|
81
|
+
|
|
82
|
+
Static analysis of a dynamic language has limits. The tool does not infer return types, so a call on a variable whose type is not declared (`model.evaluate()` where `model` is whatever a function returned) is **untraced**: counted per function, listed in the page, never drawn as an edge. It also does not follow dispatch through subclass overrides (only Protocols dispatch), property access that runs code, decorators, `getattr` and other dynamic calls, function-local imports or `__all__`. Branches and loops are flattened in the call numbering: a number means "the order written", not a guaranteed run order.
|
|
83
|
+
|
|
84
|
+
## Options
|
|
85
|
+
|
|
86
|
+
| Option | Effect |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| `-o, --output FILE` | where to write the result; by default `<name>-<view>.chartedcode.html` in the current directory, or standard output for JSON |
|
|
89
|
+
| `--format html\|json` | the page, or the analysis as JSON (`schema_version` 1; always includes private names) |
|
|
90
|
+
| `--include-private` | start with private names (a single leading underscore) shown; the page hides them by default |
|
|
91
|
+
| `--no-gitignore` | also analyse files matched by `.gitignore` (by default they and hidden directories are skipped; the global ignore file is never read, so a checkout gives the same result everywhere) |
|
|
92
|
+
| `--no-source` | `flow` only: leave source text out of the diagram, so blocks show only line numbers |
|
|
93
|
+
| `--min-complexity N` | `flow` only: only draw functions of at least this complexity |
|
|
94
|
+
|
|
95
|
+
The same input always gives byte-identical output.
|
|
96
|
+
|
|
97
|
+
**A `flow` page contains the source text of your functions.** Use `--no-source` before sharing one if that matters.
|
|
98
|
+
|
|
99
|
+
## From Python
|
|
100
|
+
|
|
101
|
+
```console
|
|
102
|
+
$ python -m chartedcode calls src/
|
|
103
|
+
$ chartedcode-py calls src/ # always inside this process
|
|
104
|
+
$ CHARTEDCODE_INPROC=1 python -m chartedcode calls src/
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`python -m chartedcode` starts the executable that came with the package. For environments where the interpreter may run code but no other program may be started, `chartedcode-py` (or `CHARTEDCODE_INPROC=1`) runs the same program inside the interpreter, through an extension module; the output is identical. `chartedcode._binary.run_binary(["calls", "src/", "--format", "json"], capture=True)` runs it from code.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```console
|
|
112
|
+
$ uv sync
|
|
113
|
+
$ uv run inv build # build the executable and the extension, and install them
|
|
114
|
+
$ uv run inv all # format, lint, test (Rust and Python), verify licences
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `uv run inv test` builds first. Run bare `pytest` as `uv run --no-sync pytest` after `uv run inv build`: a plain `uv run` re-syncs the project and can put an older cached build over the one just installed. The test session refuses to run against an executable older than the Rust sources.
|
|
118
|
+
- The browser tests drive a headless Chrome and are skipped when none is found (set `CHROME` to its path).
|
|
119
|
+
- Build wheels from the working tree after staging the extension (`uv run inv build` does that), with `maturin build --profile cli`. `uv build` builds the wheel from the source distribution, which does not contain the compiled extension.
|
|
120
|
+
|
|
121
|
+
## Licence
|
|
122
|
+
|
|
123
|
+
MIT (see `LICENSE`). Every page embeds [elkjs](https://github.com/kieler/elkjs) 0.11.0, unmodified, which is licensed under the Eclipse Public License 2.0; see `NOTICE` and `rust/assets/elkjs-LICENSE.md`.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["maturin>=1.0,<2.0"]
|
|
3
|
+
build-backend = "maturin"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chartedcode"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Diagrams for Python projects: class, call and control-flow diagrams, drawn as self-contained HTML by a small deterministic Rust binary."
|
|
9
|
+
keywords = ["uml", "diagram", "call-graph", "control-flow", "class-diagram", "static-analysis", "documentation"]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "William Martin" }
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENSE", "NOTICE", "rust/assets/elkjs-LICENSE.md"]
|
|
16
|
+
requires-python = ">=3.11"
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: Python :: 3.15",
|
|
28
|
+
"Topic :: Software Development :: Documentation",
|
|
29
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
30
|
+
]
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/w-martin/chartedcode"
|
|
35
|
+
Repository = "https://github.com/w-martin/chartedcode"
|
|
36
|
+
"Issue Tracker" = "https://github.com/w-martin/chartedcode/issues"
|
|
37
|
+
|
|
38
|
+
[tool.maturin]
|
|
39
|
+
manifest-path = "rust/Cargo.toml"
|
|
40
|
+
python-source = "src"
|
|
41
|
+
# The wheel carries the Rust `chartedcode` executable (installed as a script) next to the Python package.
|
|
42
|
+
bindings = "bin"
|
|
43
|
+
module-name = "chartedcode"
|
|
44
|
+
# `chartedcode-py`: a Python script installed beside the executable, which maturin cannot declare as an entry point.
|
|
45
|
+
data = "rust/python-data"
|
|
46
|
+
include = [
|
|
47
|
+
{ path = "src/chartedcode/_rust.*", format = "wheel" },
|
|
48
|
+
{ path = "rust/python/**/*", format = "sdist" },
|
|
49
|
+
{ path = "rust/assets/**/*", format = "sdist" },
|
|
50
|
+
{ path = "LICENSE", format = "sdist" },
|
|
51
|
+
{ path = "NOTICE", format = "sdist" },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
addopts = "--cov=src --cov-report=term-missing --cov-report=json --cov-branch --cov-fail-under=100"
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
markers = ["ui: drives a headless Chrome; skipped when none is installed"]
|
|
58
|
+
|
|
59
|
+
[tool.coverage.run]
|
|
60
|
+
branch = true
|
|
61
|
+
source = ["src"]
|
|
62
|
+
|
|
63
|
+
[tool.coverage.report]
|
|
64
|
+
exclude_lines = [
|
|
65
|
+
"pragma: no cover",
|
|
66
|
+
"if TYPE_CHECKING:",
|
|
67
|
+
"raise NotImplementedError",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
line-length = 120
|
|
72
|
+
target-version = "py311"
|
|
73
|
+
fix = true
|
|
74
|
+
# Not project code: the research spike, fixture projects whose exact text the tests depend on, and build output.
|
|
75
|
+
extend-exclude = ["spike", "rust/tests/fixtures", "rust/target"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.format]
|
|
78
|
+
quote-style = "double"
|
|
79
|
+
indent-style = "space"
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint]
|
|
82
|
+
select = ["ALL"]
|
|
83
|
+
ignore = [
|
|
84
|
+
"D203", # Conflicts with D211
|
|
85
|
+
"D212", # Conflicts with D213
|
|
86
|
+
"COM812", # Conflicts with ruff formatter
|
|
87
|
+
"CPY001", # Project does not use copyright header notices
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.per-file-ignores]
|
|
91
|
+
"tests/*" = ["S101", "SLF001", "S603", "S607"]
|
|
92
|
+
"tasks.py" = ["T201"]
|
|
93
|
+
# The one module that starts the Rust executable. Its command is always the installed `chartedcode`
|
|
94
|
+
# executable, never user-supplied text.
|
|
95
|
+
"src/chartedcode/_binary.py" = ["S603", "S606"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.mccabe]
|
|
98
|
+
max-complexity = 10
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.pydocstyle]
|
|
101
|
+
convention = "google"
|
|
102
|
+
|
|
103
|
+
[dependency-groups]
|
|
104
|
+
dev = [
|
|
105
|
+
"bandit>=1.9.4",
|
|
106
|
+
"invoke>=3.0.3",
|
|
107
|
+
"maturin>=1.15.0",
|
|
108
|
+
"pytest>=9.1.1",
|
|
109
|
+
"pytest-cov>=7.1.0",
|
|
110
|
+
"ruff>=0.17.2",
|
|
111
|
+
"trustedlicenses>=0.3.4",
|
|
112
|
+
"ty>=0.0.91",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
[tool.bandit]
|
|
116
|
+
# bandit cannot skip one rule for one file, so the module that starts the Rust executable (B404, B603, B606;
|
|
117
|
+
# see `_binary.py`) is excluded whole. It does nothing else.
|
|
118
|
+
exclude_dirs = ["tests", "src/chartedcode/_binary.py"]
|
|
119
|
+
|
|
120
|
+
[tool.trustedlicenses]
|
|
121
|
+
allowed-categories = ["Permissive", "Public Domain", "Copyleft Limited"]
|
|
122
|
+
|
|
123
|
+
[tool.ty.terminal]
|
|
124
|
+
output-format = "concise"
|
|
125
|
+
|
|
126
|
+
[tool.ty.src]
|
|
127
|
+
# The same directories ruff skips: the spike, fixture projects that are broken on purpose, and build output.
|
|
128
|
+
exclude = ["spike", "rust/tests/fixtures", "rust/target"]
|