cs-survival-kit 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 (26) hide show
  1. cs_survival_kit-0.1.0/.gitignore +18 -0
  2. cs_survival_kit-0.1.0/CHANGELOG.md +13 -0
  3. cs_survival_kit-0.1.0/LICENSE +21 -0
  4. cs_survival_kit-0.1.0/PKG-INFO +192 -0
  5. cs_survival_kit-0.1.0/README.md +166 -0
  6. cs_survival_kit-0.1.0/benchmarks/bench_dynamic_array.py +55 -0
  7. cs_survival_kit-0.1.0/pyproject.toml +76 -0
  8. cs_survival_kit-0.1.0/scripts/check_docs.py +140 -0
  9. cs_survival_kit-0.1.0/src/cs_survival_kit/__init__.py +14 -0
  10. cs_survival_kit-0.1.0/src/cs_survival_kit/_data/benchmarks.json +4 -0
  11. cs_survival_kit-0.1.0/src/cs_survival_kit/algorithms/__init__.py +1 -0
  12. cs_survival_kit-0.1.0/src/cs_survival_kit/bench/__init__.py +19 -0
  13. cs_survival_kit-0.1.0/src/cs_survival_kit/bench/__main__.py +5 -0
  14. cs_survival_kit-0.1.0/src/cs_survival_kit/bench/cli.py +140 -0
  15. cs_survival_kit-0.1.0/src/cs_survival_kit/bench/core.py +359 -0
  16. cs_survival_kit-0.1.0/src/cs_survival_kit/bench/storage.py +57 -0
  17. cs_survival_kit-0.1.0/src/cs_survival_kit/data_structures/__init__.py +5 -0
  18. cs_survival_kit-0.1.0/src/cs_survival_kit/data_structures/dynamic_array.py +204 -0
  19. cs_survival_kit-0.1.0/src/cs_survival_kit/py.typed +0 -0
  20. cs_survival_kit-0.1.0/tests/algorithms/.gitkeep +0 -0
  21. cs_survival_kit-0.1.0/tests/bench/test_bench_cli.py +105 -0
  22. cs_survival_kit-0.1.0/tests/bench/test_bench_core.py +311 -0
  23. cs_survival_kit-0.1.0/tests/bench/test_bench_storage.py +49 -0
  24. cs_survival_kit-0.1.0/tests/conftest.py +1 -0
  25. cs_survival_kit-0.1.0/tests/data_structures/.gitkeep +0 -0
  26. cs_survival_kit-0.1.0/tests/tooling/test_check_docs.py +175 -0
