Skip to content

Commit 0e37ada

Browse files
authored
improve board mock and implement busio singletons (#7)
* 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
1 parent 93b6b08 commit 0e37ada

File tree

11 files changed

+279
-25
lines changed

11 files changed

+279
-25
lines changed

circuitpython_mocks/board.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"""A module that hosts mock pins."""
1+
"""A module that hosts mock pins and default :py:class:`~busio.SPI`,
2+
:py:class:`~busio.I2C`, and :py:class:`~busio.UART` data buses."""
3+
4+
#: A dummy identifier to allow detection when using this mock library.
5+
board_id = "CIRCUITPYTHON_MOCK"
26

37

48
class Pin:
@@ -7,6 +11,33 @@ class Pin:
711
pass
812

913

14+
A0 = Pin()
15+
A1 = Pin()
16+
A2 = Pin()
17+
A3 = Pin()
18+
A4 = Pin()
19+
A5 = Pin()
20+
A6 = Pin()
21+
A7 = Pin()
22+
A8 = Pin()
23+
A9 = Pin()
24+
A10 = Pin()
25+
A11 = Pin()
26+
A12 = Pin()
27+
A13 = Pin()
28+
A14 = Pin()
29+
A15 = Pin()
30+
A16 = Pin()
31+
A17 = Pin()
32+
A18 = Pin()
33+
A19 = Pin()
34+
A20 = Pin()
35+
A21 = Pin()
36+
A22 = Pin()
37+
A23 = Pin()
38+
A24 = Pin()
39+
A25 = Pin()
40+
1041
D0 = Pin()
1142
D1 = Pin()
1243
D2 = Pin()
@@ -110,6 +141,8 @@ class Pin:
110141

111142
SDA = Pin()
112143
SCL = Pin()
144+
SDA1 = Pin()
145+
SCL1 = Pin()
113146

114147
CE1 = Pin()
115148
CE0 = Pin()
@@ -121,11 +154,46 @@ class Pin:
121154
TXD = Pin()
122155
RXD = Pin()
123156

124-
# create alias for most of the examples
125157
TX = Pin()
126158
RX = Pin()
127159

128160
MISO_1 = Pin()
129161
MOSI_1 = Pin()
130162
SCLK_1 = Pin()
131163
SCK_1 = Pin()
164+
CS = Pin()
165+
166+
WS = Pin()
167+
SD = Pin()
168+
169+
LED = Pin()
170+
NEOPIXEL = Pin()
171+
DOTSTAR = Pin()
172+
173+
174+
def SPI():
175+
"""Creates a default instance (singleton) of :py:class:`~busio.SPI`"""
176+
from circuitpython_mocks.busio import SPI as ImplSPI
177+
178+
return ImplSPI(SCK, MOSI, MISO)
179+
180+
181+
def I2C():
182+
"""Creates a default instance (singleton) of :py:class:`~busio.I2C`"""
183+
from circuitpython_mocks.busio import I2C as ImplI2C
184+
185+
return ImplI2C(SCL, SDA)
186+
187+
188+
def STEMMA_I2C():
189+
"""Creates a default instance (singleton) of :py:class:`~busio.I2C`"""
190+
from circuitpython_mocks.busio import I2C as ImplI2C
191+
192+
return ImplI2C(SCL1, SDA1)
193+
194+
195+
def UART():
196+
"""Creates a default instance (singleton) of :py:class:`~busio.UART`"""
197+
from circuitpython_mocks.busio import UART as ImplUART
198+
199+
return ImplUART(TX, RX)

circuitpython_mocks/busio/__init__.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from enum import Enum, auto
44
import sys
5-
from typing import List
5+
from typing import List, Optional
66

77
import circuitpython_typing
88

@@ -17,11 +17,39 @@
1717
SPITransfer,
1818
)
1919
from circuitpython_mocks._mixins import Expecting, Lockable
20-
from circuitpython_mocks.board import Pin
20+
from circuitpython_mocks.board import (
21+
Pin,
22+
SDA,
23+
SDA1,
24+
SCL,
25+
SCL1,
26+
SCK,
27+
MOSI as PinMOSI,
28+
MISO as PinMISO,
29+
TX,
30+
RX,
31+
MISO_1,
32+
MOSI_1,
33+
SCK_1,
34+
)
2135

