Skip to content

Commit 9057de3

Browse files
committed
feat: add mktestdocs to catch docs/code drift (closes #1083)
Adds automated testing of Python code examples in the documentation to prevent examples from drifting out of sync with the library. Changes: - Add `mktestdocs` and `pytest` to the `doc` dependency group in `pyproject.toml` so they are available alongside the other doc-build tools without pulling in the full `dev` group. - Add `scripts/check-docs-drift.py`: a pytest-based script that uses `mktestdocs.grab_code_blocks()` to collect every ```python fenced block under `docs/`, skips any block whose first line is `# skip`, and executes the rest via `exec_python()`. A new taskipy task `docs-check-drift` runs it with `pytest scripts/check-docs-drift.py -v`. - Fix all ```python code blocks across `docs/` so they are correctly picked up by mktestdocs: - Remove the stray space in ``` python fences (changed to ```python) so that mktestdocs can identify them (it matches on the exact string "python" immediately after the backticks). - Add `save_to_file=False, log_level="error"` to `EmissionsTracker` and `OfflineEmissionsTracker` instantiations to avoid creating CSV files or noisy output during CI runs. - Add `# skip` as the first line of blocks that cannot run in CI because they depend on external services or optional heavy dependencies (TensorFlow, Prometheus, Logfire, Google Cloud, Comet ML, live CodeCarbon API). - Correct a `pip install` command that was incorrectly fenced as ```python in `comet.md`; changed to ```console. - Update `.github/workflows/build-docs.yml` to run `docs-check-drift` as a step before the docs build, triggered on changes to `docs/**`, `mkdocs.yml`, or `scripts/check-docs-drift.py`. - Document the drift check and the `# skip` convention in `CONTRIBUTING.md` under the "Build Documentation" section.
1 parent 9be333a commit 9057de3

12 files changed

Lines changed: 412 additions & 4210 deletions

File tree

.github/workflows/build-docs.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ on:
99
paths:
1010
- "docs/**"
1111
- "mkdocs.yml"
12+
- "scripts/check-docs-drift.py"
1213
pull_request:
1314
branches: [master]
1415
paths:
1516
- "docs/**"
1617
- "mkdocs.yml"
18+
- "scripts/check-docs-drift.py"
1719

1820
jobs:
1921
build:
@@ -32,5 +34,8 @@ jobs:
3234
- name: Install dependencies
3335
run: uv sync --group doc
3436

37+
- name: Check docs code examples
38+
run: uv run task docs-check-drift
39+
3540
- name: Build docs
3641
run: uv run zensical build -f mkdocs.yml --clean

CONTRIBUTING.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,28 @@ uv run --only-group doc task docs
284284

285285
to regenerate the html files. For local preview with live reload, run `uv run --only-group doc task docs-serve`.
286286

287+
#### Testing documentation code examples
288+
289+
Python code blocks in the docs can be checked to catch examples that have drifted out of sync with the library. Run the check with:
290+
291+
```sh
292+
uv run task docs-check-drift
293+
```
294+
295+
This executes every ` ```python ` fenced block found under `docs/` using [mktestdocs](https://github.com/koaning/mktestdocs). The check is driven by `scripts/check-docs-drift.py`.
296+
297+
**Adding a new code block to the docs?** Use a ` ```python ` fence so it is picked up. If the block cannot run in CI (e.g. it requires TensorFlow, a live API, or other external dependencies), add `# skip` as the first line inside the block:
298+
299+
````markdown
300+
```python
301+
# skip
302+
import tensorflow as tf
303+
...
304+
```
305+
````
306+
307+
Blocks marked `# skip` are excluded from the check but still rendered normally in the documentation.
308+
287309
### Rebase your branch on master
288310

289311
Before creating a PR, please make sure to rebase your branch on master to avoid merge conflicts and make the review easier. You can do it with the following command:

docs/getting-started/api.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@ codecarbon monitor
3232

3333
Or use the API in your code:
3434

35-
``` python
35+
```python
36+
# skip
3637
from codecarbon import track_emissions
3738

3839
@track_emissions(save_to_api=True)
3940
def train_model():
40-
# GPU intensive training code goes here
41+
# GPU intensive training code goes here
42+
pass
4143

42-
if __name__ =="__main__":
44+
if __name__ == "__main__":
4345
train_model()
4446
```
4547

@@ -59,14 +61,16 @@ You then have to set your experiment id in CodeCarbon, with two options:
5961

6062
In the code:
6163

62-
``` python
64+
```python
6365
from codecarbon import track_emissions
6466

6567
@track_emissions(
6668
measure_power_secs=30,
6769
api_call_interval=4,
6870
experiment_id="your experiment id",
6971
save_to_api=True,
72+
save_to_file=False,
73+
log_level="error",
7074
)
7175
def train_model():
7276
...

docs/getting-started/comet.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ and more.
1313
To get started with the Comet-CodeCarbon integration, make sure you have
1414
comet-ml installed:
1515

16-
``` python
16+
``` console
1717
pip install comet_ml>=3.2.2
1818
```
1919

@@ -24,7 +24,8 @@ In the
2424
[mnist-comet.py](https://github.com/mlco2/codecarbon/blob/master/examples/mnist-comet.py)
2525
example file, replace the placeholder code with your API key:
2626

27-
``` python
27+
```python
28+
# skip
2829
experiment = Experiment(api_key="YOUR API KEY")
2930
```
3031

docs/getting-started/examples.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ automatically and printed at the end of the training.
1313
But you can't get them in your code, see the Context Manager section
1414
below for that.
1515

16-
``` python
16+
```python
17+
# skip
1718
import tensorflow as tf
1819
from codecarbon import track_emissions
1920

@@ -49,7 +50,8 @@ if __name__ == "__main__":
4950
We think this is the best way to use CodeCarbon. Still only two lines of
5051
code, and you can get the emissions in your code.
5152

52-
``` python
53+
```python
54+
# skip
5355
import tensorflow as tf
5456

5557
from codecarbon import EmissionsTracker
@@ -95,7 +97,8 @@ CodeCarbon scheduler is stopped. If you don't use
9597
background after your computation code has crashed, so your program will
9698
never finish.
9799

98-
``` python
100+
```python
101+
# skip
99102
import tensorflow as tf
100103

101104
from codecarbon import EmissionsTracker

docs/getting-started/usage.md

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ code base, users can instantiate a `EmissionsTracker` object and pass it
145145
as a parameter to function calls to start and stop the emissions
146146
tracking of the compute section.
147147

148-
``` python
148+
```python
149149
from codecarbon import EmissionsTracker
150-
tracker = EmissionsTracker()
150+
tracker = EmissionsTracker(save_to_file=False, log_level="error")
151151
tracker.start()
152152
try:
153153
# Compute intensive code goes here
@@ -167,7 +167,8 @@ depending on the configuration, but keep running the experiment.
167167
If you want to monitor small piece of code, like a model inference, you
168168
could use the task manager:
169169

170-
``` python
170+
```python
171+
# skip
171172
try:
172173
tracker = EmissionsTracker(project_name="bert_inference", measure_power_secs=10)
173174
tracker.start_task("load dataset")
@@ -193,11 +194,11 @@ to interfere with the task measurement.
193194

194195
The `Emissions tracker` also works as a context manager.
195196

196-
``` python
197+
```python
197198
from codecarbon import EmissionsTracker
198199

199-
with EmissionsTracker() as tracker:
200-
# Compute intensive training code goes here
200+
with EmissionsTracker(save_to_file=False, log_level="error") as tracker:
201+
_ = 1 + 1 # Compute intensive training code goes here
201202
```
202203

203204
This mode is recommended when you want to monitor a specific block of
@@ -209,12 +210,13 @@ In case the training code base is wrapped in a function, users can use
209210
the decorator `@track_emissions` within the function to enable tracking
210211
emissions of the training code.
211212

212-
``` python
213+
```python
213214
from codecarbon import track_emissions
214215

215-
@track_emissions
216+
@track_emissions(save_to_file=False, log_level="error")
216217
def training_loop():
217218
# Compute intensive training code goes here
219+
pass
218220
```
219221

220222
This mode is recommended if you have a training function.
@@ -239,9 +241,9 @@ can be found on
239241
Developers can use the `OfflineEmissionsTracker` object to track
240242
emissions as follows:
241243

242-
``` python
244+
```python
243245
from codecarbon import OfflineEmissionsTracker
244-
tracker = OfflineEmissionsTracker(country_iso_code="CAN")
246+
tracker = OfflineEmissionsTracker(country_iso_code="CAN", save_to_file=False, log_level="error")
245247
tracker.start()
246248
# GPU intensive training code
247249
tracker.stop()
@@ -251,11 +253,12 @@ tracker.stop()
251253

252254
The `OfflineEmissionsTracker` also works as a context manager
253255

254-
``` python
256+
```python
255257
from codecarbon import OfflineEmissionsTracker
256258

257-
with OfflineEmissionsTracker() as tracker:
258-
# GPU intensive training code goes here
259+
with OfflineEmissionsTracker(country_iso_code="CAN", save_to_file=False, log_level="error") as tracker:
260+
# GPU intensive training code goes here
261+
pass
259262
```
260263

261264
### Decorator
@@ -270,7 +273,7 @@ parameters:
270273

271274
```python
272275
from codecarbon import track_emissions
273-
@track_emissions(offline=True, country_iso_code="CAN")
276+
@track_emissions(offline=True, country_iso_code="CAN", save_to_file=False, log_level="error")
274277
def training_loop():
275278
# training code goes here
276279
pass
@@ -334,7 +337,8 @@ CodeCarbon is structured so that you can configure it in a hierarchical manner:
334337
- script parameters will override environment variables if the same
335338
parameter is set in both:
336339

337-
``` python
340+
```python
341+
# skip
338342
EmissionsTracker(
339343
api_call_interval=4,
340344
save_to_api=True,
@@ -343,7 +347,7 @@ CodeCarbon is structured so that you can configure it in a hierarchical manner:
343347

344348
Yields attributes:
345349

346-
``` python
350+
```python
347351
{
348352
"measure_power_secs": 10, # from ~/.codecarbon.config
349353
"save_to_file": True, # from ./.codecarbon.config (override ~/.codecarbon.config)
@@ -382,7 +386,7 @@ export HTTPS_PROXY="http://0.0.0.0:0000"
382386

383387
Or in your Python code:
384388

385-
``` python
389+
```python
386390
import os
387391

388392
os.environ["HTTPS_PROXY"] = "http://0.0.0.0:0000"

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Only a few lines of code:
3838
```python
3939
from codecarbon import EmissionsTracker
4040

41-
tracker = EmissionsTracker()
41+
tracker = EmissionsTracker(save_to_file=False, log_level="error")
4242
tracker.start()
4343

4444
# Your code here

docs/logging/output.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ docker-compose up
6363
Run your EmissionsTracker as usual, with `save_to_prometheus=True`:
6464

6565
```python
66+
# skip
6667
tracker = OfflineEmissionsTracker(
6768
project_name="my_project",
6869
country_iso_code="USA",
@@ -86,6 +87,7 @@ CodeCarbon exposes all its metrics with the suffix `codecarbon_`.
8687
Run your EmissionsTracker as usual, with `save_to_logfire=True`:
8788

8889
```python
90+
# skip
8991
tracker = OfflineEmissionsTracker(
9092
project_name="my_project",
9193
country_iso_code="USA",

docs/logging/to_logger.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ or cloud-based collector.
2020

2121
### Python logger
2222

23-
``` python
23+
```python
24+
# skip
2425
import logging
2526

2627
# Create a dedicated logger (log name can be the CodeCarbon project name for example)
@@ -40,7 +41,8 @@ my_logger = LoggerOutput(_logger, logging.INFO)
4041

4142
### Google Cloud Logging
4243

43-
``` python
44+
```python
45+
# skip
4446
import google.cloud.logging
4547

4648

@@ -61,7 +63,8 @@ documentation](https://cloud.google.com/logging/docs/reference/libraries#setting
6163
Create an EmissionTracker saving output to the logger. Other save
6264
options are still usable and valid.
6365

64-
``` python
66+
```python
67+
# skip
6568
tracker = EmissionsTracker(save_to_logger=True, logging_logger=my_logger)
6669
tracker.start()
6770
# Your code here

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ doc = [
123123
"zensical",
124124
"mike",
125125
"mkdocstrings[python]>=0.26",
126+
"mktestdocs",
127+
"pytest",
126128
]
127129

128130
[project.optional-dependencies]
@@ -191,7 +193,7 @@ test-coverage = "CODECARBON_ALLOW_MULTIPLE_RUNS=True pytest --cov --cov-report=x
191193
test-package-integ = "CODECARBON_ALLOW_MULTIPLE_RUNS=True python -m pytest -vv tests/"
192194
docs = "zensical build -f mkdocs.yml"
193195
docs-serve = "zensical serve -f mkdocs.yml"
194-
docs-check-drift = "python scripts/check-docs-drift.py"
196+
docs-check-drift = "pytest scripts/check-docs-drift.py -v"
195197
carbonboard = "python codecarbon/viz/carbonboard.py"
196198

197199
[tool.bumpver]

0 commit comments

Comments
 (0)