Contributing to the CircuitPython Community Bundle
As I mentioned in my last post, I've been building a new PCB around the AT42QT2120 touch sensor. To make use of the board, I wanted to write a CircuitPython driver for it. Continuing on my open source adventures, I decided to publish the library for others to use.
I linked to Adafruit's MP121 breakout last time, and it since it offers very similar functionality to the AT42QT2120, I decided to use their library as the basis for my own. I modelled my library's interface in such a way that you should be able to swap out the library and the sensor without making any other changes to your project.
-import adafruit_mpr121
+import at42qt2120
i2c = busio.I2C(board.SCL, board.SDA)
-mpr121 = adafruit_mpr121.MPR121(i2c)
+at42qt = at42qt2120.AT42QT2120(i2c)
while True:
for i in range(12):
- if mpr121[i].value:
+ if at42qt[i].value:
print(f"Input {i} touched!")
time.sleep(0.25)
In the past I've used pip
and PyPI to distribute my code, but they run within your shell on "real" computers for "proper" Python projects, not on microcontrollers running CircuitPython.
I needed to find a way to distribute my code to the CircuitPython community.
Luckily, Adafruit has a tutorial (which could probably do with updating) on how to do this.
The excellent circup
tool makes it easy to get libraries on to your device and start using them.
$ circup bundle-show
#...
adafruit/Adafruit_CircuitPython_Bundle
https://github.com/adafruit/Adafruit_CircuitPython_Bundle
version = 20250925
adafruit/CircuitPython_Community_Bundle
https://github.com/adafruit/CircuitPython_Community_Bundle
version = 20250919
$ circup install adafruit_dht
#...
Searching for dependencies for: ['adafruit_dht']
Ready to install: ['adafruit_dht']
Installed 'adafruit_dht'.
Out-of-the-box, it comes with the Adafruit and Community bundles, so it appears that the CircuitPython Community Bundle is the best way to get my library into the hands and onto the boards of other makers.
Paul Cutler publishes The CircuitPython Show podcast, and they had a great episode last season with Jan Goolsbey and Tod Kurt where they shared their experience in writing libraries and drivers for the CircuitPython Community Bundle. After listening to their advice and playing with a few throwaway test projects, there seemed to be two ways to approach this:
- start from the ground up, writing your library code first, then building the supporting project files around it
- use the CircuitPython Cookiecutter template to generate the project files, then slot your library code in to place
I'm always wary of using templates for this sort of thing. I like to get a ground-up understanding of what's going on behind the scenes, so I picked the code-first, project-later approach. By the time I'd finished, however, I'd pretty much just rebuilt the entire structure of the template by hand, with no significant changes. I'd recommend anyone following along should pick the Cookiecutter route and make changes to that instead.
Changes
With that in mind, here's a run-down of where I deviated from the template, but in a way that doesn't seem to have negatively affected anything.
Change ReStructuredText to Markdown
Look, I get it. ReStructured text is the "Pythonic" way to write documentation, but it's just not my thing. I've been using Markdown for years, and so I've got that muscle memory burned in.
Change Sphinx to MkDocs
Again, sorry. I get that the Python world has always used Sphinx, but I've not since I've always been in the Markdown world. Luckily, ReadTheDocs supports MkDocs as well, so switching to that was pretty easy.
Add unit tests
In the Expectations for Library Inclusion section of the Community Bundle README, it says:
- If your library has a problematic release that impacts Community Bundle releases, you will do your best to resolve it in a timely manner
- If future changes to circuitpython-build-tools impact your library, you will do your best to resolve it in a timely manner
The easiest way to meet these expectations is to build a decent suite of tests.
Python's built-in unittest
module is fine, but the pytest
framework brings a nice sprinkling of syntactic sugar with it.
It will, however, pick up any example files that end with *test.py
, but adding norecursedirs = examples
to the pytest.ini
file will nip that in the bud.
Separate hardware interactions and business logic
Writing tests which rely on hardware, or mocked hardware, is a pain. If you can separate the code that performs your hardware interactions from the code that does your "business logic", then you can write tests which don't require any hardware.
In my library, for example, I need to read a 16-bit integer from the chip. This is achieved by performing two 8-bit reads over the I2C bus and then combining the results in memory. I extracted the code to combine those two bytes into a separate function, which allowed me to write fairly extensive unit tests. I can even prove how the function works when provided with deliberately incorrect data.
Add doc tests
Doc tests are a great feature of Python.
It's well known that code and comments can easily get out of sync.
Doc tests are small example snippets which live in your method's docstrings but which are run as part of the test suite.
If you modify the method but forget to update the comment, then the test will fail.
Popping addopts = --doctest-modules
into your pytest.ini
file will make sure these tests are run as part of the rest of the test suite.
They're also a great way to ensure that users of your library have a clear, tested example of how to use it.
Update pre-commit actions to the latest version
The supplied cookiecutter pre-commit file was last updated a little over a year ago. It's not very much work to visit each of the pre-commit actions' repos and work out the latest version numbers. There's a little tweaking to the individual actions' configurations to get them to work with the latest versions, but it's not too bad.
Update all the copyright statements
This one's straightforward. Make sure to pop your name at the top of any file you meaningfully change. Our pre-commit tools check all of our files for SPDX headers, so make sure your name's in there. You did the hard work writing the library, make sure you get credit for it!
PyPI action
I'm unclear whether the PyPI action is needed in all circumstances or just when you're using your library alongside Adafruit's blinka. Regardless, publishing Python packages to PyPI is a sensible thing to do, and the included action will publish your library to PyPI each time to push a new release. Out of the box, however, it will fail.
The "PyPI Release Actions" workflow will fail at the "upload-release-assets" step with an "HTTPError: 403 Forbidden" error and an "Invalid or non-existent authentication information" message.
The workflow needs two secrets to be set up in your GitHub repo, pypi_username
and pypi_password
.
Despite these variables' names, DO NOT ENTER YOUR PYPI USERNAME AND PYPI PASSWORD HERE.
Create a new PyPI API token that can publish to your package and use that instead.
According to the PyPI docs, here's how you make use of an API token:
To use an API token:
- Set your username to
__token__
- Set your password to the token value, including the
pypi-
prefix
GitHub action
As far as I can tell, the Community Bundle collation process pulls in your built library from its GitHub Releases artefacts. The supplied "GitHub Release Actions" workflow will automatically build and archive your assets whenever you push a new release. Out of the box, however, it will also fail.
The "upload-release-assets" step fails with a 403 error code and a "Resource not accessible by integration" message.
After a bit of digging, it looks like the GITHUB_TOKEN
secret used in the release_gh.yml
workflow doesn't have the right permissions to upload assets on your behalf.
You need to go in to the GitHub page for your repo, and within the Settings tab navigate to the Actions, General page. Select "Read and write permissions" from the "Workflow permissions" section of this page.
Success!
After completing the above steps, built, tested, and packaged copies of my library were being automatically pushed to GitHub and PyPI, and after a small pull-request to the Community Bundle repo, my library is available to the CircuitPython community.
Whether you've got one of my Touchy Subject boards, or are working with your own design incorporating an AT42QT2120, CircuitPython driver support is now just a circup install
away.
$ circup install at42qt2120
#...
Searching for dependencies for: ['at42qt2120']
Ready to install: ['adafruit_bus_device', 'at42qt2120']
Installed 'adafruit_bus_device'.
Installed 'at42qt2120'.
2025-10-02