Skip to content

Commit 2ca1aaf

Browse files
authored
Use typeshed stubs for more complete static analysis of CircuitPython code (#304)
1 parent b884500 commit 2ca1aaf

31 files changed

+366
-664
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ build/
1313
pysquared.egg-info/
1414
**/*.mpy
1515
site/
16+
typeshed/

.pre-commit-config.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ repos:
2121
entry: '# type:? *ignore'
2222
language: pygrep
2323
types: [python]
24+
files: ^pysquared/
25+
exclude: ^pysquared/beacon\.py|^pysquared/logger\.py|^pysquared/rtc/manager/microcontroller\.py
26+
27+
- repo: local
28+
hooks:
29+
- id: prevent-pyright-ignore
30+
name: prevent pyright ignore annotations
31+
description: 'Enforce that no `# type: ignore` annotations exist in the codebase.'
32+
entry: '# pyright:? *.*false'
33+
language: pygrep
34+
types: [python]
35+
files: ^pysquared/
2436

2537
- repo: https://github.com/codespell-project/codespell
2638
rev: v2.4.1

Makefile

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.PHONY: all
2-
all: .venv pre-commit-install
2+
all: .venv typeshed pre-commit-install
33

44
.PHONY: help
55
help: ## Display this help.
@@ -13,6 +13,11 @@ help: ## Display this help.
1313
@$(UV) venv
1414
@$(UV) pip install --requirement pyproject.toml
1515

16+
typeshed: ## Install CircuitPython typeshed stubs
17+
@echo "Installing CircuitPython typeshed stubs..."
18+
@$(MAKE) uv
19+
@$(UV) pip install circuitpython-typeshed==0.1.0 --target typeshed
20+
1621
.PHONY: pre-commit-install
1722
pre-commit-install: uv
1823
@echo "Installing pre-commit hooks..."
@@ -22,7 +27,8 @@ pre-commit-install: uv
2227
fmt: pre-commit-install ## Lint and format files
2328
$(UVX) pre-commit run --all-files
2429

25-
typecheck: .venv ## Run type check
30+
.PHONY: typecheck
31+
typecheck: .venv typeshed ## Run type check
2632
@$(UV) run -m pyright .
2733

2834
.PHONY: test
@@ -68,7 +74,7 @@ $(TOOLS_DIR):
6874
mkdir -p $(TOOLS_DIR)
6975

7076
### Tool Versions
71-
UV_VERSION ?= 0.7.13
77+
UV_VERSION ?= 0.8.14
7278
MPY_CROSS_VERSION ?= 9.0.5
7379

7480
UV_DIR ?= $(TOOLS_DIR)/uv-$(UV_VERSION)

docs/design-guide.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,19 @@ PySquared is built on top of CircuitPython, which is a version of Python designe
1919

2020
We use type hints throughout the PySquared codebase to ensure that our code is clear and maintainable. Type hints help us catch errors early and make it easier to understand the expected types of variables and function parameters.
2121

22-
We do not accept changes with lines that are ignored the type checker i.e. `# type: ignore`. If you run into an issue where you think you need to ignore a type, it is likely a problem with the design of your component. Please take a moment to think about how you can fix the type error instead. If you need help, please reach out for assistance.
22+
We use [typeshed](https://peps.python.org/pep-0561/) stubs to provide more accurate type hints for CircuitPython, replacing the default Python standard library type hints. These CircuitPython-specific stubs are located in the `typeshed/` directory. This helps the typechecker catch compatibility issues with CircuitPython code before running it on a device.
23+
24+
However, using these stubs means that type hints in test files also reference CircuitPython types, not the standard Python types available in the test environment. As a workaround, we add pyright ignore comments (e.g., `# pyright: reportOptionalMemberAccess=false`) when necessary at the top of test files to suppress related errors. This workaround isn’t ideal. If you have suggestions for handling this issue more effectively, please share your feedback.
25+
26+
We do not accept changes to files in the `pysquared/` directory that include lines ignoring the type checker (e.g., `# type: ignore`). The only exceptions are:
27+
28+
- **Upstream Fix in Progress:** If a type error is caused by a bug or limitation in an external dependency, you may ignore the line only when leaving a comment with a link to the issue or PR where it is fixed or a fix is in progress. A valid type hint might look like this:
29+
30+
```python
31+
some_variable = some_function() # type: ignore # PR https://github.com/adafruit/circuitpython/pull/10603
32+
```
33+
34+
If you encounter a type error, first consider if it can be resolved by improving your code's design. If you believe an exception is necessary, please reach out for assistance before proceeding.
2335

2436
??? note "Using the Typing Module"
2537
For more advanced type hinting we can use the Python standard library's `typing` module which was introduced in Python 3.5. This module provides a variety of type hints that can be used to specify more complex types, such as `List`, `Dict`, and `Optional`. CircuitPython does not support the `typing` module so we must wrap the import in a try/except block to avoid import errors. For example:

mocks/circuitpython/busio.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
the need for actual hardware.
66
"""
77

8-
from __future__ import annotations
9-
108
from typing import Optional
119

12-
import mocks.circuitpython.microcontroller as microcontroller
10+
import microcontroller
1311

1412

1513
class SPI:

mocks/circuitpython/digitalio.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
the need for actual hardware.
66
"""
77

8-
from __future__ import annotations
9-
10-
import mocks.circuitpython.microcontroller as microcontroller
8+
import microcontroller
119

1210

1311
class DriveMode:

pyproject.toml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ dev = [
2929
"coverage==7.9.1",
3030
"freezegun>=1.5.2",
3131
"pre-commit==4.2.0",
32-
"pyright[nodejs]==1.1.402",
32+
"pyright[nodejs]==1.1.404",
3333
"pytest==8.4.1",
3434
"hypothesis==6.136.7",
3535
]
@@ -92,15 +92,18 @@ directory = ".coverage-reports/html"
9292
[tool.coverage.xml]
9393
output = ".coverage-reports/coverage.xml"
9494

95+
[tool.ty.environment]
96+
typeshed = "./typeshed"
97+
9598
[tool.pyright]
9699
include = ["pysquared"]
97100
exclude = [
98101
"**/__pycache__",
99102
".venv",
100103
".git",
101-
"typings",
104+
"typeshed",
102105
]
103-
stubPath = "./typings"
106+
typeshedPath = "./typeshed"
104107
reportMissingModuleSource = false
105108

