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.
Files changed (68) hide show
  1. chartedcode-0.1.0/LICENSE +21 -0
  2. chartedcode-0.1.0/NOTICE +18 -0
  3. chartedcode-0.1.0/PKG-INFO +153 -0
  4. chartedcode-0.1.0/README.md +123 -0
  5. chartedcode-0.1.0/pyproject.toml +128 -0
  6. chartedcode-0.1.0/rust/Cargo.lock +1101 -0
  7. chartedcode-0.1.0/rust/Cargo.toml +50 -0
  8. chartedcode-0.1.0/rust/assets/calls.html +403 -0
  9. chartedcode-0.1.0/rust/assets/classes.html +110 -0
  10. chartedcode-0.1.0/rust/assets/elk.bundled.js +6663 -0
  11. chartedcode-0.1.0/rust/assets/elk.bundled.js.sha256 +1 -0
  12. chartedcode-0.1.0/rust/assets/elkjs-LICENSE.md +264 -0
  13. chartedcode-0.1.0/rust/assets/flow.html +181 -0
  14. chartedcode-0.1.0/rust/python/Cargo.toml +24 -0
  15. chartedcode-0.1.0/rust/python/build.rs +4 -0
  16. chartedcode-0.1.0/rust/python/src/lib.rs +27 -0
  17. chartedcode-0.1.0/rust/python-data/scripts/chartedcode-py +4 -0
  18. chartedcode-0.1.0/rust/src/analysis.rs +202 -0
  19. chartedcode-0.1.0/rust/src/bundle.rs +129 -0
  20. chartedcode-0.1.0/rust/src/calls.rs +552 -0
  21. chartedcode-0.1.0/rust/src/cfg.rs +555 -0
  22. chartedcode-0.1.0/rust/src/classes.rs +188 -0
  23. chartedcode-0.1.0/rust/src/cli.rs +243 -0
  24. chartedcode-0.1.0/rust/src/discover.rs +62 -0
  25. chartedcode-0.1.0/rust/src/lib.rs +22 -0
  26. chartedcode-0.1.0/rust/src/main.rs +10 -0
  27. chartedcode-0.1.0/rust/src/model.rs +292 -0
  28. chartedcode-0.1.0/rust/src/modules.rs +48 -0
  29. chartedcode-0.1.0/rust/src/parse.rs +451 -0
  30. chartedcode-0.1.0/rust/src/render.rs +170 -0
  31. chartedcode-0.1.0/rust/src/resolve.rs +310 -0
  32. chartedcode-0.1.0/rust/tests/bundle.rs +272 -0
  33. chartedcode-0.1.0/rust/tests/calls.rs +422 -0
  34. chartedcode-0.1.0/rust/tests/cli.rs +194 -0
  35. chartedcode-0.1.0/rust/tests/discover.rs +120 -0
  36. chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/__init__.py +1 -0
  37. chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/_internal.py +5 -0
  38. chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/app.py +61 -0
  39. chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/priv.py +27 -0
  40. chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/rec.py +16 -0
  41. chartedcode-0.1.0/rust/tests/fixtures/calls/pkg/util.py +29 -0
  42. chartedcode-0.1.0/rust/tests/fixtures/flow/flow.py +131 -0
  43. chartedcode-0.1.0/rust/tests/fixtures/imports/app/__init__.py +1 -0
  44. chartedcode-0.1.0/rust/tests/fixtures/imports/app/base.py +2 -0
  45. chartedcode-0.1.0/rust/tests/fixtures/imports/app/core.py +2 -0
  46. chartedcode-0.1.0/rust/tests/fixtures/imports/app/models.py +9 -0
  47. chartedcode-0.1.0/rust/tests/fixtures/imports/app/other.py +2 -0
  48. chartedcode-0.1.0/rust/tests/fixtures/imports/app/services.py +35 -0
  49. chartedcode-0.1.0/rust/tests/fixtures/imports/app/sub/__init__.py +0 -0
  50. chartedcode-0.1.0/rust/tests/fixtures/imports/app/sub/deep.py +15 -0
  51. chartedcode-0.1.0/rust/tests/fixtures/imports/app/sub/helper.py +2 -0
  52. chartedcode-0.1.0/rust/tests/fixtures/imports/app/sub/stars.py +5 -0
  53. chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/__init__.py +1 -0
  54. chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/base.py +2 -0
  55. chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/core.py +2 -0
  56. chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/models.py +9 -0
  57. chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/other.py +2 -0
  58. chartedcode-0.1.0/rust/tests/fixtures/imports/copy2/app/services.py +35 -0
  59. chartedcode-0.1.0/rust/tests/fixtures/imports/tests_dir/test_fakes.py +17 -0
  60. chartedcode-0.1.0/rust/tests/fixtures/imports/uniq_pkg/__init__.py +0 -0
  61. chartedcode-0.1.0/rust/tests/fixtures/imports/uniq_pkg/ports.py +5 -0
  62. chartedcode-0.1.0/rust/tests/flow.rs +421 -0
  63. chartedcode-0.1.0/rust/tests/imports.rs +224 -0
  64. chartedcode-0.1.0/rust/tests/render.rs +204 -0
  65. chartedcode-0.1.0/src/chartedcode/__init__.py +1 -0
  66. chartedcode-0.1.0/src/chartedcode/__main__.py +5 -0
  67. chartedcode-0.1.0/src/chartedcode/_binary.py +134 -0
  68. 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.
@@ -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"]