Skip to content

Commit

Permalink
improve board mock and implement busio singletons (#7)
Browse files Browse the repository at this point in the history
* add `Ax` and secondary I2C pins to mock board module
* add STEMMA_I2C and implement default buses as singletons
* add `board.board_id` string (set to lib-specific ID)
* review docs
  • Loading branch information
2bndy5 authored Aug 6, 2024
1 parent 93b6b08 commit 0e37ada
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 25 deletions.
72 changes: 70 additions & 2 deletions circuitpython_mocks/board.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""A module that hosts mock pins."""
"""A module that hosts mock pins and default :py:class:`~busio.SPI`,
:py:class:`~busio.I2C`, and :py:class:`~busio.UART` data buses."""

#: A dummy identifier to allow detection when using this mock library.
board_id = "CIRCUITPYTHON_MOCK"


class Pin:
Expand All @@ -7,6 +11,33 @@ class Pin:
pass


A0 = Pin()
A1 = Pin()
A2 = Pin()
A3 = Pin()
A4 = Pin()
A5 = Pin()
A6 = Pin()
A7 = Pin()
A8 = Pin()
A9 = Pin()
A10 = Pin()
A11 = Pin()
A12 = Pin()
A13 = Pin()
A14 = Pin()
A15 = Pin()
A16 = Pin()
A17 = Pin()
A18 = Pin()
A19 = Pin()
A20 = Pin()
A21 = Pin()
A22 = Pin()
A23 = Pin()
A24 = Pin()
A25 = Pin()

D0 = Pin()
D1 = Pin()
D2 = Pin()
Expand Down Expand Up @@ -110,6 +141,8 @@ class Pin:

SDA = Pin()
SCL = Pin()
SDA1 = Pin()
SCL1 = Pin()

CE1 = Pin()
CE0 = Pin()
Expand All @@ -121,11 +154,46 @@ class Pin:
TXD = Pin()
RXD = Pin()

# create alias for most of the examples
TX = Pin()
RX = Pin()

MISO_1 = Pin()
MOSI_1 = Pin()
SCLK_1 = Pin()
SCK_1 = Pin()
CS = Pin()

WS = Pin()
SD = Pin()

LED = Pin()
NEOPIXEL = Pin()
DOTSTAR = Pin()


def SPI():
"""Creates a default instance (singleton) of :py:class:`~busio.SPI`"""
from circuitpython_mocks.busio import SPI as ImplSPI

return ImplSPI(SCK, MOSI, MISO)


def I2C():
"""Creates a default instance (singleton) of :py:class:`~busio.I2C`"""
from circuitpython_mocks.busio import I2C as ImplI2C

return ImplI2C(SCL, SDA)


def STEMMA_I2C():
"""Creates a default instance (singleton) of :py:class:`~busio.I2C`"""
from circuitpython_mocks.busio import I2C as ImplI2C

return ImplI2C(SCL1, SDA1)


def UART():
"""Creates a default instance (singleton) of :py:class:`~busio.UART`"""
from circuitpython_mocks.busio import UART as ImplUART

return ImplUART(TX, RX)
64 changes: 61 additions & 3 deletions circuitpython_mocks/busio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from enum import Enum, auto
import sys
from typing import List
from typing import List, Optional

import circuitpython_typing

Expand All @@ -17,11 +17,39 @@
SPITransfer,
)
from circuitpython_mocks._mixins import Expecting, Lockable
from circuitpython_mocks.board import Pin
from circuitpython_mocks.board import (
Pin,
SDA,
SDA1,
SCL,
SCL1,
SCK,
MOSI as PinMOSI,
MISO as PinMISO,
TX,
RX,
MISO_1,
MOSI_1,
SCK_1,
)


class I2C(Expecting, Lockable):
"""A mock of `busio.I2C` class."""
"""A mock of :external:py:class:`busio.I2C` class."""

_primary_singleton: Optional["I2C"] = None
_secondary_singleton: Optional["I2C"] = None

def __new__(cls, scl: Pin, sda: Pin, **kwargs) -> "I2C":
if scl == SCL and sda == SDA:
if cls._primary_singleton is None:
cls._primary_singleton = super().__new__(cls)
return cls._primary_singleton
if scl == SCL1 and sda == SDA1:
if cls._secondary_singleton is None:
cls._secondary_singleton = super().__new__(cls)
return cls._secondary_singleton
return super().__new__(cls)

def __init__(
self,
Expand All @@ -31,6 +59,8 @@ def __init__(
frequency: int = 100000,
timeout: int = 255,
):
if hasattr(self, "expectations"):
return
super().__init__()

def scan(self) -> List[int]:
Expand Down Expand Up @@ -99,6 +129,22 @@ def writeto_then_readfrom(


class SPI(Expecting, Lockable):
"""A mock of :external:py:class:`busio.SPI` class."""

_primary_singleton: Optional["SPI"] = None
_secondary_singleton: Optional["SPI"] = None

def __new__(cls, clock: Pin, MOSI: Pin, MISO: Pin, **kwargs) -> "SPI":
if clock == SCK and MOSI == PinMOSI and MISO == PinMISO:
if cls._primary_singleton is None:
cls._primary_singleton = super().__new__(cls)
return cls._primary_singleton
if clock == SCK_1 and MOSI == MOSI_1 and MISO == MISO_1:
if cls._secondary_singleton is None:
cls._secondary_singleton = super().__new__(cls)
return cls._secondary_singleton
return super().__new__(cls)

def __init__(
self,
clock: Pin,
Expand Down Expand Up @@ -183,6 +229,15 @@ def write_readinto(
class UART(Expecting, Lockable):
"""A class that mocks :external:py:class:`busio.UART`."""

_primary_singleton: Optional["UART"] = None

def __new__(cls, tx: Pin, rx: Pin, **kwargs) -> "UART":
if tx == TX and rx == RX:
if cls._primary_singleton is None:
cls._primary_singleton = super().__new__(cls)
return cls._primary_singleton
return super().__new__(cls)

class Parity(Enum):
ODD = auto()
EVEN = auto()
Expand Down Expand Up @@ -250,3 +305,6 @@ def write(self, buf: circuitpython_typing.ReadableBuffer) -> int | None:
len_buf = len(op.expected)
op.assert_expected(buf, 0, len_buf)
return len(buf) or None


_UART = UART(TX, RX)
26 changes: 15 additions & 11 deletions circuitpython_mocks/digitalio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum, auto

from typing import Union, Optional
from circuitpython_mocks._mixins import ContextManaged, Expecting
from circuitpython_mocks.digitalio.operations import GetState, SetState
from circuitpython_mocks.board import Pin
Expand Down Expand Up @@ -34,13 +34,17 @@ def __init__(self, pin: Pin, **kwargs):
self._pin = pin
self.switch_to_input()

def switch_to_output(self, value=False, drive_mode=DriveMode.PUSH_PULL):
def switch_to_output(
self,
value: Union[bool, int] = False,
drive_mode: DriveMode = DriveMode.PUSH_PULL,
):
"""Switch the Digital Pin Mode to Output"""
self.direction = Direction.OUTPUT
self.value = value
self.drive_mode = drive_mode

def switch_to_input(self, pull=None):
def switch_to_input(self, pull: Optional[Pull] = None):
"""Switch the Digital Pin Mode to Input"""
self.direction = Direction.INPUT
self.pull = pull
Expand All @@ -50,12 +54,12 @@ def deinit(self):
del self._pin

@property
def direction(self):
def direction(self) -> Direction:
"""Get or Set the Digital Pin Direction"""
return self.__direction

@direction.setter
def direction(self, value):
def direction(self, value: Direction):
self.__direction = value
if value == Direction.OUTPUT:
# self.value = False
Expand All @@ -66,7 +70,7 @@ def direction(self, value):
raise AttributeError("Not a Direction")

@property
def value(self):
def value(self) -> Union[bool, int]:
"""The Digital Pin Value.
This property will check against `SetState` and `GetState`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`."""
Expand All @@ -78,7 +82,7 @@ def value(self):
return op.state

@value.setter
def value(self, val):
def value(self, val: Union[bool, int]):
if self.direction != Direction.OUTPUT:
raise AttributeError("Not an output")
assert self.expectations, "No expectations found for DigitalInOut.value.setter"
Expand All @@ -89,25 +93,25 @@ def value(self, val):
op.assert_state(val)

@property
def pull(self):
def pull(self) -> Optional[Pull]:
"""The pin pull direction"""
if self.direction == Direction.INPUT:
return self.__pull
raise AttributeError("Not an input")

@pull.setter
def pull(self, pul):
def pull(self, pul: Optional[Pull]):
if self.direction != Direction.INPUT:
raise AttributeError("Not an input")
self.__pull = pul

@property
def drive_mode(self):
def drive_mode(self) -> DriveMode:
"""The Digital Pin Drive Mode"""
if self.direction != Direction.OUTPUT:
raise AttributeError("Not an output")
return self.__drive_mode

@drive_mode.setter
def drive_mode(self, mod):
def drive_mode(self, mod: DriveMode):
self.__drive_mode = mod
1 change: 0 additions & 1 deletion docs/board.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

.. automodule:: circuitpython_mocks.board
:members:
:undoc-members:

This module includes the following dummy pins for soft-testing:

Expand Down
17 changes: 16 additions & 1 deletion docs/busio.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@
=================

.. automodule:: circuitpython_mocks.busio
:members:

.. autoclass:: circuitpython_mocks.busio.I2C
:members: readfrom_into, writeto, writeto_then_readfrom, scan
.. autoclass:: circuitpython_mocks.busio.SPI
:members: readinto, write, write_readinto, configure, frequency
.. autoclass:: circuitpython_mocks.busio.UART
:members: readinto, readline, write

.. py:class:: circuitpython_mocks.busio.UART.Parity
A mock enumeration of :external:py:class:`busio.Parity`.

.. py:attribute:: ODD
:type: Parity
.. py:attribute:: EVEN
:type: Parity

``busio.operations``
--------------------
Expand Down
10 changes: 8 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@
"sphinx_immaterial",
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinx_jinja",
]
autodoc_class_signature = "separated"

# autodoc_class_signature = "separated"
autodoc_default_options = {
"exclude-members": "__new__",
}

templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
Expand All @@ -32,7 +37,7 @@
"pins": [
x
for x in dir(circuitpython_mocks.board)
if not x.startswith("_") and x != "Pin"
if not x.startswith("_") and x not in ("Pin", "board_id")
]
}
}
Expand Down Expand Up @@ -63,6 +68,7 @@
"features": [
"navigation.top",
"search.share",
"toc.follow",
],
"palette": [
{
Expand Down
Loading

0 comments on commit 0e37ada

Please sign in to comment.