Skip to content

Commit c3b10a4

Browse files
JnyJnyclaude
andcommitted
refactor: Standardize docstrings to Sphinx reStructuredText format
## Summary - Convert all docstrings from mixed formats to consistent Sphinx reStructuredText - Focus on programmer intent rather than redundant type information - Rename Light.available_lights() to available_hardware() for clarity ## Key Changes - **Core classes**: Light, Hardware, Word - enhanced with usage guidance - **Mixins**: ColorableMixin, TaskableMixin - improved parameter documentation - **Vendor base classes**: All updated with inheritance patterns and usage context - **Vendor implementations**: Flag, Blynclight, BlinkStick, Blink1 - focused on device capabilities - **Test fixtures**: Enhanced parameter documentation in conftest.py - **Method rename**: available_lights() → available_hardware() (returns Hardware instances) ## Technical Details - Used `:param name: description` format consistently - Removed indented blank lines per style guidelines - Fixed line length compliance issues - All tests pass (714 passed, 3 skipped) - Code quality checks pass (ruff clean) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ccabfc8 commit c3b10a4

File tree

19 files changed

+394
-149
lines changed

19 files changed

+394
-149
lines changed

src/busylight_core/hardware.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525

2626

2727
class ConnectionType(int, Enum):
28-
"""USB Hardware connection types."""
28+
"""USB device connection protocols supported by the library.
29+
30+
Defines the different communication protocols that can be used
31+
to interact with USB status light devices. Use these values
32+
with Hardware.enumerate() to filter device discovery by protocol type.
33+
"""
2934

3035
ANY: int = -1
3136
UNKNOWN: int = 0
@@ -39,7 +44,14 @@ class ConnectionType(int, Enum):
3944

4045
@dataclass
4146
class Hardware:
42-
"""USB Hardware description."""
47+
"""Represents a USB-connected status light hardware device.
48+
49+
Contains all the information needed to identify, connect to, and
50+
communicate with a USB device that can function as a status light.
51+
Hardware instances are typically created through discovery methods
52+
(enumerate, from_hid, from_portinfo) and then used to initialize
53+
Light instances for device control.
54+
"""
4355

4456
device_type: ConnectionType
4557
path: bytes
@@ -57,11 +69,20 @@ class Hardware:
5769

