diff --git a/circuitpython_mocks/board.py b/circuitpython_mocks/board.py index 61bc016..e4285d2 100644 --- a/circuitpython_mocks/board.py +++ b/circuitpython_mocks/board.py @@ -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: @@ -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() @@ -110,6 +141,8 @@ class Pin: SDA = Pin() SCL = Pin() +SDA1 = Pin() +SCL1 = Pin() CE1 = Pin() CE0 = Pin() @@ -121,7 +154,6 @@ class Pin: TXD = Pin() RXD = Pin() -# create alias for most of the examples TX = Pin() RX = Pin() @@ -129,3 +161,39 @@ class 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) diff --git a/circuitpython_mocks/busio/__init__.py b/circuitpython_mocks/busio/__init__.py index c9a0c77..3777ba0 100644 --- a/circuitpython_mocks/busio/__init__.py +++ b/circuitpython_mocks/busio/__init__.py @@ -2,7 +2,7 @@ from enum import Enum, auto import sys -from typing import List +from typing import List, Optional import circuitpython_typing @@ -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, @@ -31,6 +59,8 @@ def __init__( frequency: int = 100000, timeout: int = 255, ): + if hasattr(self, "expectations"): + return super().__init__() def scan(self) -> List[int]: @@ -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, @@ -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() @@ -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) diff --git a/circuitpython_mocks/digitalio/__init__.py b/circuitpython_mocks/digitalio/__init__.py index 487c72f..1cd455f 100644 --- a/circuitpython_mocks/digitalio/__init__.py +++ b/circuitpython_mocks/digitalio/__init__.py @@ -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 @@ -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 @@ -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 @@ -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`.""" @@ -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" @@ -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 diff --git a/docs/board.rst b/docs/board.rst index 63ad627..20c9796 100644 --- a/docs/board.rst +++ b/docs/board.rst @@ -3,7 +3,6 @@ .. automodule:: circuitpython_mocks.board :members: - :undoc-members: This module includes the following dummy pins for soft-testing: diff --git a/docs/busio.rst b/docs/busio.rst index 3665683..fae3ad9 100644 --- a/docs/busio.rst +++ b/docs/busio.rst @@ -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`` -------------------- diff --git a/docs/conf.py b/docs/conf.py index 3b86a1f..07d417c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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"] @@ -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") ] } } @@ -63,6 +68,7 @@ "features": [ "navigation.top", "search.share", + "toc.follow", ], "palette": [ { diff --git a/docs/digitalio.rst b/docs/digitalio.rst index 46f1072..0ae48d7 100644 --- a/docs/digitalio.rst +++ b/docs/digitalio.rst @@ -2,10 +2,42 @@ ================= .. automodule:: circuitpython_mocks.digitalio - :members: + + .. autoclass:: circuitpython_mocks.digitalio.DigitalInOut + :members: value, deinit, direction, drive_mode, pull, switch_to_input, switch_to_output + + .. py:class:: circuitpython_mocks.digitalio.Direction + + A mock enumeration of :external:py:class:`digitalio.Direction`. + + .. py:attribute:: INPUT + :type: Direction + .. py:attribute:: OUTPUT + :type: Direction + + .. py:class:: circuitpython_mocks.digitalio.DriveMode + + A mock enumeration of :external:py:class:`digitalio.DriveMode`. + + .. py:attribute:: PUSH_PULL + :type: DriveMode + .. py:attribute:: OPEN_DRAIN + :type: DriveMode + + .. py:class:: circuitpython_mocks.digitalio.Pull + + A mock enumeration of :external:py:class:`digitalio.Pull`. + + .. py:attribute:: UP + :type: Pull + .. py:attribute:: DOWN + :type: Pull + ``digitalio.operations`` ------------------------ .. automodule:: circuitpython_mocks.digitalio.operations - :members: + + .. autoclass:: circuitpython_mocks.digitalio.operations.SetState + .. autoclass:: circuitpython_mocks.digitalio.operations.GetState diff --git a/docs/index.rst b/docs/index.rst index 646caef..3a68f06 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,7 @@ projects (with pytest). .. literalinclude:: ../tests/test_i2c.py :language: python + :end-before: def test_default(): .. code-annotations:: #. |pytest-used-import| @@ -26,6 +27,7 @@ projects (with pytest). .. literalinclude:: ../tests/test_spi.py :language: python + :end-before: def test_default(): .. code-annotations:: #. |pytest-used-import| @@ -34,6 +36,7 @@ projects (with pytest). .. literalinclude:: ../tests/test_uart.py :language: python + :end-before: def test_default(): .. code-annotations:: #. |pytest-used-import| diff --git a/tests/test_i2c.py b/tests/test_i2c.py index b125b18..152d52d 100644 --- a/tests/test_i2c.py +++ b/tests/test_i2c.py @@ -9,7 +9,6 @@ def test_i2c(): import board from busio import I2C - from adafruit_bus_device.i2c_device import I2CDevice address = 0x42 @@ -38,3 +37,30 @@ def test_i2c(): i2c.write(buf, end=1) with i2c_dev as i2c: i2c.write_then_readinto(buf, buf, out_end=1, in_end=1) + + +def test_default(): + # here we cannot import from the monkey-patched sys path because + # the mock modules use absolute imports. + from circuitpython_mocks import board, busio + from collections import deque + + i2c = board.I2C() + assert hasattr(i2c, "expectations") + assert isinstance(i2c.expectations, deque) + i2c_dupe = busio.I2C(board.SCL, board.SDA) + assert i2c == i2c_dupe + i2c.expectations.append(I2CRead(0x42, bytearray(1))) + assert i2c_dupe.expectations == i2c.expectations + op = i2c_dupe.expectations.popleft() + assert not i2c.expectations + + i2c1 = board.STEMMA_I2C() + assert hasattr(i2c1, "expectations") + assert isinstance(i2c1.expectations, deque) + i2c1_dupe = busio.I2C(board.SCL1, board.SDA1) + assert i2c1 == i2c1_dupe + i2c1.expectations.append(op) + assert i2c1_dupe.expectations == i2c1.expectations + op = i2c1_dupe.expectations.popleft() + assert not i2c1.expectations diff --git a/tests/test_spi.py b/tests/test_spi.py index b2c1ffa..de64262 100644 --- a/tests/test_spi.py +++ b/tests/test_spi.py @@ -10,9 +10,8 @@ def test_spi(): from busio import SPI from digitalio import DigitalInOut - - from adafruit_bus_device.spi_device import SPIDevice import board + from adafruit_bus_device.spi_device import SPIDevice # do setup with SPI(board.SCK, board.MOSI, board.MISO) as spi_bus: @@ -41,3 +40,30 @@ def test_spi(): spi.write(buf, end=1) with spi_dev as spi: spi.write_readinto(buf, buf, out_end=1, in_end=1) + + +def test_default(): + # here we cannot import from the monkey-patched sys path because + # the mock modules use absolute imports. + from circuitpython_mocks import board, busio + from collections import deque + + spi = board.SPI() + assert hasattr(spi, "expectations") + assert isinstance(spi.expectations, deque) + spi_dupe = busio.SPI(board.SCK, board.MOSI, board.MISO) + assert spi == spi_dupe + spi.expectations.append(SPIRead(bytearray(1))) + assert spi_dupe.expectations == spi.expectations + op = spi_dupe.expectations.popleft() + assert not spi.expectations + + spi1 = busio.SPI(board.SCK_1, board.MOSI_1, board.MISO_1) + assert hasattr(spi1, "expectations") + assert isinstance(spi1.expectations, deque) + spi1_dupe = busio.SPI(board.SCK_1, board.MOSI_1, board.MISO_1) + assert spi1 == spi1_dupe + spi1.expectations.append(op) + assert spi1_dupe.expectations == spi1.expectations + _ = spi1_dupe.expectations.popleft() + assert not spi1.expectations diff --git a/tests/test_uart.py b/tests/test_uart.py index c333bbc..c4fa92e 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -27,3 +27,20 @@ def test_uart(): serial.readinto(buf) _result = serial.readline() assert serial.write(buf) == 1 + + +def test_default(): + # here we cannot import from the monkey-patched sys path because + # the mock modules use absolute imports. + from circuitpython_mocks import board, busio + from collections import deque + + uart = board.UART() + assert hasattr(uart, "expectations") + assert isinstance(uart.expectations, deque) + uart_dupe = busio.UART(board.TX, board.RX) + assert uart == uart_dupe + uart.expectations.append(UARTRead(bytearray(1))) + assert uart_dupe.expectations == uart.expectations + _ = uart_dupe.expectations.popleft() + assert not uart.expectations