Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ build/
pysquared.egg-info/
**/*.mpy
site/
typeshed/
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ repos:
entry: '# type:? *ignore'
language: pygrep
types: [python]
files: ^pysquared/
exclude: ^pysquared/beacon\.py|^pysquared/logger\.py|^pysquared/rtc/manager/microcontroller\.py

- repo: local
hooks:
- id: prevent-pyright-ignore
name: prevent pyright ignore annotations
description: 'Enforce that no `# type: ignore` annotations exist in the codebase.'
entry: '# pyright:? *.*false'
language: pygrep
types: [python]
files: ^pysquared/

- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
Expand Down
12 changes: 9 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: all
all: .venv pre-commit-install
all: .venv typeshed pre-commit-install

.PHONY: help
help: ## Display this help.
Expand All @@ -13,6 +13,11 @@ help: ## Display this help.
@$(UV) venv
@$(UV) pip install --requirement pyproject.toml

typeshed: ## Install CircuitPython typeshed stubs
@echo "Installing CircuitPython typeshed stubs..."
@$(MAKE) uv
@$(UV) pip install circuitpython-typeshed==0.1.0 --target typeshed

.PHONY: pre-commit-install
pre-commit-install: uv
@echo "Installing pre-commit hooks..."
Expand All @@ -22,7 +27,8 @@ pre-commit-install: uv
fmt: pre-commit-install ## Lint and format files
$(UVX) pre-commit run --all-files

typecheck: .venv ## Run type check
.PHONY: typecheck
typecheck: .venv typeshed ## Run type check
@$(UV) run -m pyright .

.PHONY: test
Expand Down Expand Up @@ -68,7 +74,7 @@ $(TOOLS_DIR):
mkdir -p $(TOOLS_DIR)

### Tool Versions
UV_VERSION ?= 0.7.13
UV_VERSION ?= 0.8.14
MPY_CROSS_VERSION ?= 9.0.5

UV_DIR ?= $(TOOLS_DIR)/uv-$(UV_VERSION)
Expand Down
14 changes: 13 additions & 1 deletion docs/design-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,19 @@ PySquared is built on top of CircuitPython, which is a version of Python designe

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.

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.
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.

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.

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:

- **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:

```python
some_variable = some_function() # type: ignore # PR https://github.com/adafruit/circuitpython/pull/10603
```

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.

??? note "Using the Typing Module"
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:
Expand Down
4 changes: 1 addition & 3 deletions mocks/circuitpython/busio.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
the need for actual hardware.
"""

from __future__ import annotations

from typing import Optional

import mocks.circuitpython.microcontroller as microcontroller
import microcontroller


class SPI:
Expand Down
4 changes: 1 addition & 3 deletions mocks/circuitpython/digitalio.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
the need for actual hardware.
"""

from __future__ import annotations

import mocks.circuitpython.microcontroller as microcontroller
import microcontroller


class DriveMode:
Expand Down
12 changes: 9 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dev = [
"coverage==7.9.1",
"freezegun>=1.5.2",
"pre-commit==4.2.0",
"pyright[nodejs]==1.1.402",
"pyright[nodejs]==1.1.404",
"pytest==8.4.1",
"hypothesis==6.136.7",
]
Expand Down Expand Up @@ -92,15 +92,18 @@ directory = ".coverage-reports/html"
[tool.coverage.xml]
output = ".coverage-reports/coverage.xml"

[tool.ty.environment]
typeshed = "./typeshed"

[tool.pyright]
include = ["pysquared"]
exclude = [
"**/__pycache__",
".venv",
".git",
"typings",
"typeshed",
]
stubPath = "./typings"
typeshedPath = "./typeshed"
reportMissingModuleSource = false

