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.
- cs_survival_kit-0.1.0/.gitignore +18 -0
- cs_survival_kit-0.1.0/CHANGELOG.md +13 -0
- cs_survival_kit-0.1.0/LICENSE +21 -0
- cs_survival_kit-0.1.0/PKG-INFO +192 -0
- cs_survival_kit-0.1.0/README.md +166 -0
- cs_survival_kit-0.1.0/benchmarks/bench_dynamic_array.py +55 -0
- cs_survival_kit-0.1.0/pyproject.toml +76 -0
- cs_survival_kit-0.1.0/scripts/check_docs.py +140 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/__init__.py +14 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/_data/benchmarks.json +4 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/algorithms/__init__.py +1 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/bench/__init__.py +19 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/bench/__main__.py +5 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/bench/cli.py +140 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/bench/core.py +359 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/bench/storage.py +57 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/data_structures/__init__.py +5 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/data_structures/dynamic_array.py +204 -0
- cs_survival_kit-0.1.0/src/cs_survival_kit/py.typed +0 -0
- cs_survival_kit-0.1.0/tests/algorithms/.gitkeep +0 -0
- cs_survival_kit-0.1.0/tests/bench/test_bench_cli.py +105 -0
- cs_survival_kit-0.1.0/tests/bench/test_bench_core.py +311 -0
- cs_survival_kit-0.1.0/tests/bench/test_bench_storage.py +49 -0
- cs_survival_kit-0.1.0/tests/conftest.py +1 -0
- cs_survival_kit-0.1.0/tests/data_structures/.gitkeep +0 -0
- cs_survival_kit-0.1.0/tests/tooling/test_check_docs.py +175 -0
|
@@ -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())
|