@@ -0,0 +1,18 @@
1
+ # Python
2
+ .venv/
3
+ __pycache__/
4
+ *.egg-info/
5
+
6
+ # Tooling caches
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .hypothesis/
10
+
11
+ # Build output
12
+ dist/
13
+
14
+ # Agent scratch space
15
+ .superpowers/
16
+
17
+ # OS
18
+ .DS_Store
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-10-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * **bench:** add benchmark toolkit ([#2](https://github.com/jwallace145/cs-survival-kit/issues/2)) ([3f28939](https://github.com/jwallace145/cs-survival-kit/commit/3f28939626bf68b0e3523a14b8fda053e2221612))
9
+
10
+
11
+ ### Miscellaneous
12
+
13
+ * **repo:** scaffold library, tooling, and CI ([#1](https://github.com/jwallace145/cs-survival-kit/issues/1)) ([3b9d319](https://github.com/jwallace145/cs-survival-kit/commit/3b9d319859145a92ff7a4aa02f548ce4e0ad7dbe))
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jimmy Wallace
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,192 @@
1
+ Metadata-Version: 2.5
2
+ Name: cs-survival-kit
3
+ Version: 0.1.0
4
+ Summary: Hand-written data structures and algorithms, plus a benchmarking toolkit.
5
+ Project-URL: Repository, https://github.com/jwallace145/cs-survival-kit
6
+ Project-URL: Documentation, https://jwallace145.github.io/cs-survival-guide/
7
+ Project-URL: Changelog, https://github.com/jwallace145/cs-survival-kit/blob/main/CHANGELOG.md
8
+ Author: Jimmy Wallace
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: algorithms,benchmarking,data-structures,education
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Education
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Provides-Extra: bench
25
+ Description-Content-Type: text/markdown
26
+
27
+ # cs-survival-kit
28
+
29
+ Hand-written data structures and algorithms in Python, plus a small toolkit
30
+ for benchmarking them.
31
+
32
+ This library is the companion to the
33
+ [CS Survival Guide](https://jwallace145.github.io/cs-survival-guide/)
34
+ ([source](https://github.com/jwallace145/cs-survival-guide)). The guide
35
+ explains the ideas; this package is the code. The guide's Reference section
36
+ is rendered directly from this package's docstrings and source.
37
+
38
+ Every data structure and algorithm here is written by hand, for study. The
39
+ goal is clarity over cleverness: read the source alongside the guide.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install cs-survival-kit
45
+ ```
46
+
47
+ Requires Python 3.12 or newer. The core package has no runtime dependencies.
48
+
49
+ ```python
50
+ from cs_survival_kit.data_structures import DynamicArray
51
+ ```
52
+
53
+ Structures land one at a time. A module whose functions still raise
54
+ `NotImplementedError` is a stub waiting for its implementation.
55
+
56
+ ## Benchmarks
57
+
58
+ `cs_survival_kit.bench` is a small, stdlib-only toolkit for measuring how code
59
+ scales with input size, and for checking the result against the complexity a
60
+ docstring claims.
61
+
62
+ ### Write a benchmark
63
+
64
+ A benchmark is a set of cases to compare across a range of sizes. Each case
65
+ has a `setup(n)` that builds the inputs (not timed) and a `run(inputs)` that
66
+ is timed:
67
+
68
+ ```python
69
+ # benchmarks/bench_sorting.py
70
+ from cs_survival_kit.bench import Benchmark
71
+
72
+
73
+ def bubble_sort(items: list[int]) -> None:
74
+ for end in range(len(items) - 1, 0, -1):
75
+ for i in range(end):
76
+ if items[i] > items[i + 1]:
77
+ items[i], items[i + 1] = items[i + 1], items[i]
78
+
79
+
80
+ sorting = Benchmark("sorting", sizes=[100, 200, 400, 800])
81
+ sorting.case("bubble sort", setup=lambda n: list(range(n, 0, -1)), run=bubble_sort)
82
+ sorting.case("list.sort", setup=lambda n: list(range(n, 0, -1)), run=list.sort)
83
+
84
+ BENCHMARKS = [sorting]
85
+ ```
86
+
87
+ `setup` is called again before every timed call, so `run` may mutate its
88
+ inputs. A case can pass its own `sizes=` to cap a slow implementation at
89
+ smaller inputs. A case that raises `NotImplementedError` is reported as
90
+ `not implemented` and skipped, so a benchmark can be written before the code
91
+ it measures.
92
+
93
+ ### Run it
94
+
95
+ ```bash
96
+ python -m cs_survival_kit.bench # every benchmarks/bench_*.py
97
+ python -m cs_survival_kit.bench benchmarks/bench_sorting.py # just one file
98
+ python -m cs_survival_kit.bench --smoke # check they execute; write nothing
99
+ ```
100
+
101
+ ```text
102
+ sorting
103
+ n bubble sort list.sort
104
+ 100 140 µs 256 ns
105
+ 200 546 µs 472 ns
106
+ 400 2.28 ms 905 ns
107
+ 800 10.4 ms 1.74 µs
108
+ slope 2.07 0.92
109
+ growth ~ quadratic ~ linear
110
+ ```
111
+
112
+ Each time is per call: the minimum of 5 measurements, with garbage collection
113
+ disabled, looping fast calls until a measurement lasts about 0.1 seconds.
114
+
115
+ `slope` is the least-squares slope of time against size on a log-log scale,
116
+ which approximates the exponent `k` in `O(n^k)`: about 0 is constant, about 1
117
+ is linear, about 2 is quadratic. `O(n log n)` reads as slightly above 1. It is
118
+ an empirical sanity check, not a proof.
119
+
120
+ A benchmark can also be driven from Python: `results = sorting.run(repeat=5)`,
121
+ then `results.table()`, `results.fit()` or `results.to_dict()`.
122
+
123
+ ### Stored results
124
+
125
+ A full run merges its results into
126
+ `src/cs_survival_kit/_data/benchmarks.json` (or `--output FILE`), keyed by
127
+ benchmark name, so re-running one file updates only its own entries. That file
128
+ ships inside the package.
129
+
130
+ Published numbers come from a single development machine, never from CI:
131
+ shared runners are too noisy. CI only runs `--smoke`.
132
+
133
+ ## Local development
134
+
135
+ ```bash
136
+ uv sync # create .venv and install dev tools
137
+
138
+ uv run ruff check # lint
139
+ uv run ruff format --check # formatting
140
+ uv run pyright # type check
141
+ uv run python scripts/check_docs.py # docs-completeness check
142
+ uv run pytest # tests and doctests
143
+ uv run python -m cs_survival_kit.bench --smoke # benchmarks execute
144
+ uv build # sdist and wheel into dist/
145
+ ```
146
+
147
+ All of these run in CI and must pass before a PR can merge.
148
+
149
+ ## Releases
150
+
151
+ cs-survival-kit uses [Conventional Commits](https://www.conventionalcommits.org)
152
+ and [Semantic Versioning](https://semver.org), starting in the `0.x`
153
+ development lifecycle.
154
+
155
+ ```text
156
+ feat: -> minor release (0.1.0 -> 0.2.0)
157
+ fix: -> patch release (0.2.0 -> 0.2.1)
158
+ BREAKING CHANGE / feat!: -> while in 0.x, also bumps the minor version
159
+ ```
160
+
161
+ [Release Please](https://github.com/googleapis/release-please) watches `main`
162
+ and maintains a release PR that accumulates changes. Merging that PR:
163
+
164
+ - updates the version in `pyproject.toml` (the version source of truth)
165
+ - updates `CHANGELOG.md`
166
+ - creates the SemVer git tag (e.g. `v0.4.0`) and the GitHub Release
167
+ - publishes the release to [PyPI](https://pypi.org/project/cs-survival-kit/)
168
+ - notifies the guide, which opens a PR to document the new version
169
+
170
+ The version and changelog are never edited by hand.
171
+
172
+ The publishing pipeline can be rehearsed without releasing anything: running
173
+ the **Publish to TestPyPI** workflow from the Actions tab builds `main` as a
174
+ throwaway `0.0.0.devN` version, publishes it to
175
+ [TestPyPI](https://test.pypi.org/project/cs-survival-kit/), and installs it
176
+ back.
177
+
178
+ ### Commit examples
179
+
180
+ ```text
181
+ feat(ds): add dynamic array
182
+ feat(algo): add binary search
183
+ fix(ds): correct dynamic array shrink threshold
184
+ feat(bench): add memory benchmarks
185
+ chore(deps): update ruff
186
+ ```
187
+
188
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full convention.
189
+
190
+ ## License
191
+
192
+ [MIT](LICENSE)
@@ -0,0 +1,166 @@
1
+ # cs-survival-kit
2
+
3
+ Hand-written data structures and algorithms in Python, plus a small toolkit
4
+ for benchmarking them.
5
+
6
+ This library is the companion to the
7
+ [CS Survival Guide](https://jwallace145.github.io/cs-survival-guide/)
8
+ ([source](https://github.com/jwallace145/cs-survival-guide)). The guide
9
+ explains the ideas; this package is the code. The guide's Reference section
10
+ is rendered directly from this package's docstrings and source.
11
+
12
+ Every data structure and algorithm here is written by hand, for study. The
13
+ goal is clarity over cleverness: read the source alongside the guide.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install cs-survival-kit
19
+ ```
20
+
21
+ Requires Python 3.12 or newer. The core package has no runtime dependencies.
22
+
23
+ ```python
24
+ from cs_survival_kit.data_structures import DynamicArray
25
+ ```
26
+
27
+ Structures land one at a time. A module whose functions still raise
28
+ `NotImplementedError` is a stub waiting for its implementation.
29
+
30
+ ## Benchmarks
31
+
32
+ `cs_survival_kit.bench` is a small, stdlib-only toolkit for measuring how code
33
+ scales with input size, and for checking the result against the complexity a
34
+ docstring claims.
35
+
36
+ ### Write a benchmark
37
+
38
+ A benchmark is a set of cases to compare across a range of sizes. Each case
39
+ has a `setup(n)` that builds the inputs (not timed) and a `run(inputs)` that
40
+ is timed:
41
+
42
+ ```python
43
+ # benchmarks/bench_sorting.py
44
+ from cs_survival_kit.bench import Benchmark
45
+
46
+
47
+ def bubble_sort(items: list[int]) -> None:
48
+ for end in range(len(items) - 1, 0, -1):
49
+ for i in range(end):
50
+ if items[i] > items[i + 1]:
51
+ items[i], items[i + 1] = items[i + 1], items[i]
52
+
53
+
54
+ sorting = Benchmark("sorting", sizes=[100, 200, 400, 800])
55
+ sorting.case("bubble sort", setup=lambda n: list(range(n, 0, -1)), run=bubble_sort)
56
+ sorting.case("list.sort", setup=lambda n: list(range(n, 0, -1)), run=list.sort)
57
+
58
+ BENCHMARKS = [sorting]
59
+ ```
60
+
61
+ `setup` is called again before every timed call, so `run` may mutate its
62
+ inputs. A case can pass its own `sizes=` to cap a slow implementation at
63
+ smaller inputs. A case that raises `NotImplementedError` is reported as
64
+ `not implemented` and skipped, so a benchmark can be written before the code
65
+ it measures.
66
+
67
+ ### Run it
68
+
69
+ ```bash
70
+ python -m cs_survival_kit.bench # every benchmarks/bench_*.py
71
+ python -m cs_survival_kit.bench benchmarks/bench_sorting.py # just one file
72
+ python -m cs_survival_kit.bench --smoke # check they execute; write nothing
73
+ ```
74
+
75
+ ```text
76
+ sorting
77
+ n bubble sort list.sort
78
+ 100 140 µs 256 ns
79
+ 200 546 µs 472 ns
80
+ 400 2.28 ms 905 ns
81
+ 800 10.4 ms 1.74 µs
82
+ slope 2.07 0.92
83
+ growth ~ quadratic ~ linear
84
+ ```
85
+
86
+ Each time is per call: the minimum of 5 measurements, with garbage collection
87
+ disabled, looping fast calls until a measurement lasts about 0.1 seconds.
88
+
89
+ `slope` is the least-squares slope of time against size on a log-log scale,
90
+ which approximates the exponent `k` in `O(n^k)`: about 0 is constant, about 1
91
+ is linear, about 2 is quadratic. `O(n log n)` reads as slightly above 1. It is
92
+ an empirical sanity check, not a proof.
93
+
94
+ A benchmark can also be driven from Python: `results = sorting.run(repeat=5)`,
95
+ then `results.table()`, `results.fit()` or `results.to_dict()`.
96
+
97
+ ### Stored results
98
+
99
+ A full run merges its results into
100
+ `src/cs_survival_kit/_data/benchmarks.json` (or `--output FILE`), keyed by
101
+ benchmark name, so re-running one file updates only its own entries. That file
102
+ ships inside the package.
103
+
104
+ Published numbers come from a single development machine, never from CI:
105
+ shared runners are too noisy. CI only runs `--smoke`.
106
+
107
+ ## Local development
108
+
109
+ ```bash
110
+ uv sync # create .venv and install dev tools
111
+
112
+ uv run ruff check # lint
113
+ uv run ruff format --check # formatting
114
+ uv run pyright # type check
115
+ uv run python scripts/check_docs.py # docs-completeness check
116
+ uv run pytest # tests and doctests
117
+ uv run python -m cs_survival_kit.bench --smoke # benchmarks execute
118
+ uv build # sdist and wheel into dist/
119
+ ```
120
+
121
+ All of these run in CI and must pass before a PR can merge.
122
+
123
+ ## Releases
124
+
125
+ cs-survival-kit uses [Conventional Commits](https://www.conventionalcommits.org)
126
+ and [Semantic Versioning](https://semver.org), starting in the `0.x`
127
+ development lifecycle.
128
+
129
+ ```text
130
+ feat: -> minor release (0.1.0 -> 0.2.0)
131
+ fix: -> patch release (0.2.0 -> 0.2.1)
132
+ BREAKING CHANGE / feat!: -> while in 0.x, also bumps the minor version
133
+ ```
134
+
135
+ [Release Please](https://github.com/googleapis/release-please) watches `main`
136
+ and maintains a release PR that accumulates changes. Merging that PR:
137
+
138
+ - updates the version in `pyproject.toml` (the version source of truth)
139
+ - updates `CHANGELOG.md`
140
+ - creates the SemVer git tag (e.g. `v0.4.0`) and the GitHub Release
141
+ - publishes the release to [PyPI](https://pypi.org/project/cs-survival-kit/)
142
+ - notifies the guide, which opens a PR to document the new version
143
+
144
+ The version and changelog are never edited by hand.
145
+
146
+ The publishing pipeline can be rehearsed without releasing anything: running
147
+ the **Publish to TestPyPI** workflow from the Actions tab builds `main` as a
148
+ throwaway `0.0.0.devN` version, publishes it to
149
+ [TestPyPI](https://test.pypi.org/project/cs-survival-kit/), and installs it
150
+ back.
151
+
152
+ ### Commit examples
153
+
154
+ ```text
155
+ feat(ds): add dynamic array
156
+ feat(algo): add binary search
157
+ fix(ds): correct dynamic array shrink threshold
158
+ feat(bench): add memory benchmarks
159
+ chore(deps): update ruff
160
+ ```
161
+
162
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full convention.
163
+
164
+ ## License
165
+
166
+ [MIT](LICENSE)
@@ -0,0 +1,55 @@
1
+ """Benchmarks for DynamicArray: the cost of n appends under each growth policy.
2
+
3
+ Expected story: geometric growth (doubling, or any factor above 1) makes
4
+ append amortized O(1), so n appends are linear in n (slope about 1), the same
5
+ as the built-in list. Additive growth copies the whole array every `step`
6
+ appends, so n appends are quadratic (slope about 2).
7
+ """
8
+
9
+ from typing import Protocol
10
+
11
+ from cs_survival_kit.bench import Benchmark
12
+ from cs_survival_kit.data_structures.dynamic_array import (
13
+ DynamicArray,
14
+ additive,
15
+ doubling,
16
+ geometric,
17
+ )
18
+
19
+
20
+ class Appendable(Protocol):
21
+ """Anything with a list-style `append`."""
22
+
23
+ def append(self, value: int, /) -> None:
24
+ """Add a value at the end."""
25
+ ...
26
+
27
+
28
+ def append_n(inputs: tuple[Appendable, int]) -> None:
29
+ """Append n integers to a fresh container."""
30
+ container, n = inputs
31
+ for i in range(n):
32
+ container.append(i)
33
+
34
+
35
+ append = Benchmark("dynamic_array.append", sizes=[10**k for k in range(2, 7)])
36
+ append.case(
37
+ "DynamicArray(doubling)",
38
+ setup=lambda n: (DynamicArray[int](growth=doubling), n),
39
+ run=append_n,
40
+ )
41
+ append.case(
42
+ "DynamicArray(geometric(1.5))",
43
+ setup=lambda n: (DynamicArray[int](growth=geometric(1.5)), n),
44
+ run=append_n,
45
+ )
46
+ append.case(
47
+ "DynamicArray(additive(16))",
48
+ setup=lambda n: (DynamicArray[int](growth=additive(16)), n),
49
+ run=append_n,
50
+ # Quadratic: capped well below the other cases so a full run stays short.
51
+ sizes=[1_000, 2_000, 5_000, 10_000, 20_000, 50_000],
52
+ )
53
+ append.case("list", setup=lambda n: (list[int](), n), run=append_n)
54
+
55
+ BENCHMARKS = [append]
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cs-survival-kit"
7
+ # Managed by Release Please. Do not edit by hand.
8
+ version = "0.1.0"
9
+ description = "Hand-written data structures and algorithms, plus a benchmarking toolkit."
10
+ readme = "README.md"
11
+ requires-python = ">=3.12"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ authors = [{ name = "Jimmy Wallace" }]
15
+ keywords = ["algorithms", "data-structures", "benchmarking", "education"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Education",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Education",
26
+ "Topic :: Software Development :: Libraries",
27
+ "Typing :: Typed",
28
+ ]
29
+ # The core package has zero runtime dependencies. Keep it that way.
30
+ dependencies = []
31
+
32
+ [project.optional-dependencies]
33
+ bench = []
34
+
35
+ [project.urls]
36
+ Repository = "https://github.com/jwallace145/cs-survival-kit"
37
+ Documentation = "https://jwallace145.github.io/cs-survival-guide/"
38
+ Changelog = "https://github.com/jwallace145/cs-survival-kit/blob/main/CHANGELOG.md"
39
+
40
+ [dependency-groups]
41
+ dev = ["hypothesis", "pyright", "pytest", "ruff"]
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/cs_survival_kit"]
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = ["/src", "/tests", "/benchmarks", "/scripts", "/README.md", "/LICENSE", "/CHANGELOG.md"]
48
+
49
+ [tool.ruff]
50
+ src = ["src", "scripts"]
51
+
52
+ [tool.ruff.format]
53
+ # Only format Python sources; leave code blocks in markdown as written.
54
+ exclude = ["*.md"]
55
+
56
+ [tool.ruff.lint]
57
+ select = ["E", "F", "I", "UP", "B", "D"]
58
+ # Constructor args are documented on the class, not on __init__.
59
+ ignore = ["D107"]
60
+
61
+ [tool.ruff.lint.pydocstyle]
62
+ convention = "google"
63
+
64
+ [tool.ruff.lint.per-file-ignores]
65
+ "tests/**" = ["D"]
66
+
67
+ [tool.pyright]
68
+ include = ["src", "scripts", "tests", "benchmarks"]
69
+ strict = ["src"]
70
+ extraPaths = ["scripts"]
71
+ pythonVersion = "3.12"
72
+
73
+ [tool.pytest.ini_options]
74
+ addopts = "--doctest-modules"
75
+ testpaths = ["src", "tests"]
76
+ pythonpath = ["scripts"]
@@ -0,0 +1,140 @@
1
+ """Docs-completeness check for the study modules.
2
+
3
+ Enforces two rules over ``data_structures/`` and ``algorithms/``:
4
+
5
+ 1. Every public class has a ``Complexity:`` section in its docstring.
6
+ 2. An implemented function or method (its body is no longer just
7
+ ``raise NotImplementedError``) must not have ``TODO`` in its own docstring,
8
+ its class docstring, or its module docstring.
9
+
10
+ Stubs may keep their placeholders; nothing ships implemented and undocumented.
11
+
12
+ Usage:
13
+ python scripts/check_docs.py [PATH ...]
14
+ """
15
+
16
+ import ast
17
+ import sys
18
+ from collections.abc import Iterator, Sequence
19
+ from pathlib import Path
20
+
21
+ DEFAULT_PATHS = (
22
+ "src/cs_survival_kit/data_structures",
23
+ "src/cs_survival_kit/algorithms",
24
+ )
25
+ PLACEHOLDER = "TODO"
26
+ COMPLEXITY_HEADER = "Complexity:"
27
+
28
+ type FunctionNode = ast.FunctionDef | ast.AsyncFunctionDef
29
+
30
+
31
+ def _has_placeholder(node: ast.Module | ast.ClassDef | FunctionNode) -> bool:
32
+ return PLACEHOLDER in (ast.get_docstring(node) or "")
33
+
34
+
35
+ def _is_stub(function: FunctionNode) -> bool:
36
+ """Return whether the body is only ``raise NotImplementedError``."""
37
+ body = function.body
38
+ if (
39
+ body
40
+ and isinstance(body[0], ast.Expr)
41
+ and isinstance(body[0].value, ast.Constant)
42
+ and isinstance(body[0].value.value, str)
43
+ ):
44
+ body = body[1:]
45
+ if len(body) != 1 or not isinstance(body[0], ast.Raise):
46
+ return False
47
+ raised = body[0].exc
48
+ if isinstance(raised, ast.Call):
49
+ raised = raised.func
50
+ return isinstance(raised, ast.Name) and raised.id == "NotImplementedError"
51
+
52
+
53
+ def _walk(
54
+ body: Sequence[ast.stmt], classes: tuple[ast.ClassDef, ...] = ()
55
+ ) -> Iterator[tuple[ast.ClassDef | FunctionNode, tuple[ast.ClassDef, ...]]]:
56
+ """Yield each class, function and method with its enclosing classes.
57
+
58
+ Functions nested inside other functions are implementation details and
59
+ are not visited.
60
+ """
61
+ for node in body:
62
+ if isinstance(node, ast.ClassDef):
63
+ yield node, classes
64
+ yield from _walk(node.body, (*classes, node))
65
+ elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
66
+ yield node, classes
67
+
68
+
69
+ def check_source(source: str, filename: str) -> list[str]:
70
+ """Check one module's source and return its violations.
71
+
72
+ Args:
73
+ source: The module's source code.
74
+ filename: Name used to prefix each violation.
75
+
76
+ Returns:
77
+ One ``file:line: message`` string per violation, in source order.
78
+ """
79
+ module = ast.parse(source, filename=filename)
80
+ errors: list[str] = []
81
+
82
+ def report(node: ast.ClassDef | FunctionNode, message: str) -> None:
83
+ errors.append(f"{filename}:{node.lineno}: {message}")
84
+
85
+ for node, classes in _walk(module.body):
86
+ name = ".".join([*(cls.name for cls in classes), node.name])
87
+ if isinstance(node, ast.ClassDef):
88
+ public = not any(part.startswith("_") for part in name.split("."))
89
+ if public and COMPLEXITY_HEADER not in (ast.get_docstring(node) or ""):
90
+ report(node, f"class {name} has no '{COMPLEXITY_HEADER}' section")
91
+ continue
92
+ if _is_stub(node):
93
+ continue
94
+ scopes = [("its", node), ("its module", module)]
95
+ if classes:
96
+ scopes.insert(1, ("its class", classes[-1]))
97
+ for owner, scope in scopes:
98
+ if _has_placeholder(scope):
99
+ report(
100
+ node,
101
+ f"{name} is implemented but {owner} docstring "
102
+ f"contains {PLACEHOLDER}",
103
+ )
104
+ return errors
105
+
106
+
107
+ def main(argv: Sequence[str] | None = None) -> int:
108
+ """Run the check over the given paths (default: the study modules).
109
+
110
+ Returns:
111
+ 0 if clean, 1 if there are violations, 2 if a path does not exist.
112
+ """
113
+ args = sys.argv[1:] if argv is None else argv
114
+ roots = [Path(arg) for arg in (args or DEFAULT_PATHS)]
115
+
116
+ files: list[Path] = []
117
+ for root in roots:
118
+ if root.is_file():
119
+ files.append(root)
120
+ elif root.is_dir():
121
+ files.extend(sorted(root.rglob("*.py")))
122
+ else:
123
+ print(f"check_docs: path does not exist: {root}", file=sys.stderr)
124
+ return 2
125
+
126
+ errors: list[str] = []
127
+ for file in files:
128
+ errors.extend(check_source(file.read_text(encoding="utf-8"), str(file)))
129
+
130
+ for error in errors:
131
+ print(error)
132
+ if errors:
133
+ print(f"\ncheck_docs: {len(errors)} problem(s) in {len(files)} file(s)")
134
+ return 1
135
+ print(f"check_docs: {len(files)} file(s) OK")
136
+ return 0
137
+
138
+
139
+ if __name__ == "__main__":
140
+ raise SystemExit(main())