[tool.interrogate]
Expand All @@ -109,3 +112,6 @@ omit-covered-files = true
fail-under = 100
verbose = 2
color = true
exclude = [
"typeshed",
]
5 changes: 2 additions & 3 deletions pysquared/beacon.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,9 @@ def _add_system_info(self, state: OrderedDict[str, object]) -> None:
"""
state["name"] = self._name

# Warning: CircuitPython does not support time.gmtime(), when testing this code it will use your local timezone
now = time.localtime()
now = time.localtime() # type: ignore # PR: https://github.com/adafruit/circuitpython/pull/10603
state["time"] = (
f"{now.tm_year}-{now.tm_mon:02d}-{now.tm_mday:02d} {now.tm_hour:02d}:{now.tm_min:02d}:{now.tm_sec:02d}"
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
)

state["uptime"] = time.time() - self._boot_time
Expand Down
4 changes: 2 additions & 2 deletions pysquared/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ def _log(self, level: str, level_value: int, message: str, **kwargs) -> None:
message (str): The log message.
**kwargs: Additional key/value pairs to include in the log.
"""
now = time.localtime()
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}"
now = time.localtime() # type: ignore # PR: https://github.com/adafruit/circuitpython/pull/10603
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
Copy link
Member

@Mikefly123 Mikefly123 Aug 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great that upstream will soon get this patch!

I made a new issue ticket on the FC board repo to remind me to update all of our firmware files to get this upstream patch. @nateinaction remind me when a version is cut using that patch if you can.

I do want to note though that although V4x boards should be able to automatically update to the latest CircuitPython version V5 boards will be slow to get there because there is still no official upstream support for low power modes and we are having to use a weird branch / fork / open PR to get them with the current firmware.

I don't think this will be a blocker for this PR going in, but we will have to circle back to thinking about doing one of a few options regarding low power sleep in a separate context:

  1. Remain on the bablokb fork and not get additional circuitpython updates for V5x boards for the time being
  2. Try to update the fork to be in sync with main while preserving the low power mode code
  3. Remove the use of alarm from the sleep_helper and stop using low power sleeps until it is officially merged upstream
  4. Task someone on the team to sort things out and get something merged upstream ourselves

To be honest I am kind of in the mindset to implement Option 3 for the codebase for the time being. It will hit our power consumption, but the latest numbers seem to indicate we shouldn't be in trouble (assuming the solar panels are working as intended) and removing the use of deep_sleep and light_sleep will greatly simplify the end user experience with our reference code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those all sound like good options. 3 sounds like what we might do if we're trying to reduce commitment to pysquared while still shipping a release soon and ramping up on fprime. Good option.

Copy link
Member Author

@nateinaction nateinaction Aug 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes don't technically require a the board firmware to be updated. It would be good to keep pysquared at the same release as the board firmware but what we really need is the new python stubs that get shipped with each release. We can update those independently but the version skew might not be a good development experience.


