Adding a New Library¶

This guide walks you through the full lifecycle of adding a new library to ChuMicro, from idea to published package. If you're working with an AI agent, point it at the new-library skill.
Before you start¶
Check open issues and discussions, and skim plans/next-up.md and plans/decisions/ to see if your idea overlaps with planned work or settled design choices. If you're unsure whether the library fits the project, open a discussion first. It's much faster to align on scope before building.
Is your package host-only? This guide is for device libraries: code that runs on CircuitPython, MicroPython, and CPython. If you're adding a tool that runs only on the developer's laptop (a CLI that drives devices, a REPL client, a firmware helper), it belongs in
workbench/instead. Seeworkbench.mdfor the layout and conventions. The scaffolder supports workbench packages: from inside the mono-repo runpython scripts/run.py new-library --workbench <name>; for a workspace project outside the mono-repo usechumicro-workspace new --workbench <name>.
1. Scaffold¶
The mono-repo wrapper composes the chumicro_workspace.scaffold.scaffold_library primitive (owned by the workbench chumicro-workspace package) with the mono-repo-only follow-ups (editable install + IDE config sync). External users developing their own chumicro-style libraries get the same scaffolder via python run.py new --library <name> from inside their workspace.
This creates libraries/my-sensor/ with:
libraries/my-sensor/
├── VERSION # starts at 0.1.0
├── pyproject.toml # package metadata
├── README.md # package README (fill in TODOs)
├── mkdocs.yml # docs config
├── src/chumicro_my_sensor/
│ ├── __init__.py # public exports (imports MySensor)
│ ├── core.py # starter class with patterns
│ └── testing.py # test fakes (keep or delete)
├── tests/
│ ├── conftest.py # collection anchor (see note below)
│ └── test_my_sensor.py # starter tests (100% coverage)
├── docs/
│ ├── index.md # docs landing page
│ ├── guide.md # user guide (fill in)
│ ├── api.md # API reference (mkdocstrings renders it)
│ └── testing.md # testing helpers docs
├── examples/
│ └── basic_usage.py # working example using MySensor
└── functional_tests/
└── .gitkeep # on-device tests live here
In addition to creating the library directory, the scaffolder runs an editable install (pip install -e libraries/my-sensor) and re-runs sync-ide, which updates the local .idea/chumicro.iml plus the tracked .idea/runConfigurations/, pyrightconfig.json, .vscode/tasks.json, and .vscode/settings.json so your IDE picks up the new package immediately. The .iml is tracked in git (un-ignored via !.idea/chumicro.iml in .gitignore) and regenerated by sync-ide; it and the other config files are the ones you may see in git status after running new-library.
The scaffold is immediately runnable: tests pass at 100% coverage, lint is clean, and the example executes. Start by replacing the starter MySensor class in core.py with your real implementation.
The tests/conftest.py looks empty but is load-bearing: the workspace runs one pytest rootdir over every library with --import-mode=importlib, and the file anchors each tests/ directory as its own collection root so same-named test modules in sibling libraries don't collide. Don't delete it.
2. Implement¶
Put your code in src/chumicro_my_sensor/. Follow the Style Guide for naming, annotations, docstrings, and formatting. Key rules for library code:
- No
async/await. Use the tick-based runner pattern. If your library has active components, implementcheck(now_ms) -> boolso they work withRunner. - No third-party dependencies that aren't available on all three runtimes.
- No
typingimports. Use PEP 604/585 syntax:int | None,list[int]. - Memory patterns are optional on day one:
const(),memoryview, pre-allocated buffers. Focus on correctness first. The Style Guide has the full list when you're ready.
Constructor injection¶
Accept dependencies (time sources, I/O objects, network sockets) as constructor parameters instead of importing hardware modules at the top level. This makes your code testable without real hardware:
# ✅ Good: testable, injectable
class MySensor:
"""Reads from a sensor on a schedule."""
def __init__(self, i2c: object, interval_ms: int = 1000) -> None:
self._i2c = i2c
self._interval_ms = interval_ms
# ❌ Bad: hard-wired to hardware, can't test without a board
import board
import busio
class MySensor:
def __init__(self) -> None:
self._i2c = busio.I2C(board.SCL, board.SDA)
See Decision 0010 for the reasoning.
Public API¶
Export your public API from __init__.py:
"""ChuMicro my-sensor: one-line description."""
from chumicro_my_sensor.core import MyClass, helper_function
__all__ = ["MyClass", "helper_function"]
3. Write tests¶
Tests go in libraries/my-sensor/tests/. Every library must independently meet the coverage threshold configured in pyproject.toml.
pytest libraries/my-sensor/tests/ # everyday iteration
pytest libraries/my-sensor/tests/test_core.py -x -v # one file, stop on first failure
python scripts/run.py test --libraries my-sensor # gated run with coverage enforcement
What a test looks like¶
Since you accepted dependencies as constructor parameters, testing is straightforward. Pass in a fake:
"""Tests for MySensor reading behavior."""
from chumicro_my_sensor import MySensor
class FakeI2C:
"""Fake I2C bus that returns predetermined data."""
def __init__(self, data: list) -> None:
self._data = data
self.read_count = 0
def readfrom_into(self, address: int, buffer: bytearray) -> None:
"""Fill buffer with the next predetermined response."""
buffer[:] = self._data[self.read_count]
self.read_count += 1
def test_sensor_reads_from_bus() -> None:
"""Sensor returns data from the I2C bus."""
fake_i2c = FakeI2C(data=[b"\x01\x02"])
sensor = MySensor(fake_i2c, interval_ms=100)
result = sensor.read()
assert result == b"\x01\x02"
Create lightweight fakes for your own interfaces. Use fakes from upstream ChuMicro libraries when available (from chumicro_timing.testing import FakeTicks). unittest.mock is fine when a purpose-built fake doesn't exist or doesn't make sense, but with dependency injection, you'll usually find that a simple fake gives better test control than patching.
Testing submodule¶
If downstream libraries or users would benefit from test fakes, keep src/chumicro_my_sensor/testing.py and implement real fakes. If there's nothing worth faking, delete it and its references:
- Delete
src/chumicro_my_sensor/testing.py - Delete
docs/testing.md - Remove
- Testing Helpers: testing.mdfrommkdocs.yml - Remove the Testing Helpers link from
docs/index.md - Remove the Testing Helpers link from
README.md
Functional tests on real boards¶
Every new library scaffold also includes functional_tests/ for behavior that needs a real board. You do not need to fill this directory immediately, but if the library has timing-, transport-, GPIO-, or storage-sensitive behavior, plan to add real-board tests here once the host/unit tests are stable.
Run them with:
See Device Testing for devices.yml, deploy modes, and IDE play-button behavior.
4. Write docs¶
User guide (docs/guide.md)¶
Replace the placeholder with a real guide covering:
- Overview: what the library does and when to use it
- Installation: circup, mip, and pip commands
- Quick start: minimal working example
- API walkthrough: main classes and functions with examples
- Cross-runtime notes: any behavior differences across runtimes
API reference (docs/api.md)¶
mkdocstrings renders API docs from your docstrings. The scaffold starts with a single directive. Add section headings and per-module directives as you add modules. See libraries/timing/docs/api.md for an example.
Every public function, method, and class needs a docstring. Types go on the signature as annotations; docstrings carry descriptions only:
def ticks_diff(end: int, start: int) -> int:
"""Signed difference between two tick values.
Args:
end: Later tick value.
start: Earlier tick value.
Returns:
Signed difference in milliseconds.
"""
See the Style Guide for the full format.
5. Write examples¶
Put examples in libraries/my-sensor/examples/. Rules:
- Top-level code: no
if __name__ == "__main__":guard - Descriptive filenames:
sensor_basic_reading.py, notexample1.py - Module docstring with an
Example output::block - Self-contained: copy-paste and run
- Hardware examples: prefix with
circuitpython_ormicropython_
6. Fill in metadata¶
Update libraries/my-sensor/pyproject.toml:
description: one-line package descriptiondependencies: if your library depends on other ChuMicro libraries (e.g.,"chumicro-timing>=0.1")
Update libraries/my-sensor/README.md and replace all TODO placeholders.
The README template now includes a short development/testing section. Keep it accurate for the library you are adding so contributors can see the host-test and device-test entry points without hunting through the repository docs.
7. Preflight and PR¶
Must print Preflight passed. Then push and open a PR on GitHub targeting main. See Creating a Pull Request for the full walkthrough.
8. After merge¶
When your PR merges, the VERSION bump triggers an automatic experimental release: your package publishes to PyPI as chumicro-my-sensor-experimental, lands in the experimental bundle repo, and gets experimental docs and a git tag, so users can install it right away. When you're confident it's production-ready, open a Stable Promotion Request and a maintainer promotes it. Releases and Promotion covers the full flow, including the exact install commands and how promotion works.
Checklist¶
-
python scripts/run.py new-library <name>: scaffold created - Implementation in
src/chumicro_<name>/ - Public exports in
__init__.pywith__all__ - Tests in
tests/: coverage gate passing - Testing submodule: kept and implemented, or deleted with all references
- Examples in
examples/:verify-examplespasses -
docs/guide.md: real content, no placeholders -
README.md: description and API summary filled in -
pyproject.toml: description and dependencies set - Preflight passes
- PR opened and CI green