106109
[tool.interrogate]
@@ -109,3 +112,6 @@ omit-covered-files = true
109112
fail-under = 100
110113
verbose = 2
111114
color = true
115+
exclude = [
116+
"typeshed",
117+
]

pysquared/beacon.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,9 @@ def _add_system_info(self, state: OrderedDict[str, object]) -> None:
109109
"""
110110
state["name"] = self._name
111111

112-
# Warning: CircuitPython does not support time.gmtime(), when testing this code it will use your local timezone
113-
now = time.localtime()
112+
now = time.localtime() # type: ignore # PR: https://github.com/adafruit/circuitpython/pull/10603
114113
state["time"] = (
115-
f"{now.tm_year}-{now.tm_mon:02d}-{now.tm_mday:02d} {now.tm_hour:02d}:{now.tm_min:02d}:{now.tm_sec:02d}"
114+
f"{now.tm_year}-{now.tm_mon:02d}-{now.tm_mday:02d} {now.tm_hour:02d}:{now.tm_min:02d}:{now.tm_sec:02d}" # type: ignore # PR: https://github.com/adafruit/circuitpython/pull/10603
116115
)
117116

118117
state["uptime"] = time.time() - self._boot_time

pysquared/logger.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ def _log(self, level: str, level_value: int, message: str, **kwargs) -> None:
129129
message (str): The log message.
130130
**kwargs: Additional key/value pairs to include in the log.
131131
"""
132-
now = time.localtime()
133-
asctime = f"{now.tm_year}-{now.tm_mon:02d}-{now.tm_mday:02d} {now.tm_hour:02d}:{now.tm_min:02d}:{now.tm_sec:02d}"
132+
now = time.localtime() # type: ignore # PR: https://github.com/adafruit/circuitpython/pull/10603
133+
asctime = f"{now.tm_year}-{now.tm_mon:02d}-{now.tm_mday:02d} {now.tm_hour:02d}:{now.tm_min:02d}:{now.tm_sec:02d}" # type: ignore # PR: https://github.com/adafruit/circuitpython/pull/10603
134134

135135
# case where someone used debug, info, or warning yet also provides an 'err' kwarg with an Exception
136136
if (

pysquared/rtc/manager/microcontroller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __init__(self) -> None:
3030
This method is required on every boot to ensure the RTC is ready for use.
3131
"""
3232
microcontroller_rtc = rtc.RTC()
33-
microcontroller_rtc.datetime = time.localtime()
33+
microcontroller_rtc.datetime = time.localtime() # type: ignore # PR: https://github.com/adafruit/circuitpython/pull/10603
3434

3535
def set_time(
3636
self,

0 commit comments

Comments
 (0)