Skip to content

Commit

Permalink
migrate fixture into a separate module (#11)
Browse files Browse the repository at this point in the history
* move fixture to fixtures module
* segregate tests and add `board.SPI1()`
* make base ops private
* rm dev artifact from #7
* improve docs
  • Loading branch information
2bndy5 authored Aug 7, 2024
1 parent ce9d917 commit 42fe448
Show file tree
Hide file tree
Showing 19 changed files with 524 additions and 305 deletions.
16 changes: 0 additions & 16 deletions circuitpython_mocks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +0,0 @@
from pathlib import Path
import pytest


@pytest.fixture(autouse=True)
def monkey_patch_sys_paths(monkeypatch: pytest.MonkeyPatch):
"""A pytest fixture that monkey patches the Python runtime's import paths, such
that this package's mock modules can be imported first (instead from the
adafruit-blinka package).
.. important::
This fixture is automatically used once imported into the test module.
"""
root_pkg = Path(__file__).parent
monkeypatch.syspath_prepend(str(root_pkg))
80 changes: 66 additions & 14 deletions circuitpython_mocks/_mixins.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
from typing import Self
from collections import deque
from typing import TYPE_CHECKING
from typing import Self, Deque, Union, TYPE_CHECKING

if TYPE_CHECKING:
from circuitpython_mocks.busio.operations import Read, Write, Transfer
from circuitpython_mocks.busio.operations import (
I2CRead,
I2CWrite,
I2CTransfer,
SPIRead,
SPIWrite,
SPITransfer,
UARTRead,
UARTWrite,
)
from circuitpython_mocks.digitalio.operations import SetState, GetState


Expand All @@ -13,40 +21,84 @@ class ContextManaged:
def __enter__(self) -> Self:
return self

def __exit__(self, exc_type, exc_value, traceback):
def __exit__(self, exc_type, exc_value, traceback) -> None:
self.deinit()

def deinit(self):
def deinit(self) -> None:
"""Free any hardware used by the object."""
return


class Lockable(ContextManaged):
"""An object that must be locked to prevent collisions on a microcontroller resource."""
"""An object that must be locked to prevent collisions on a microcontroller
resource."""

_locked = False

def try_lock(self):
"""Attempt to grab the lock. Return True on success, False if the lock is already taken."""
def try_lock(self) -> bool:
"""Attempt to grab the lock. Return `True` on success, `False` if the lock is
already taken."""
if self._locked:
return False
self._locked = True
return True

def unlock(self):
def unlock(self) -> None:
"""Release the lock so others may use the resource."""
if self._locked:
self._locked = False


class Expecting:
"""A base class for the mock classes used to assert expected behaviors."""
"""A base class for the mock classes used to assert expected behaviors.
.. seealso::
:title: Mocks that derive from this mixin class
- `busio.I2C`
- `board.I2C()`
- `board.STEMMA_I2C()`
- `busio.SPI`
- `board.SPI()`
- `board.SPI1()`
- `busio.UART`
- `board.UART()`
- `digitalio.DigitalInOut`
"""

def __init__(self, **kwargs) -> None:
#: A double ended queue used to assert expected behavior
self.expectations: deque[Read | Write | Transfer | SetState | GetState] = (
deque()
)
self.expectations: Deque[
Union[
I2CRead,
I2CWrite,
I2CTransfer,
SPIRead,
SPIWrite,
SPITransfer,
UARTRead,
UARTWrite,
SetState,
GetState,
]
] = deque()
"""A double-ended queue (:py:class:`~collections.deque`) used to assert
expected behavior.
.. example::
Examples that use `expectations` can be found in the
- :doc:`busio` documentation
- :doc:`digitalio` documentation
- :py:func:`~circuitpython_mocks.fixtures.mock_blinka_imports`
(pytest fixture) documentation
.. _this package's tests files:
https://github.com/2bndy5/CircuitPython-mocks/tree/main/tests
All examples' source is located in `this package's tests files`_.
"""

super().__init__(**kwargs)

def done(self):
Expand Down
9 changes: 8 additions & 1 deletion circuitpython_mocks/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ def SPI():
return ImplSPI(SCK, MOSI, MISO)


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

return ImplSPI(SCK_1, MOSI_1, MISO_1)


def I2C():
"""Creates a default instance (singleton) of :py:class:`~busio.I2C`"""
from circuitpython_mocks.busio import I2C as ImplI2C
Expand All @@ -186,7 +193,7 @@ def I2C():


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

return ImplI2C(SCL1, SDA1)
Expand Down
103 changes: 79 additions & 24 deletions circuitpython_mocks/busio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
"""A module to mock the data bus transactions."""
"""A mock of the :external:py:mod:`busio` module.
.. md-tab-set::
.. md-tab-item:: I2C
.. literalinclude:: ../tests/test_i2c.py
:language: python
.. md-tab-item:: SPI
.. literalinclude:: ../tests/test_spi.py
:language: python
.. md-tab-item:: UART
.. literalinclude:: ../tests/test_uart.py
:language: python
"""

from enum import Enum, auto
import sys
Expand Down Expand Up @@ -77,8 +95,12 @@ def readfrom_into(
end: int = sys.maxsize,
) -> None:
"""A mock imitation of :external:py:meth:`busio.I2C.readfrom_into()`.
This function checks against `I2CRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `I2CRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for I2C.readfrom_into()"
op = self.expectations.popleft()
assert isinstance(op, I2CRead), f"Read operation expected, found {repr(op)}"
Expand All @@ -95,8 +117,12 @@ def writeto(
end: int = sys.maxsize,
) -> None:
"""A mock imitation of :external:py:meth:`busio.I2C.writeto()`.
This function checks against `I2CWrite`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `I2CWrite`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for I2C.writeto()"
op = self.expectations.popleft()
assert isinstance(op, I2CWrite), f"Read operation expected, found {repr(op)}"
Expand All @@ -115,8 +141,12 @@ def writeto_then_readfrom(
in_end: int = sys.maxsize,
) -> None:
"""A mock imitation of :external:py:meth:`busio.I2C.writeto_then_readfrom()`.
This function checks against `I2CTransfer`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `I2CTransfer`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for I2C.writeto_then_readfrom()"
op = self.expectations.popleft()
assert isinstance(
Expand Down Expand Up @@ -180,8 +210,12 @@ def write(
end: int = sys.maxsize,
) -> None:
"""A function that mocks :external:py:meth:`busio.SPI.write()`.
This function checks against `SPIWrite`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `SPIWrite`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for SPI.write()"
op = self.expectations.popleft()
assert isinstance(op, SPIWrite), f"Read operation expected, found {repr(op)}"
Expand All @@ -196,8 +230,12 @@ def readinto(
write_value: int = 0,
) -> None:
"""A function that mocks :external:py:meth:`busio.SPI.readinto()`.
This function checks against `SPIRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `SPIRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for SPI.readinto()"
op = self.expectations.popleft()
assert isinstance(op, SPIRead), f"Read operation expected, found {repr(op)}"
Expand All @@ -214,8 +252,12 @@ def write_readinto(
in_end: int = sys.maxsize,
) -> None:
"""A function that mocks :external:py:meth:`busio.SPI.write_readinto()`.
This function checks against `SPITransfer`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `SPITransfer`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for SPI.write_readinto()"
op = self.expectations.popleft()
assert isinstance(
Expand Down Expand Up @@ -262,8 +304,12 @@ def __init__(

def read(self, nbytes: int | None = None) -> bytes | None:
"""A function that mocks :external:py:meth:`busio.UART.read()`.
This function checks against `UARTRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `UARTRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for UART.read()"
op = self.expectations.popleft()
assert isinstance(op, UARTRead), f"Read operation expected, found {repr(op)}"
Expand All @@ -274,8 +320,12 @@ def read(self, nbytes: int | None = None) -> bytes | None:

def readinto(self, buf: circuitpython_typing.WriteableBuffer) -> int | None:
"""A function that mocks :external:py:meth:`busio.UART.readinto()`.
This function checks against `UARTRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `UARTRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for UART.readinto()"
op = self.expectations.popleft()
assert isinstance(op, UARTRead), f"Read operation expected, found {repr(op)}"
Expand All @@ -285,8 +335,12 @@ def readinto(self, buf: circuitpython_typing.WriteableBuffer) -> int | None:

def readline(self) -> bytes:
"""A function that mocks :external:py:meth:`busio.UART.readline()`.
This function checks against `UARTRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `UARTRead`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for UART.readline()"
op = self.expectations.popleft()
assert isinstance(op, UARTRead), f"Read operation expected, found {repr(op)}"
Expand All @@ -297,14 +351,15 @@ def readline(self) -> bytes:

def write(self, buf: circuitpython_typing.ReadableBuffer) -> int | None:
"""A function that mocks :external:py:meth:`busio.UART.write()`.
This function checks against `UARTWrite`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`"""
.. mock-expects::
This function checks against `UARTWrite`
:py:attr:`~circuitpython_mocks._mixins.Expecting.expectations`.
"""
assert self.expectations, "no expectation found for UART.write()"
op = self.expectations.popleft()
assert isinstance(op, UARTWrite), f"Read operation expected, found {repr(op)}"
len_buf = len(op.expected)
op.assert_expected(buf, 0, len_buf)
return len(buf) or None


_UART = UART(TX, RX)
Loading

0 comments on commit 42fe448

Please sign in to comment.