5870
@classmethod
5971
def enumerate(cls, by_type: ConnectionType = ConnectionType.ANY) -> list[Hardware]:
60-
"""List of all connected hardware devices.
72+
"""Discover all USB devices that could potentially be status lights.
73+
74+
Scans the system for USB devices using HID and serial protocols,
75+
creating Hardware instances for each discovered device. Use this
76+
for device discovery, inventory management, or when you need to
77+
present users with available hardware options.
6178
62-
Raises:
63-
- NotImplementedError
79+
The returned Hardware instances represent raw device information
80+
and have not been tested for compatibility with specific Light classes.
81+
Use Light.claims() to determine which Light class can control each device.
6482
83+
:param by_type: Limit discovery to specific connection types or scan all types
84+
:return: List of Hardware instances representing discovered USB devices
85+
:raises NotImplementedError: If the specified connection type is not supported
6586
"""
6687
hardware_info = []
6788

@@ -89,11 +110,16 @@ def enumerate(cls, by_type: ConnectionType = ConnectionType.ANY) -> list[Hardwar
89110

90111
@classmethod
91112
def from_portinfo(cls, port_info: ListPortInfo) -> Hardware:
92-
"""Create a Hardware object from a serial port info object.
113+
"""Create Hardware instance from serial port information.
93114
94-
Raises:
95-
- InvalidHardwareError
115+
Converts a pyserial ListPortInfo object into a Hardware instance
116+
suitable for Light class initialization. Use this when you have
117+
serial port information from pyserial's list_ports.comports()
118+
and need to create a Hardware representation.
96119
120+
:param port_info: Serial port information from pyserial enumeration
121+
:return: Hardware instance representing the serial device
122+
:raises InvalidHardwareError: If port information is incomplete or invalid
97123
"""
98124
try:
99125
return cls(
@@ -112,11 +138,15 @@ def from_portinfo(cls, port_info: ListPortInfo) -> Hardware:
112138

113139
@classmethod
114140
def from_hid(cls, device: dict) -> Hardware:
115-
"""Create a Hardware object from a HID dictionary.
141+
"""Create Hardware instance from HID device information.
116142
117-
Raises:
118-
- InvalidHardwareError
143+
Converts a HID device dictionary (from hid.enumerate()) into a Hardware
144+
instance suitable for Light class initialization. Use this when you have
145+
HID device information and need to create a Hardware representation.
119146
147+
:param device: HID device dictionary from hidapi enumeration
148+
:return: Hardware instance representing the HID device
149+
:raises InvalidHardwareError: If device information is incomplete or invalid
120150
"""
121151
try:
122152
return cls(device_type=ConnectionType.HID, **device)

src/busylight_core/light.py

Lines changed: 115 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class Light(abc.ABC, TaskableMixin):
6363
and managing USB connected lights without having to know aprori
6464
details of the hardware present.
6565
66-
- Light.available_lights() lists devices discovered
66+
- Light.available_hardware() lists devices discovered
6767
- Light.all_lights() returns all discovered lights ready for use
6868
- Light.first_light() returns the first available light
6969
@@ -84,7 +84,14 @@ class Light(abc.ABC, TaskableMixin):
8484
@classmethod
8585
@lru_cache(maxsize=1)
8686
def vendor(cls) -> str:
87-
"""Return the vendor name in title case."""
87+
"""Get the vendor name for this device class.
88+
89+
Returns a human-readable vendor name derived from the module structure.
90+
Device-specific subclasses should override this method to provide
91+
accurate vendor branding that matches the physical device labeling.
92+
93+
:return: Title-cased vendor name for display in user interfaces
94+
"""
8895
# EJO this is a low-effort way to get the vendor
8996
# name from the module name. Subclasses can
9097
# and should override this method to provide
@@ -94,18 +101,41 @@ def vendor(cls) -> str:
94101
@classmethod
95102
@lru_cache(maxsize=1)
96103
def unique_device_names(cls) -> list[str]:
97-
"""Return a list of unique device names."""
104+
"""Get all unique marketing names for devices supported by this class.
105+
106+
Returns the human-readable product names from the supported_device_ids
107+
mapping, with duplicates removed. Use this to display available device
108+
types to users or for device capability documentation.
109+
110+
:return: Sorted list of unique device marketing names
111+
"""
98112
return sorted(set(cls.supported_device_ids.values()))
99113

100114
@classmethod
101115
@lru_cache(maxsize=1)
102116
def unique_device_ids(cls) -> list[tuple[int, int]]:
103-
"""Return a list of unique device IDs."""
117+
"""Get all unique vendor/product ID pairs supported by this class.
118+
119+
Returns the USB vendor and product ID combinations that this class
120+
can control. Use this for hardware enumeration, udev rule generation,
121+
or debugging device detection issues.
122+
123+
:return: Sorted list of (vendor_id, product_id) tuples
124+
"""
104125
return sorted(set(cls.supported_device_ids.keys()))
105126

106127
@classmethod
107128
def claims(cls, hardware: Hardware) -> bool:
108-
"""Return True if the hardware is claimed by this class."""
129+
"""Check if this class can control the given hardware device.
130+
131+
Determines whether this Light subclass supports the specific hardware
132+
by checking if the device's vendor/product ID pair matches any entry
133+
in the supported_device_ids mapping. Use this during device discovery
134+
to find the appropriate Light subclass for each detected device.
135+
136+
:param hardware: Hardware instance to test for compatibility
137+
:return: True if this class can control the hardware device
138+
"""
109139
return hardware.device_id in cls.supported_device_ids
110140

111141
@classmethod
@@ -138,16 +168,19 @@ def supported_lights(cls) -> dict[str, list[str]]:
138168
return supported_lights
139169

140170
@classmethod
141-
def available_lights(cls) -> dict[type[Light], list[Hardware]]:
142-
"""Return a dictionary of available hardware by type.
171+
def available_hardware(cls) -> dict[type[Light], list[Hardware]]:
172+
"""Discover all compatible hardware devices available for control.
173+
174+
Scans the system for USB devices that match known vendor/product ID
175+
combinations and groups them by the Light subclass that can control
176+
them. Use this for device discovery, inventory management, or when
177+
you need to present users with available device options.
143178
144-
Keys are Light subclasses, values are a list of Hardware instances.
179+
The returned Hardware instances represent devices that were found
180+
and claimed by Light subclasses, but may still be in use by other
181+
processes. Actual device acquisition occurs during Light initialization.
145182
146-
The Hardware instances are a record of light devices that were
147-
discovered during the enumeration process and were claimed by
148-
a Light subclass. The hardware device may be in use by another
149-
process, which will be reported when attempting to acquire the
150-
device during Light subclass initialization.
183+
:return: Mapping from Light subclass to list of compatible Hardware instances
151184
"""
152185
available_lights: dict[type[Light], list[Hardware]] = {}
153186

@@ -168,22 +201,22 @@ def available_lights(cls) -> dict[type[Light], list[Hardware]]:
168201

169202
@classmethod
170203
def all_lights(cls, *, reset: bool = True, exclusive: bool = True) -> list[Light]:
171-
"""Return a list of all lights ready for use.
204+
"""Create initialized Light instances for all available compatible devices.
172205
173-
All the lights in the list have been initialized with the
174-
given reset and exclusive parameters. Lights acquired with
175-
exclusive=True can only be used by the current process and
176-
will block other processes from using the same hardware until
177-
the light is released.
206+
Discovers all compatible hardware and returns Light instances ready for
207+
immediate use. Each light is initialized with the specified configuration
208+
and can be used to control its device without further setup.
178209
179-
If no lights are found, an empty list is returned.
210+
Use this when you want to control all connected lights simultaneously,
211+
such as for synchronized effects or system-wide status indication.
180212
181-
:param: reset - bool - reset the hardware to a known state
182-
:param: exclusive - bool - acquire exclusive access to the hardware
213+
:param reset: Reset devices to known state during initialization
214+
:param exclusive: Acquire exclusive access to prevent interference
215+
:return: List of initialized Light instances, empty if none found
183216
"""
184217
lights: list[Light] = []
185218

186-
for subclass, devices in cls.available_lights().items():
219+
for subclass, devices in cls.available_hardware().items():
187220
for device in devices:
188221
try:
189222
lights.append(subclass(device, reset=reset, exclusive=exclusive))
@@ -194,18 +227,19 @@ def all_lights(cls, *, reset: bool = True, exclusive: bool = True) -> list[Light
194227

195228
@classmethod
196229
def first_light(cls, *, reset: bool = True, exclusive: bool = True) -> Light:
197-
"""Return the first unused light ready for use.
198-
199-
The light returned has been initialized with the given reset
200-
and exclusive parameters. If exclusive=True, the light can
201-
only be used by the current process and will block other
202-
processes from using the light until it is released.
230+
"""Create the first available Light instance ready for immediate use.
203231
204-
Raises:
205-
- NoLightsFoundError
232+
Discovers compatible devices and returns the first successfully
233+
initialized Light instance. Use this when you need a single light
234+
for simple status indication and don't care about the specific
235+
device type or vendor.
206236
237+
:param reset: Reset device to known state during initialization
238+
:param exclusive: Acquire exclusive access to prevent interference
239+
:return: Initialized Light instance ready for control
240+
:raises NoLightsFoundError: If no compatible devices found or init fails
207241
"""
208-
for subclass, devices in cls.available_lights().items():
242+
for subclass, devices in cls.available_hardware().items():
209243
for device in devices:
210244
try:
211245
return subclass(device, reset=reset, exclusive=exclusive)
@@ -255,29 +289,19 @@ def __init__(
255289
reset: bool = False,
256290
exclusive: bool = True,
257291
) -> None:
258-
"""Initialize a Light with the given hardware information.
259-
260-
The hardware argument is an instance of the Hardware class
261-
usually obtained from the Hardware.enumerate method. Due to
262-
vagaries in vendor USB implementations, the supplied hardware
263-
can contain incomplete information compared to other vendors
264-
and it's up to the Light subclass to fill in some of the
265-
blanks.
266-
267-
The reset keyword-only parameter controls whether the hardware
268-
is reset to a known state using the Light.reset method.
292+
"""Initialize a Light instance with the specified hardware device.
269293
270-
The exclusive keyword-only parameter controls whether the
271-
process creating this light has exclusive access to the
272-
hardware.
294+
Creates a Light instance bound to the given hardware device and
295+
configures it for use. The hardware should be obtained from
296+
Hardware.enumerate() and verified with the class's claims() method.
273297
274-
:param: hardware - Hardware
275-
:param: reset - bool
276-
:param: exclusive - bool
277-
278-
Raises:
279-
- HardwareUnsupportedError
298+
Use this constructor when you have specific hardware and want to
299+
create a Light instance for direct device control.
280300
301+
:param hardware: Hardware instance representing the device to control
302+
:param reset: Reset the device to a known state during initialization
303+
:param exclusive: Acquire exclusive access to prevent interference
304+
:raises HardwareUnsupportedError: If Light class cannot control hardware
281305
"""
282306
if not self.__class__.claims(hardware):
283307
raise HardwareUnsupportedError(hardware, self.__class__)
@@ -400,11 +424,17 @@ def exclusive_access(self) -> Generator[None, None, None]:
400424
self.hardware.release()
401425

402426
def update(self) -> None:
403-
"""Obtain the current state of the light and write it to the device.
427+
"""Send the current light state to the physical device.
428+
429+
Serializes the light's current state and transmits it to the hardware
430+
device using the appropriate platform-specific protocol. Call this
431+
method after making changes to light properties to apply them to
432+
the physical device.
404433
405-
Raises:
406-
- LightUnavailableError
434+
The method handles platform-specific protocol differences automatically,
435+
such as adding leading zero bytes on Windows 10.
407436
437+
:raises LightUnavailableError: If device communication fails
408438
"""
409439
state = bytes(self)
410440

@@ -426,12 +456,15 @@ def update(self) -> None:
426456

427457
@contextlib.contextmanager
428458
def batch_update(self) -> Generator[None, None, None]:
429-
"""Update the software state of the light on exit.
459+
"""Defer device updates until multiple properties are changed.
460+
461+
Context manager that accumulates multiple property changes and sends
462+
them to the device in a single update operation when exiting the
463+
context. Use this when changing multiple light properties (color,
464+
brightness, effects) to reduce USB communication overhead and improve
465+
performance.
430466
431-
This context manager is convenience for updating multiple
432-
light attribute values at once and write the new state to the
433-
hardware on exit. This approach reduces the number of writes
434-
to the hardware, which is beneficial for performance.
467+
:return: Context manager for batching multiple property updates
435468
"""
436469
yield
437470
self.update()
@@ -442,30 +475,32 @@ def on(
442475
color: tuple[int, int, int],
443476
led: int = 0,
444477
) -> None:
445-
"""Activate the light with the given red, green, blue color tuple.
478+
"""Activate the light with the specified RGB color.
446479
447-
If a subclass supports multiple LEDs, the `led` parameter
448-
specifies which LED to activate. If `led` is 0, all LEDs
449-
are activated with the specified color. If a subclasss
450-
does not support multiple LEDs, the `led` parameter
451-
is ignored and defaults to 0.
480+
Sets the light to display the given color immediately. This is the
481+
primary method for controlling light appearance and should be
482+
implemented by all device-specific subclasses.
452483
453-
Color tuple values should be in the range of 0-255.
484+
For devices with multiple LEDs, use led parameter to target specific
485+
LEDs or set to 0 to affect all LEDs simultaneously. Single-LED devices
486+
should ignore the led parameter.
454487
455-
:param: color: tuple[int, int, int] - RGB color tuple
456-
:param: led: int - LED index, 0 for all LEDs
488+
:param color: RGB intensity values from 0-255 for each color component
489+
:param led: Target LED index, 0 affects all LEDs on the device
457490
"""
458491
raise NotImplementedError
459492

460493
def off(self, led: int = 0) -> None:
461-
"""Deactivate the light.
494+
"""Turn off the light by setting it to black.
462495
463-
If a subclass supports multiple LEDs, the `led` parameter
464-
specifies which LED to deactivate. If `led` is 0, all LEDs
465-
are deactivated. If a subclass does not support multiple LEDs,
466-
the `led` parameter is ignored and defaults to 0.
496+
Deactivates the specified LED(s) by setting their color to black (0, 0, 0).
497+
Use this to turn off status indication while keeping the device available
498+
for future color changes.
467499
468-
:param: led: int - LED index, 0 for all LEDs
500+
For multi-LED devices, specify the LED index or use 0 to turn off all LEDs.
501+
Single-LED devices ignore the led parameter.
502+
503+
:param led: Target LED index, 0 affects all LEDs on the device
469504
"""
470505
self.on((0, 0, 0), led)
471506

@@ -490,7 +525,11 @@ def color(self) -> tuple[int, int, int]:
490525
def color(self, value: tuple[int, int, int]) -> None:
491526
"""Set the RGB color of the light.
492527
493-
:param: value: tuple[int, int, int] - RGB color tuple
528+
Updates the light's color state to the specified RGB values.
529+
Device-specific implementations should store this value and
530+
apply it during the next update() call.
531+
532+
:param value: RGB intensity values from 0-255 for each color component
494533
"""
495534
raise NotImplementedError
496535

0 commit comments

Comments
 (0)