# case where someone used debug, info, or warning yet also provides an 'err' kwarg with an Exception
if (
Expand Down
2 changes: 1 addition & 1 deletion pysquared/rtc/manager/microcontroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def __init__(self) -> None:
This method is required on every boot to ensure the RTC is ready for use.
"""
microcontroller_rtc = rtc.RTC()
microcontroller_rtc.datetime = time.localtime()
microcontroller_rtc.datetime = time.localtime() # type: ignore # PR: https://github.com/adafruit/circuitpython/pull/10603

def set_time(
self,
Expand Down
39 changes: 20 additions & 19 deletions tests/unit/hardware/imu/manager/test_lsm6dsox_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
and error handling for acceleration, angular_velocity, and temperature readings.
"""

import math
# pyright: reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportReturnType=false

from typing import Generator
from unittest.mock import MagicMock, PropertyMock, patch

Expand Down Expand Up @@ -37,7 +38,7 @@ def mock_logger() -> MagicMock:


@pytest.fixture
def mock_lsm6dsox(mock_i2c: MagicMock) -> Generator[MagicMock, None, None]:
def mock_lsm6dsox(mock_i2c: I2C) -> Generator[MagicMock, None, None]:
"""Mocks the LSM6DSOX class.

Args:
Expand All @@ -53,8 +54,8 @@ def mock_lsm6dsox(mock_i2c: MagicMock) -> Generator[MagicMock, None, None]:

def test_create_imu(
mock_lsm6dsox: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger,
) -> None:
"""Tests successful creation of an LSM6DSOX IMU instance.

Expand All @@ -71,8 +72,8 @@ def test_create_imu(

def test_create_imu_failed(
mock_lsm6dsox: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger,
) -> None:
"""Tests that initialization is retried when it fails.

Expand All @@ -92,8 +93,8 @@ def test_create_imu_failed(

def test_get_acceleration_success(
mock_lsm6dsox: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger: Logger,
) -> None:
"""Tests successful retrieval of the acceleration vector.

Expand All @@ -117,8 +118,8 @@ def test_get_acceleration_success(

def test_get_acceleration_failure(
mock_lsm6dsox: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger,
) -> None:
"""Tests handling of exceptions when retrieving the acceleration vector.

Expand All @@ -143,8 +144,8 @@ def test_get_acceleration_failure(

def test_get_angular_velocity_success(
mock_lsm6dsox: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger: Logger,
) -> None:
"""Tests successful retrieval of the angular_velocity vector.

Expand All @@ -167,8 +168,8 @@ def test_get_angular_velocity_success(

def test_get_angular_velocity_failure(
mock_lsm6dsox: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger,
) -> None:
"""Tests handling of exceptions when retrieving the angular_velocity vector.

Expand All @@ -192,8 +193,8 @@ def test_get_angular_velocity_failure(

def test_get_temperature_success(
mock_lsm6dsox: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger: Logger,
) -> None:
"""Tests successful retrieval of the temperature.

Expand All @@ -209,13 +210,13 @@ def test_get_temperature_success(

temp = imu_manager.get_temperature()
assert isinstance(temp, Temperature)
assert math.isclose(temp.value, expected_temp, rel_tol=1e-9)
assert pytest.approx(expected_temp, rel=1e-9) == temp.value


def test_get_temperature_failure(
mock_lsm6dsox: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger,
) -> None:
"""Tests handling of exceptions when retrieving the temperature.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Test the VEML7700Manager class."""

# pyright: reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportReturnType=false

from typing import Generator
from unittest.mock import MagicMock, PropertyMock, patch

Expand Down
19 changes: 11 additions & 8 deletions tests/unit/hardware/magnetometer/manager/test_lis2mdl_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
retrieval, and error handling for magnetic field vector readings.
"""

# pyright: reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportReturnType=false

from typing import Generator
from unittest.mock import MagicMock, patch

import pytest
from busio import I2C

from mocks.adafruit_lis2mdl.lis2mdl import LIS2MDL
from pysquared.hardware.exception import HardwareInitializationError
Expand Down Expand Up @@ -48,8 +51,8 @@ def mock_lis2mdl(mock_i2c: MagicMock) -> Generator[MagicMock, None, None]:

def test_create_magnetometer(
mock_lis2mdl: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger,
) -> None:
"""Tests successful creation of a LIS2MDL magnetometer instance.

Expand All @@ -66,8 +69,8 @@ def test_create_magnetometer(

def test_create_magnetometer_failed(
mock_lis2mdl: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger,
) -> None:
"""Tests that initialization is retried when it fails.

Expand All @@ -91,8 +94,8 @@ def test_create_magnetometer_failed(

def test_get_magnetic_field_success(
mock_lis2mdl: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger,
) -> None:
"""Tests successful retrieval of the magnetic field vector.

Expand Down Expand Up @@ -122,8 +125,8 @@ def mock_magnetic():

def test_get_magnetic_field_unknown_error(
mock_lis2mdl: MagicMock,
mock_i2c: MagicMock,
mock_logger: MagicMock,
mock_i2c: I2C,
mock_logger,
) -> None:
"""Tests handling of unknown errors when retrieving the magnetic field vector.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
retrieval, and error handling for bus voltage, shunt voltage, and current readings.
"""

# pyright: reportAttributeAccessIssue=false, reportOptionalMemberAccess=false, reportReturnType=false

from typing import Generator
from unittest.mock import MagicMock, PropertyMock, patch

Expand Down
Loading
Loading