Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve board mock and implement busio singletons #7

Merged
merged 5 commits into from
Aug 6, 2024
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
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