2236

2337
class I2C(Expecting, Lockable):
24-
"""A mock of `busio.I2C` class."""
38+
"""A mock of :external:py:class:`busio.I2C` class."""
39+
40+
_primary_singleton: Optional["I2C"] = None
41+
_secondary_singleton: Optional["I2C"] = None
42+
43+
def __new__(cls, scl: Pin, sda: Pin, **kwargs) -> "I2C":
44+
if scl == SCL and sda == SDA:
45+
if cls._primary_singleton is None:
46+
cls._primary_singleton = super().__new__(cls)
47+
return cls._primary_singleton
48+
if scl == SCL1 and sda == SDA1:
49+
if cls._secondary_singleton is None:
50+
cls._secondary_singleton = super().__new__(cls)
51+
return cls._secondary_singleton
52+
return super().__new__(cls)
2553

2654
def __init__(
2755
self,
@@ -31,6 +59,8 @@ def __init__(
3159
frequency: int = 100000,
3260
timeout: int = 255,
3361
):
62+
if hasattr(self, "expectations"):
63+
return
3464
super().__init__()
3565

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

100130

101131
class SPI(Expecting, Lockable):
132+
"""A mock of :external:py:class:`busio.SPI` class."""
133+
134+
_primary_singleton: Optional["SPI"] = None
135+
_secondary_singleton: Optional["SPI"] = None
136+
137+
def __new__(cls, clock: Pin, MOSI: Pin, MISO: Pin, **kwargs) -> "SPI":
138+
if clock == SCK and MOSI == PinMOSI and MISO == PinMISO:
139+
if cls._primary_singleton is None:
140+
cls._primary_singleton = super().__new__(cls)
141+
return cls._primary_singleton
142+
if clock == SCK_1 and MOSI == MOSI_1 and MISO == MISO_1:
143+
if cls._secondary_singleton is None:
144+
cls._secondary_singleton = super().__new__(cls)
145+
return cls._secondary_singleton
146+
return super().__new__(cls)
147+
102148
def __init__(
103149
self,
104150
clock: Pin,
@@ -183,6 +229,15 @@ def write_readinto(
183229
class UART(Expecting, Lockable):
184230
"""A class that mocks :external:py:class:`busio.UART`."""
185231

232+
_primary_singleton: Optional["UART"] = None
233+
234+
def __new__(cls, tx: Pin, rx: Pin, **kwargs) -> "UART":
235+
if tx == TX and rx == RX:
236+
if cls._primary_singleton is None:
237+
cls._primary_singleton = super().__new__(cls)
238+
return cls._primary_singleton
239+
return super().__new__(cls)
240+
186241
class Parity(Enum):
187242
ODD = auto()
188243
EVEN = auto()
@@ -250,3 +305,6 @@ def write(self, buf: circuitpython_typing.ReadableBuffer) -> int | None:
250305
len_buf = len(op.expected)
251306
op.assert_expected(buf, 0, len_buf)
252307
return len(buf) or None
308+
309+
310+
_UART = UART(TX, RX)

circuitpython_mocks/digitalio/__init__.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from enum import Enum, auto
2-
2+
from typing import Union, Optional
33
from circuitpython_mocks._mixins import ContextManaged, Expecting
44
from circuitpython_mocks.digitalio.operations import GetState, SetState
55
from circuitpython_mocks.board import Pin
@@ -34,13 +34,17 @@ def __init__(self, pin: Pin, **kwargs):
3434
self._pin = pin
3535
self.switch_to_input()
3636

37-
def switch_to_output(self, value=False, drive_mode=DriveMode.PUSH_PULL):
37+
def switch_to_output(
38+
self,
39+
value: Union[bool, int] = False,
40+
drive_mode: DriveMode = DriveMode.PUSH_PULL,
41+
):
3842
"""Switch the Digital Pin Mode to Output"""
3943
self.direction = Direction.OUTPUT
4044
self.value = value
4145
self.drive_mode = drive_mode
4246

43-
def switch_to_input(self, pull=None):
47+
def switch_to_input(self, pull: Optional[Pull] = None):
4448
"""Switch the Digital Pin Mode to Input"""
4549
self.direction = Direction.INPUT
4650
self.pull = pull
@@ -50,12 +54,12 @@ def deinit(self):
5054
del self._pin
5155

5256
@property
53-
def direction(self):
57+
def direction(self) -> Direction:
5458
"""Get or Set the Digital Pin Direction"""
5559
return self.__direction
5660

5761
@direction.setter
58-
def direction(self, value):
62+
def direction(self, value: Direction):
5963
self.__direction = value
6064
if value == Direction.OUTPUT:
6165
# self.value = False
@@ -66,7 +70,7 @@ def direction(self, value):
6670
raise AttributeError("Not a Direction")
6771

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

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

9195
@property
92-
def pull(self):
96+
def pull(self) -> Optional[Pull]:
9397
"""The pin pull direction"""
9498
if self.direction == Direction.INPUT:
9599
return self.__pull
96100
raise AttributeError("Not an input")
97101

98102
@pull.setter
99-
def pull(self, pul):
103+
def pull(self, pul: Optional[Pull]):
100104
if self.direction != Direction.INPUT:
101105
raise AttributeError("Not an input")
102106
self.__pull = pul
103107

104108
@property
105-
def drive_mode(self):
109+
def drive_mode(self) -> DriveMode:
106110
"""The Digital Pin Drive Mode"""
107111
if self.direction != Direction.OUTPUT:
108112
raise AttributeError("Not an output")
109113
return self.__drive_mode
110114

111115
@drive_mode.setter
112-
def drive_mode(self, mod):
116+
def drive_mode(self, mod: DriveMode):
113117
self.__drive_mode = mod

docs/board.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
.. automodule:: circuitpython_mocks.board
55
:members:
6-
:undoc-members:
76

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

docs/busio.rst

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,22 @@
22
=================
33

44
.. automodule:: circuitpython_mocks.busio
5-
:members:
5+
6+
.. autoclass:: circuitpython_mocks.busio.I2C
7+
:members: readfrom_into, writeto, writeto_then_readfrom, scan
8+
.. autoclass:: circuitpython_mocks.busio.SPI
9+
:members: readinto, write, write_readinto, configure, frequency
10+
.. autoclass:: circuitpython_mocks.busio.UART
11+
:members: readinto, readline, write
12+
13+
.. py:class:: circuitpython_mocks.busio.UART.Parity
14+
15+
A mock enumeration of :external:py:class:`busio.Parity`.
16+
17+
.. py:attribute:: ODD
18+
:type: Parity
19+
.. py:attribute:: EVEN
20+
:type: Parity
621

722
``busio.operations``
823
--------------------

docs/conf.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@
2020
"sphinx_immaterial",
2121
"sphinx.ext.autodoc",
2222
"sphinx.ext.intersphinx",
23+
"sphinx.ext.viewcode",
2324
"sphinx_jinja",
2425
]
25-
autodoc_class_signature = "separated"
26+
27+
# autodoc_class_signature = "separated"
28+
autodoc_default_options = {
29+
"exclude-members": "__new__",
30+
}
2631

2732
templates_path = ["_templates"]
2833
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
@@ -32,7 +37,7 @@
3237
"pins": [
3338
x
3439
for x in dir(circuitpython_mocks.board)
35-
if not x.startswith("_") and x != "Pin"
40+
if not x.startswith("_") and x not in ("Pin", "board_id")
3641
]
3742
}
3843
}
@@ -63,6 +68,7 @@
6368
"features": [
6469
"navigation.top",
6570
"search.share",
71+
"toc.follow",
6672
],
6773
"palette": [
6874
{

0 commit comments

Comments
 (0)