diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..2da681d --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# apply Black to all files +2c4173dbae8564885ece7371e08e220a25ca8dcb \ No newline at end of file diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 0000000..f382a80 --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,14 @@ +name: Formatting + +on: [push, pull_request] + +jobs: + black: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + steps: + - uses: actions/checkout@v4 + - uses: psf/black@stable + with: + options: "--check --verbose" + src: "./src ./tests" diff --git a/src/blinkstick/__init__.py b/src/blinkstick/__init__.py index 6d2c3ab..78537bc 100644 --- a/src/blinkstick/__init__.py +++ b/src/blinkstick/__init__.py @@ -1,7 +1,12 @@ from importlib.metadata import version, PackageNotFoundError from .blinkstick import BlinkStick, BlinkStickPro, BlinkStickProMatrix -from .blinkstick import find_all, find_by_serial, find_first, get_blinkstick_package_version +from .blinkstick import ( + find_all, + find_by_serial, + find_first, + get_blinkstick_package_version, +) from .colors import Color, ColorFormat from .constants import BlinkStickVariant from .exceptions import BlinkStickException @@ -9,4 +14,4 @@ try: __version__ = version("blinkstick") except PackageNotFoundError: - __version__ = "BlinkStick package not installed" \ No newline at end of file + __version__ = "BlinkStick package not installed" diff --git a/src/blinkstick/backends/base.py b/src/blinkstick/backends/base.py index 7e0e1ed..9e67007 100644 --- a/src/blinkstick/backends/base.py +++ b/src/blinkstick/backends/base.py @@ -25,7 +25,14 @@ def find_by_serial(serial: str) -> BaseBackend | None: raise NotImplementedError @abstractmethod - def control_transfer(self, bmRequestType: int, bRequest: int, wValue: int, wIndex: int, data_or_wLength: bytes | int): + def control_transfer( + self, + bmRequestType: int, + bRequest: int, + wValue: int, + wIndex: int, + data_or_wLength: bytes | int, + ): raise NotImplementedError @abstractmethod diff --git a/src/blinkstick/backends/unix_like.py b/src/blinkstick/backends/unix_like.py index e2a7172..9310389 100644 --- a/src/blinkstick/backends/unix_like.py +++ b/src/blinkstick/backends/unix_like.py @@ -39,7 +39,9 @@ def _refresh_device(self): @staticmethod def find_blinksticks(find_all: bool = True): - return usb.core.find(find_all=find_all, idVendor=VENDOR_ID, idProduct=PRODUCT_ID) + return usb.core.find( + find_all=find_all, idVendor=VENDOR_ID, idProduct=PRODUCT_ID + ) @staticmethod def find_by_serial(serial: str) -> list | None: @@ -51,23 +53,37 @@ def find_by_serial(serial: str) -> list | None: except Exception as e: print("{0}".format(e)) - def control_transfer(self, bmRequestType: int, bRequest: int, wValue: int, wIndex: int, - data_or_wLength: bytes | int): + def control_transfer( + self, + bmRequestType: int, + bRequest: int, + wValue: int, + wIndex: int, + data_or_wLength: bytes | int, + ): try: - return self.device.ctrl_transfer(bmRequestType, bRequest, wValue, wIndex, data_or_wLength) + return self.device.ctrl_transfer( + bmRequestType, bRequest, wValue, wIndex, data_or_wLength + ) except usb.USBError: # Could not communicate with BlinkStick backend # attempt to find it again based on serial if self._refresh_device(): - return self.device.ctrl_transfer(bmRequestType, bRequest, wValue, wIndex, data_or_wLength) + return self.device.ctrl_transfer( + bmRequestType, bRequest, wValue, wIndex, data_or_wLength + ) else: - raise BlinkStickException("Could not communicate with BlinkStick {0} - it may have been removed".format(self.serial)) + raise BlinkStickException( + "Could not communicate with BlinkStick {0} - it may have been removed".format( + self.serial + ) + ) def get_serial(self) -> str: return self._usb_get_string(3) - def get_manufacturer(self)-> str: + def get_manufacturer(self) -> str: return self._usb_get_string(1) def get_version_attribute(self) -> int: @@ -86,4 +102,8 @@ def _usb_get_string(self, index: int) -> str: if self._refresh_device(): return str(usb.util.get_string(self.device, index, 1033)) else: - raise BlinkStickException("Could not communicate with BlinkStick {0} - it may have been removed".format(self.serial)) \ No newline at end of file + raise BlinkStickException( + "Could not communicate with BlinkStick {0} - it may have been removed".format( + self.serial + ) + ) diff --git a/src/blinkstick/backends/win32.py b/src/blinkstick/backends/win32.py index a383cdc..a41c382 100644 --- a/src/blinkstick/backends/win32.py +++ b/src/blinkstick/backends/win32.py @@ -17,17 +17,17 @@ def __init__(self, device=None): if device: self.device.open() self.reports = self.device.find_feature_reports() - self.serial = self.get_serial() + self.serial = self.get_serial() @staticmethod def find_by_serial(serial: str) -> list | None: - devices = [d for d in Win32Backend.find_blinksticks() - if d.serial_number == serial] + devices = [ + d for d in Win32Backend.find_blinksticks() if d.serial_number == serial + ] if len(devices) > 0: return devices - def _refresh_device(self): # TODO This is weird semantics. fix up return values to be more sensible if not self.serial: @@ -40,7 +40,9 @@ def _refresh_device(self): @staticmethod def find_blinksticks(find_all: bool = True): - devices = hid.HidDeviceFilter(vendor_id =VENDOR_ID, product_id =PRODUCT_ID).get_devices() + devices = hid.HidDeviceFilter( + vendor_id=VENDOR_ID, product_id=PRODUCT_ID + ).get_devices() if find_all: return devices elif len(devices) > 0: @@ -48,19 +50,28 @@ def find_blinksticks(find_all: bool = True): else: return None - - def control_transfer(self, bmRequestType, bRequest, wValue, wIndex, data_or_wLength): + def control_transfer( + self, bmRequestType, bRequest, wValue, wIndex, data_or_wLength + ): if bmRequestType == 0x20: if sys.version_info[0] < 3: - data = (c_ubyte * len(data_or_wLength))(*[c_ubyte(ord(c)) for c in data_or_wLength]) + data = (c_ubyte * len(data_or_wLength))( + *[c_ubyte(ord(c)) for c in data_or_wLength] + ) else: - data = (c_ubyte * len(data_or_wLength))(*[c_ubyte(c) for c in data_or_wLength]) + data = (c_ubyte * len(data_or_wLength))( + *[c_ubyte(c) for c in data_or_wLength] + ) data[0] = wValue if not self.device.send_feature_report(data): if self._refresh_device(): self.device.send_feature_report(data) else: - raise BlinkStickException("Could not communicate with BlinkStick {0} - it may have been removed".format(self.serial)) + raise BlinkStickException( + "Could not communicate with BlinkStick {0} - it may have been removed".format( + self.serial + ) + ) elif bmRequestType == 0x80 | 0x20: return self.reports[wValue - 1].get() @@ -75,4 +86,4 @@ def get_version_attribute(self) -> int: return int(self.device.version_number) def get_description(self) -> str: - return str(self.device.product_name) \ No newline at end of file + return str(self.device.product_name) diff --git a/src/blinkstick/blinkstick.py b/src/blinkstick/blinkstick.py index 5238fee..5f9079d 100644 --- a/src/blinkstick/blinkstick.py +++ b/src/blinkstick/blinkstick.py @@ -11,7 +11,7 @@ remap_color, remap_rgb_value, remap_rgb_value_reverse, - ColorFormat + ColorFormat, ) from blinkstick.constants import VENDOR_ID, PRODUCT_ID, BlinkStickVariant from blinkstick.exceptions import BlinkStickException @@ -49,7 +49,7 @@ class BlinkStick: backend: USBBackend bs_serial: str - def __init__(self, device=None, error_reporting: bool=True): + def __init__(self, device=None, error_reporting: bool = True): """ Constructor for the class. @@ -64,7 +64,6 @@ def __init__(self, device=None, error_reporting: bool=True): self.backend = USBBackend(device) self.bs_serial = self.get_serial() - def get_serial(self) -> str: """ Returns the serial number of backend.:: @@ -133,7 +132,16 @@ def set_error_reporting(self, error_reporting: bool) -> None: """ self.error_reporting = error_reporting - def set_color(self, channel: int = 0, index: int = 0, red: int = 0, green: int = 0, blue: int = 0, name: str | None = None, hex: str | None = None) -> None : + def set_color( + self, + channel: int = 0, + index: int = 0, + red: int = 0, + green: int = 0, + blue: int = 0, + name: str | None = None, + hex: str | None = None, + ) -> None: """ Set the color to the backend as RGB @@ -153,7 +161,9 @@ def set_color(self, channel: int = 0, index: int = 0, red: int = 0, green: int = @param hex: Specify color using hexadecimal color value e.g. '#FF3366' """ - red, green, blue = self._determine_rgb(red=red, green=green, blue=blue, name=name, hex=hex) + red, green, blue = self._determine_rgb( + red=red, green=green, blue=blue, name=name, hex=hex + ) r = int(round(red, 3)) g = int(round(green, 3)) @@ -177,7 +187,14 @@ def set_color(self, channel: int = 0, index: int = 0, red: int = 0, green: int = except Exception: pass - def _determine_rgb(self, red: int = 0, green: int = 0, blue: int = 0, name: str | None = None, hex: str | None = None) -> tuple[int, int, int]: + def _determine_rgb( + self, + red: int = 0, + green: int = 0, + blue: int = 0, + name: str | None = None, + hex: str | None = None, + ) -> tuple[int, int, int]: try: if name: @@ -201,9 +218,15 @@ def _determine_rgb(self, red: int = 0, green: int = 0, blue: int = 0, name: str def _get_color_rgb(self, index: int = 0) -> tuple[int, int, int]: if index == 0: - device_bytes = self.backend.control_transfer(0x80 | 0x20, 0x1, 0x0001, 0, 33) + device_bytes = self.backend.control_transfer( + 0x80 | 0x20, 0x1, 0x0001, 0, 33 + ) if self.inverse: - return [255 - device_bytes[1], 255 - device_bytes[2], 255 - device_bytes[3]] + return [ + 255 - device_bytes[1], + 255 - device_bytes[2], + 255 - device_bytes[3], + ] else: return [device_bytes[1], device_bytes[2], device_bytes[3]] else: @@ -213,9 +236,14 @@ def _get_color_rgb(self, index: int = 0) -> tuple[int, int, int]: def _get_color_hex(self, index: int = 0) -> str: r, g, b = self._get_color_rgb(index) - return '#%02x%02x%02x' % (r, g, b) + return "#%02x%02x%02x" % (r, g, b) - def get_color(self, index: int=0, color_mode: ColorFormat = ColorFormat.RGB, color_format: str=None) -> tuple[int, int, int] | str: + def get_color( + self, + index: int = 0, + color_mode: ColorFormat = ColorFormat.RGB, + color_format: str = None, + ) -> tuple[int, int, int] | str: """ Get the current backend color in the defined format. @@ -245,7 +273,10 @@ def get_color(self, index: int=0, color_mode: ColorFormat = ColorFormat.RGB, col # if color_format is specified, then raise a DeprecationWarning, but attempt to convert it to a ColorFormat enum # if it's not possible, then default to ColorFormat.RGB, in line with the previous behavior if color_format: - warnings.warn("color_format is deprecated, please use color_mode instead", DeprecationWarning) + warnings.warn( + "color_format is deprecated, please use color_mode instead", + DeprecationWarning, + ) try: color_mode = ColorFormat.from_name(color_format) except ValueError: @@ -253,7 +284,7 @@ def get_color(self, index: int=0, color_mode: ColorFormat = ColorFormat.RGB, col color_funcs = { ColorFormat.RGB: self._get_color_rgb, - ColorFormat.HEX: self._get_color_hex + ColorFormat.HEX: self._get_color_hex, } return color_funcs.get(color_mode, ColorFormat.RGB)(index) @@ -311,9 +342,11 @@ def get_led_data(self, count: int) -> list[int]: report_id, max_leds = self._determine_report_id(count) - device_bytes = self.backend.control_transfer(0x80 | 0x20, 0x1, report_id, 0, max_leds * 3 + 2) + device_bytes = self.backend.control_transfer( + 0x80 | 0x20, 0x1, report_id, 0, max_leds * 3 + 2 + ) - return device_bytes[2: 2 + count * 3] + return device_bytes[2 : 2 + count * 3] def set_mode(self, mode: int) -> None: """ @@ -368,7 +401,6 @@ def set_led_count(self, count: int) -> None: self.backend.control_transfer(0x20, 0x9, 0x81, 0, control_string) - def get_led_count(self) -> int: """ Get number of LEDs for supported devices @@ -474,7 +506,19 @@ def turn_off(self) -> None: """ self.set_color() - def pulse(self, channel: int = 0, index: int = 0, red: int = 0, green: int = 0, blue: int = 0, name: str | None = None, hex: str | None = None, repeats: int = 1, duration: int = 1000, steps: int = 50) -> None: + def pulse( + self, + channel: int = 0, + index: int = 0, + red: int = 0, + green: int = 0, + blue: int = 0, + name: str | None = None, + hex: str | None = None, + repeats: int = 1, + duration: int = 1000, + steps: int = 50, + ) -> None: """ Morph to the specified color from black and back again. @@ -501,10 +545,39 @@ def pulse(self, channel: int = 0, index: int = 0, red: int = 0, green: int = 0, """ self.turn_off() for x in range(repeats): - self.morph(channel=channel, index=index, red=red, green=green, blue=blue, name=name, hex=hex, duration=duration, steps=steps) - self.morph(channel=channel, index=index, red=0, green=0, blue=0, duration=duration, steps=steps) - - def blink(self, channel: int = 0, index: int = 0, red: int = 0, green: int = 0, blue: int = 0, name: str | None = None, hex: str | None = None, repeats: int = 1, delay: int = 500) -> None: + self.morph( + channel=channel, + index=index, + red=red, + green=green, + blue=blue, + name=name, + hex=hex, + duration=duration, + steps=steps, + ) + self.morph( + channel=channel, + index=index, + red=0, + green=0, + blue=0, + duration=duration, + steps=steps, + ) + + def blink( + self, + channel: int = 0, + index: int = 0, + red: int = 0, + green: int = 0, + blue: int = 0, + name: str | None = None, + hex: str | None = None, + repeats: int = 1, + delay: int = 500, + ) -> None: """ Blink the specified color. @@ -531,11 +604,30 @@ def blink(self, channel: int = 0, index: int = 0, red: int = 0, green: int = 0, for x in range(repeats): if x: time.sleep(ms_delay) - self.set_color(channel=channel, index=index, red=red, green=green, blue=blue, name=name, hex=hex) + self.set_color( + channel=channel, + index=index, + red=red, + green=green, + blue=blue, + name=name, + hex=hex, + ) time.sleep(ms_delay) self.set_color(channel=channel, index=index) - def morph(self, channel: int = 0, index: int = 0, red: int = 0, green: int = 0, blue: int = 0, name: str | None = None, hex: str | None = None, duration: int = 1000, steps: int = 50) -> None: + def morph( + self, + channel: int = 0, + index: int = 0, + red: int = 0, + green: int = 0, + blue: int = 0, + name: str | None = None, + hex: str | None = None, + duration: int = 1000, + steps: int = 50, + ) -> None: """ Morph to the specified color. @@ -559,11 +651,17 @@ def morph(self, channel: int = 0, index: int = 0, red: int = 0, green: int = 0, @param steps: Number of gradient steps (default 50) """ - r_end, g_end, b_end = self._determine_rgb(red=red, green=green, blue=blue, name=name, hex=hex) + r_end, g_end, b_end = self._determine_rgb( + red=red, green=green, blue=blue, name=name, hex=hex + ) # descale the above values - r_end, g_end, b_end = remap_rgb_value_reverse((r_end, g_end, b_end), self.max_rgb_value) + r_end, g_end, b_end = remap_rgb_value_reverse( + (r_end, g_end, b_end), self.max_rgb_value + ) - r_start, g_start, b_start = remap_rgb_value_reverse(self._get_color_rgb(index), self.max_rgb_value) + r_start, g_start, b_start = remap_rgb_value_reverse( + self._get_color_rgb(index), self.max_rgb_value + ) if r_start > 255 or g_start > 255 or b_start > 255: r_start = 0 @@ -583,12 +681,16 @@ def morph(self, channel: int = 0, index: int = 0, red: int = 0, green: int = 0, ms_delay = float(duration) / float(1000 * steps) - self.set_color(channel=channel, index=index, red=r_start, green=g_start, blue=b_start) + self.set_color( + channel=channel, index=index, red=r_start, green=g_start, blue=b_start + ) for grad in gradient: grad_r, grad_g, grad_b = grad - self.set_color(channel=channel, index=index, red=grad_r, green=grad_g, blue=grad_b) + self.set_color( + channel=channel, index=index, red=grad_r, green=grad_g, blue=grad_b + ) time.sleep(ms_delay) self.set_color(channel=channel, index=index, red=r_end, green=g_end, blue=b_end) @@ -675,8 +777,14 @@ class BlinkStickPro: data: list[list[list[int]]] bstick: BlinkStick | None - - def __init__(self, r_led_count: int = 0, g_led_count: int = 0, b_led_count: int = 0, delay: float = 0.002, max_rgb_value: int = 255): + def __init__( + self, + r_led_count: int = 0, + g_led_count: int = 0, + b_led_count: int = 0, + delay: float = 0.002, + max_rgb_value: int = 255, + ): """ Initialize BlinkStickPro class. @@ -718,7 +826,15 @@ def __init__(self, r_led_count: int = 0, g_led_count: int = 0, b_led_count: int self.bstick = None - def set_color(self, channel: int, index: int, r: int, g: int, b: int, remap_values: bool = True) -> None: + def set_color( + self, + channel: int, + index: int, + r: int, + g: int, + b: int, + remap_values: bool = True, + ) -> None: """ Set the color of a single pixel @@ -822,6 +938,7 @@ def send_data_all(self) -> None: if self.b_led_count > 0: self.send_data(2) + class BlinkStickProMatrix(BlinkStickPro): """ BlinkStickProMatrix class is specifically designed to control the individually @@ -863,7 +980,17 @@ class BlinkStickProMatrix(BlinkStickPro): cols: int matrix_data: list[list[int]] - def __init__(self, r_columns: int = 0, r_rows: int = 0, g_columns: int = 0, g_rows: int = 0, b_columns: int = 0, b_rows: int = 0, delay: float = 0.002, max_rgb_value: int = 255): + def __init__( + self, + r_columns: int = 0, + r_rows: int = 0, + g_columns: int = 0, + g_rows: int = 0, + b_columns: int = 0, + b_rows: int = 0, + delay: float = 0.002, + max_rgb_value: int = 255, + ): """ Initialize BlinkStickProMatrix class. @@ -889,7 +1016,13 @@ def __init__(self, r_columns: int = 0, r_rows: int = 0, g_columns: int = 0, g_ro self.b_columns = b_columns self.b_rows = b_rows - super(BlinkStickProMatrix, self).__init__(r_led_count=r_leds, g_led_count=g_leds, b_led_count=b_leds, delay=delay, max_rgb_value=max_rgb_value) + super(BlinkStickProMatrix, self).__init__( + r_led_count=r_leds, + g_led_count=g_leds, + b_led_count=b_leds, + delay=delay, + max_rgb_value=max_rgb_value, + ) self.rows = max(r_rows, g_rows, b_rows) self.cols = r_columns + g_columns + b_columns @@ -900,7 +1033,9 @@ def __init__(self, r_columns: int = 0, r_rows: int = 0, g_columns: int = 0, g_ro for i in range(0, self.rows * self.cols): self.matrix_data.append([0, 0, 0]) - def set_color(self, x: int, y: int, r: int, g: int, b: int, remap_values: bool = True) -> None: + def set_color( + self, x: int, y: int, r: int, g: int, b: int, remap_values: bool = True + ) -> None: """ Set the color of a single pixel in the internal framebuffer. @@ -1121,7 +1256,9 @@ def number(self, x: int, y: int, n: int, r: int, g: int, b: int) -> None: self.set_color(x + 2, y + 1, r, g, b) self.set_color(x + 2, y + 3, r, g, b) - def rectangle(self, x1: int, y1: int, x2: int, y2: int, r: int, g: int, b: int) -> None: + def rectangle( + self, x1: int, y1: int, x2: int, y2: int, r: int, g: int, b: int + ) -> None: """ Draw a rectangle with it's corners at x1:y1 and x2:y2 @@ -1146,7 +1283,9 @@ def rectangle(self, x1: int, y1: int, x2: int, y2: int, r: int, g: int, b: int) self.line(x2, y1, x2, y2, r, g, b) self.line(x1, y2, x2, y2, r, g, b) - def line(self, x1: int, y1: int, x2: int, y2: int, r: int, g: int, b: int) -> list[tuple[int, int]]: + def line( + self, x1: int, y1: int, x2: int, y2: int, r: int, g: int, b: int + ) -> list[tuple[int, int]]: """ Draw a line from x1:y1 and x2:y2 @@ -1187,11 +1326,11 @@ def line(self, x1: int, y1: int, x2: int, y2: int, r: int, g: int, b: int) -> li y_step = -1 for x in range(x1, x2 + 1): if is_steep: - #print y, "~", x + # print y, "~", x self.set_color(y, x, r, g, b) points.append((y, x)) else: - #print x, " ", y + # print x, " ", y self.set_color(x, y, r, g, b) points.append((x, y)) error -= delta_y @@ -1238,18 +1377,21 @@ def send_data(self, channel: int) -> None: self.data[channel] = [] - #slice the huge array to individual packets + # slice the huge array to individual packets for y in range(0, self.rows): start = y * self.cols + start_col end = y * self.cols + end_col - self.data[channel].extend(self.matrix_data[start: end]) + self.data[channel].extend(self.matrix_data[start:end]) super(BlinkStickProMatrix, self).send_data(channel) + def _find_blicksticks(find_all: bool = True) -> list[BlinkStick] | None: if sys.platform == "win32": - devices = hid.HidDeviceFilter(vendor_id =VENDOR_ID, product_id =PRODUCT_ID).get_devices() + devices = hid.HidDeviceFilter( + vendor_id=VENDOR_ID, product_id=PRODUCT_ID + ).get_devices() if find_all: return devices elif len(devices) > 0: @@ -1258,7 +1400,9 @@ def _find_blicksticks(find_all: bool = True) -> list[BlinkStick] | None: return None else: - return usb.core.find(find_all=find_all, idVendor=VENDOR_ID, idProduct=PRODUCT_ID) + return usb.core.find( + find_all=find_all, idVendor=VENDOR_ID, idProduct=PRODUCT_ID + ) def find_all() -> list[BlinkStick]: diff --git a/src/blinkstick/colors.py b/src/blinkstick/colors.py index 325c6fb..12ab0f7 100644 --- a/src/blinkstick/colors.py +++ b/src/blinkstick/colors.py @@ -1,7 +1,8 @@ import re from enum import Enum, auto -HEX_COLOR_RE = re.compile(r'^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$') +HEX_COLOR_RE = re.compile(r"^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$") + class Color(Enum): ALICEBLUE = "#f0f8ff" @@ -230,8 +231,8 @@ def normalize_hex(hex_value: str) -> str: except AttributeError: raise ValueError("'%s' is not a valid hexadecimal color value." % hex_value) if len(hex_digits) == 3: - hex_digits = ''.join([2 * s for s in hex_digits]) - return '#%s' % hex_digits.lower() + hex_digits = "".join([2 * s for s in hex_digits]) + return "#%s" % hex_digits.lower() def hex_to_rgb(hex_value: str) -> tuple[int, int, int]: @@ -275,7 +276,9 @@ def name_to_rgb(name: str) -> tuple[int, int, int]: return hex_to_rgb(name_to_hex(name)) -def remap(value: int, left_min: int, left_max: int, right_min: int, right_max: int) -> int: +def remap( + value: int, left_min: int, left_max: int, right_min: int, right_max: int +) -> int: """ Remap a value from one range to another. """ @@ -296,17 +299,25 @@ def remap_color(value: int, max_value: int) -> int: return remap(value, 0, 255, 0, max_value) -def remap_color_reverse(value: int, max_value : int) -> int: +def remap_color_reverse(value: int, max_value: int) -> int: return remap(value, 0, max_value, 0, 255) -def remap_rgb_value(rgb_val: tuple[int, int, int], max_value: int) -> tuple[int, int, int]: - return (remap_color(rgb_val[0], max_value), - remap_color(rgb_val[1], max_value), - remap_color(rgb_val[2], max_value)) - - -def remap_rgb_value_reverse(rgb_val: tuple[int, int, int], max_value: int) -> tuple[int, int, int]: - return (remap_color_reverse(rgb_val[0], max_value), - remap_color_reverse(rgb_val[1], max_value), - remap_color_reverse(rgb_val[2], max_value)) +def remap_rgb_value( + rgb_val: tuple[int, int, int], max_value: int +) -> tuple[int, int, int]: + return ( + remap_color(rgb_val[0], max_value), + remap_color(rgb_val[1], max_value), + remap_color(rgb_val[2], max_value), + ) + + +def remap_rgb_value_reverse( + rgb_val: tuple[int, int, int], max_value: int +) -> tuple[int, int, int]: + return ( + remap_color_reverse(rgb_val[0], max_value), + remap_color_reverse(rgb_val[1], max_value), + remap_color_reverse(rgb_val[2], max_value), + ) diff --git a/src/blinkstick/constants.py b/src/blinkstick/constants.py index 8cee3b8..14348d3 100644 --- a/src/blinkstick/constants.py +++ b/src/blinkstick/constants.py @@ -2,8 +2,9 @@ from enum import Enum -VENDOR_ID = 0x20a0 -PRODUCT_ID = 0x41e5 +VENDOR_ID = 0x20A0 +PRODUCT_ID = 0x41E5 + class BlinkStickVariant(Enum): UNKNOWN = (0, "Unknown") @@ -23,7 +24,9 @@ def description(self) -> str: return self._value_[1] @staticmethod - def identify(major_version: int, version_attribute: int | None) -> "BlinkStickVariant": + def identify( + major_version: int, version_attribute: int | None + ) -> "BlinkStickVariant": if major_version == 1: return BlinkStickVariant.BLINKSTICK elif major_version == 2: @@ -37,4 +40,4 @@ def identify(major_version: int, version_attribute: int | None) -> "BlinkStickVa return BlinkStickVariant.BLINKSTICK_NANO elif version_attribute == 0x203: return BlinkStickVariant.BLINKSTICK_FLEX - return BlinkStickVariant.UNKNOWN \ No newline at end of file + return BlinkStickVariant.UNKNOWN diff --git a/src/blinkstick/main.py b/src/blinkstick/main.py index 5d9b87f..62a7c40 100644 --- a/src/blinkstick/main.py +++ b/src/blinkstick/main.py @@ -7,23 +7,25 @@ from .blinkstick import get_blinkstick_package_version, find_all, find_by_serial from .constants import BlinkStickVariant + logging.basicConfig() class IndentedHelpFormatterWithNL(IndentedHelpFormatter): def format_description(self, description): - if not description: return "" + if not description: + return "" desc_width = self.width - self.current_indent indent = " " * self.current_indent # the above is still the same - bits = description.split('\n') + bits = description.split("\n") formatted_bits = [ - textwrap.fill(bit, - desc_width, - initial_indent=indent, - subsequent_indent=indent) - for bit in bits] + textwrap.fill( + bit, desc_width, initial_indent=indent, subsequent_indent=indent + ) + for bit in bits + ] result = "\n".join(formatted_bits) + "\n" return result @@ -62,17 +64,22 @@ def format_option(self, option): for para in help_text.split("\n"): help_lines.extend(textwrap.wrap(para, self.help_width)) # Everything is the same after here - result.append("%*s%s\n" % ( - indent_first, "", help_lines[0])) - result.extend(["%*s%s\n" % (self.help_position, "", line) - for line in help_lines[1:]]) + result.append("%*s%s\n" % (indent_first, "", help_lines[0])) + result.extend( + ["%*s%s\n" % (self.help_position, "", line) for line in help_lines[1:]] + ) elif opts[-1] != "\n": result.append("\n") return "".join(result) def format_usage(self, usage): - return "BlinkStick control script %s\n(c) Agile Innovative Ltd 2013-2014\n\n%s" % ( - get_blinkstick_package_version(), IndentedHelpFormatter.format_usage(self, usage)) + return ( + "BlinkStick control script %s\n(c) Agile Innovative Ltd 2013-2014\n\n%s" + % ( + get_blinkstick_package_version(), + IndentedHelpFormatter.format_usage(self, usage), + ) + ) def print_info(stick): @@ -100,121 +107,169 @@ def main(): global options global sticks - parser = OptionParser(usage="usage: %prog [options] [color]", - formatter=IndentedHelpFormatterWithNL() + parser = OptionParser( + usage="usage: %prog [options] [color]", formatter=IndentedHelpFormatterWithNL() ) - parser.add_option("-i", "--info", - action="store_true", dest="info", - help="Display BlinkStick info") - - parser.add_option("-s", "--serial", - dest="serial", - help="Select backend by serial number. If unspecified, action will be performed on all BlinkSticks.") + parser.add_option( + "-i", "--info", action="store_true", dest="info", help="Display BlinkStick info" + ) - parser.add_option("-v", "--verbose", - action="store_true", dest="verbose", - help="Display debug output") + parser.add_option( + "-s", + "--serial", + dest="serial", + help="Select backend by serial number. If unspecified, action will be performed on all BlinkSticks.", + ) + parser.add_option( + "-v", + "--verbose", + action="store_true", + dest="verbose", + help="Display debug output", + ) - group = OptionGroup(parser, "Change color", - "These options control the color of the backend ") + group = OptionGroup( + parser, "Change color", "These options control the color of the backend " + ) - group.add_option("--channel", - default=0, dest="channel", - help="Select channel. Applies only to BlinkStick Pro.") + group.add_option( + "--channel", + default=0, + dest="channel", + help="Select channel. Applies only to BlinkStick Pro.", + ) - group.add_option("--index", - default=0, dest="index", - help="Select index. Applies only to BlinkStick Pro.") + group.add_option( + "--index", + default=0, + dest="index", + help="Select index. Applies only to BlinkStick Pro.", + ) - group.add_option("--brightness", - default=100, dest="limit", - help="Limit the brightness of the color 0..100") + group.add_option( + "--brightness", + default=100, + dest="limit", + help="Limit the brightness of the color 0..100", + ) - group.add_option("--limit", - default=100, dest="limit", - help="Alias to --brightness option") + group.add_option( + "--limit", default=100, dest="limit", help="Alias to --brightness option" + ) - group.add_option("--set-color", - dest="color", - help="Set the color for the backend. This can also be the last argument for the script. " - "The value can either be a named color, hex value, 'random' or 'off'.\n\n" - "CSS color names are defined http://www.w3.org/TR/css3-color/ e.g. red, green, blue. " - "Specify color using hexadecimal color value e.g. 'FF3366'") - group.add_option("--inverse", - action="store_true", dest="inverse", - help="Control BlinkSticks in inverse mode") + group.add_option( + "--set-color", + dest="color", + help="Set the color for the backend. This can also be the last argument for the script. " + "The value can either be a named color, hex value, 'random' or 'off'.\n\n" + "CSS color names are defined http://www.w3.org/TR/css3-color/ e.g. red, green, blue. " + "Specify color using hexadecimal color value e.g. 'FF3366'", + ) + group.add_option( + "--inverse", + action="store_true", + dest="inverse", + help="Control BlinkSticks in inverse mode", + ) - group.add_option("--set-led-count", - dest="led_count", - help="Set the number of LEDs to control for supported devices.") + group.add_option( + "--set-led-count", + dest="led_count", + help="Set the number of LEDs to control for supported devices.", + ) parser.add_option_group(group) - group = OptionGroup(parser, "Control animations", - "These options will blink, morph or pulse selected color. ") + group = OptionGroup( + parser, + "Control animations", + "These options will blink, morph or pulse selected color. ", + ) - group.add_option("--blink", - dest="blink", - action='store_true', - help="Blink LED (requires --set-color or color set as last argument, and optionally --delay)") + group.add_option( + "--blink", + dest="blink", + action="store_true", + help="Blink LED (requires --set-color or color set as last argument, and optionally --delay)", + ) - group.add_option("--pulse", - dest="pulse", - action='store_true', - help="Pulse LED (requires --set-color or color set as last argument, and optionally --duration).") + group.add_option( + "--pulse", + dest="pulse", + action="store_true", + help="Pulse LED (requires --set-color or color set as last argument, and optionally --duration).", + ) - group.add_option("--morph", - dest="morph", - action='store_true', - help="Morph to specified color (requires --set-color or color set as last argument, and optionally --duration).") + group.add_option( + "--morph", + dest="morph", + action="store_true", + help="Morph to specified color (requires --set-color or color set as last argument, and optionally --duration).", + ) - group.add_option("--duration", - dest="duration", - default=1000, - help="Set duration of transition in milliseconds (use with --morph and --pulse).") + group.add_option( + "--duration", + dest="duration", + default=1000, + help="Set duration of transition in milliseconds (use with --morph and --pulse).", + ) - group.add_option("--delay", - dest="delay", - default=500, - help="Set time in milliseconds to light LED for (use with --blink).") + group.add_option( + "--delay", + dest="delay", + default=500, + help="Set time in milliseconds to light LED for (use with --blink).", + ) - group.add_option("--repeats", - dest="repeats", - default=1, - help="Number of repetitions (use with --blink and --pulse).") + group.add_option( + "--repeats", + dest="repeats", + default=1, + help="Number of repetitions (use with --blink and --pulse).", + ) parser.add_option_group(group) - group = OptionGroup(parser, "Device data and behaviour", - "These options will change backend mode and data stored internally. ") + group = OptionGroup( + parser, + "Device data and behaviour", + "These options will change backend mode and data stored internally. ", + ) - group.add_option("--set-mode", - default=0, dest="mode", - help="Set mode for BlinkStick Pro:\n\n 0 - default\n\n 1 - inverse\n\n 2 - ws2812\n\n 3 - ws2812 mirror") + group.add_option( + "--set-mode", + default=0, + dest="mode", + help="Set mode for BlinkStick Pro:\n\n 0 - default\n\n 1 - inverse\n\n 2 - ws2812\n\n 3 - ws2812 mirror", + ) - group.add_option("--set-infoblock1", - dest="infoblock1", - help="Set the first info block for the backend.") + group.add_option( + "--set-infoblock1", + dest="infoblock1", + help="Set the first info block for the backend.", + ) - group.add_option("--set-infoblock2", - dest="infoblock2", - help="Set the second info block for the backend.") + group.add_option( + "--set-infoblock2", + dest="infoblock2", + help="Set the second info block for the backend.", + ) parser.add_option_group(group) - group = OptionGroup(parser, "Advanced options", - "") + group = OptionGroup(parser, "Advanced options", "") - group.add_option("--add-udev-rule", - action="store_true", dest="udev", - help="Add udev rule to access BlinkSticks without root permissions. Must be run as root.") + group.add_option( + "--add-udev-rule", + action="store_true", + dest="udev", + help="Add udev rule to access BlinkSticks without root permissions. Must be run as root.", + ) parser.add_option_group(group) - - (options, args) = parser.parse_args() if options.serial is None: @@ -226,19 +281,23 @@ def main(): print("BlinkStick with serial number " + options.backend + " not found...") return 64 - #Global action + # Global action if options.udev: try: filename = "/etc/udev/rules.d/85-blinkstick.rules" - file = open(filename, 'w') - file.write('SUBSYSTEM=="usb", ATTR{idVendor}=="20a0", ATTR{idProduct}=="41e5", MODE:="0666"') + file = open(filename, "w") + file.write( + 'SUBSYSTEM=="usb", ATTR{idVendor}=="20a0", ATTR{idProduct}=="41e5", MODE:="0666"' + ) file.close() print("Rule added to {0}".format(filename)) except IOError as e: print(str(e)) - print("Make sure you run this script as root: sudo blinkstick --add-udev-rule") + print( + "Make sure you run this script as root: sudo blinkstick --add-udev-rule" + ) return 64 print("Reboot your computer for changes to take effect") @@ -252,7 +311,7 @@ def main(): stick.set_error_reporting(False) - #Actions here work on all BlinkSticks + # Actions here work on all BlinkSticks for stick in sticks: if options.infoblock1: stick.set_info_block1(options.infoblock1) @@ -261,7 +320,12 @@ def main(): stick.set_info_block2(options.infoblock2) if options.mode: - if options.mode == "0" or options.mode == "1" or options.mode == "2" or options.mode == "3": + if ( + options.mode == "0" + or options.mode == "1" + or options.mode == "2" + or options.mode == "3" + ): stick.set_mode(int(options.mode)) else: print("Error: Invalid mode parameter value") @@ -284,43 +348,42 @@ def main(): # determine color fargs = {} - if color.startswith('#'): - fargs['hex'] = color + if color.startswith("#"): + fargs["hex"] = color elif color == "random": - fargs['name'] = 'random' + fargs["name"] = "random" elif color == "off": - fargs['hex'] = "#000000" + fargs["hex"] = "#000000" else: if len(color) == 6: # If color contains 6 chars check if it's hex try: int(color, 16) - fargs['hex'] = "#" + color + fargs["hex"] = "#" + color except: - fargs['name'] = color + fargs["name"] = color else: - fargs['name'] = color + fargs["name"] = color - fargs['index'] = int(options.index) - fargs['channel'] = int(options.channel) + fargs["index"] = int(options.index) + fargs["channel"] = int(options.channel) # handle blink/pulse/morph func = stick.set_color if options.blink: func = stick.blink - fargs['delay'] = options.delay - fargs['repeats'] = int(options.repeats) + fargs["delay"] = options.delay + fargs["repeats"] = int(options.repeats) elif options.pulse: func = stick.pulse - fargs['duration'] = options.duration - fargs['repeats'] = int(options.repeats) + fargs["duration"] = options.duration + fargs["repeats"] = int(options.repeats) elif options.morph: func = stick.morph - fargs['duration'] = options.duration + fargs["duration"] = options.duration func(**fargs) - else: parser.print_help() return 0 diff --git a/tests/clients/test_blinkstick.py b/tests/clients/test_blinkstick.py index fb3e9ce..ad6fc09 100644 --- a/tests/clients/test_blinkstick.py +++ b/tests/clients/test_blinkstick.py @@ -14,28 +14,45 @@ def test_instantiate(): bs = BlinkStick() assert bs is not None -@pytest.mark.parametrize("serial, version_attribute, expected_variant, expected_variant_value", [ - ("BS12345-1.0", 0x0000, BlinkStickVariant.BLINKSTICK, 1), - ("BS12345-2.0", 0x0000, BlinkStickVariant.BLINKSTICK_PRO, 2), - ("BS12345-3.0", 0x200, BlinkStickVariant.BLINKSTICK_SQUARE, 4), # major version 3, version attribute 0x200 is BlinkStickSquare - ("BS12345-3.0", 0x201, BlinkStickVariant.BLINKSTICK_STRIP, 3), # major version 3 is BlinkStickStrip - ("BS12345-3.0", 0x202, BlinkStickVariant.BLINKSTICK_NANO, 5), - ("BS12345-3.0", 0x203, BlinkStickVariant.BLINKSTICK_FLEX, 6), - ("BS12345-4.0", 0x0000, BlinkStickVariant.UNKNOWN, 0), - ("BS12345-3.0", 0x9999, BlinkStickVariant.UNKNOWN, 0), - ("BS12345-0.0", 0x0000, BlinkStickVariant.UNKNOWN, 0), -], ids=[ - "v1==BlinkStick", - "v2==BlinkStickPro", - "v3,0x200==BlinkStickSquare", - "v3,0x201==BlinkStickStrip", - "v3,0x202==BlinkStickNano", - "v3,0x203==BlinkStickFlex", - "v4==Unknown", - "v3,Unknown==Unknown", - "v0,0==Unknown" -]) -def test_get_variant(make_blinkstick, serial, version_attribute, expected_variant, expected_variant_value): + +@pytest.mark.parametrize( + "serial, version_attribute, expected_variant, expected_variant_value", + [ + ("BS12345-1.0", 0x0000, BlinkStickVariant.BLINKSTICK, 1), + ("BS12345-2.0", 0x0000, BlinkStickVariant.BLINKSTICK_PRO, 2), + ( + "BS12345-3.0", + 0x200, + BlinkStickVariant.BLINKSTICK_SQUARE, + 4, + ), # major version 3, version attribute 0x200 is BlinkStickSquare + ( + "BS12345-3.0", + 0x201, + BlinkStickVariant.BLINKSTICK_STRIP, + 3, + ), # major version 3 is BlinkStickStrip + ("BS12345-3.0", 0x202, BlinkStickVariant.BLINKSTICK_NANO, 5), + ("BS12345-3.0", 0x203, BlinkStickVariant.BLINKSTICK_FLEX, 6), + ("BS12345-4.0", 0x0000, BlinkStickVariant.UNKNOWN, 0), + ("BS12345-3.0", 0x9999, BlinkStickVariant.UNKNOWN, 0), + ("BS12345-0.0", 0x0000, BlinkStickVariant.UNKNOWN, 0), + ], + ids=[ + "v1==BlinkStick", + "v2==BlinkStickPro", + "v3,0x200==BlinkStickSquare", + "v3,0x201==BlinkStickStrip", + "v3,0x202==BlinkStickNano", + "v3,0x203==BlinkStickFlex", + "v4==Unknown", + "v3,Unknown==Unknown", + "v0,0==Unknown", + ], +) +def test_get_variant( + make_blinkstick, serial, version_attribute, expected_variant, expected_variant_value +): bs = make_blinkstick() bs.get_serial = MagicMock(return_value=serial) bs.backend.get_version_attribute = MagicMock(return_value=version_attribute) @@ -43,23 +60,27 @@ def test_get_variant(make_blinkstick, serial, version_attribute, expected_varian assert bs.get_variant().value == expected_variant_value -@pytest.mark.parametrize("expected_variant, expected_name", [ - (BlinkStickVariant.BLINKSTICK, "BlinkStick"), - (BlinkStickVariant.BLINKSTICK_PRO, "BlinkStick Pro"), - (BlinkStickVariant.BLINKSTICK_STRIP, "BlinkStick Strip"), - (BlinkStickVariant.BLINKSTICK_SQUARE, "BlinkStick Square"), - (BlinkStickVariant.BLINKSTICK_NANO, "BlinkStick Nano"), - (BlinkStickVariant.BLINKSTICK_FLEX, "BlinkStick Flex"), - (BlinkStickVariant.UNKNOWN, "Unknown"), -], ids=[ - "1==BlinkStick", - "2==BlinkStickPro", - "3==BlinkStickStrip", - "4==BlinkStickSquare", - "5==BlinkStickNano", - "6==BlinkStickFlex", - "0==Unknown" -]) +@pytest.mark.parametrize( + "expected_variant, expected_name", + [ + (BlinkStickVariant.BLINKSTICK, "BlinkStick"), + (BlinkStickVariant.BLINKSTICK_PRO, "BlinkStick Pro"), + (BlinkStickVariant.BLINKSTICK_STRIP, "BlinkStick Strip"), + (BlinkStickVariant.BLINKSTICK_SQUARE, "BlinkStick Square"), + (BlinkStickVariant.BLINKSTICK_NANO, "BlinkStick Nano"), + (BlinkStickVariant.BLINKSTICK_FLEX, "BlinkStick Flex"), + (BlinkStickVariant.UNKNOWN, "Unknown"), + ], + ids=[ + "1==BlinkStick", + "2==BlinkStickPro", + "3==BlinkStickStrip", + "4==BlinkStickSquare", + "5==BlinkStickNano", + "6==BlinkStickFlex", + "0==Unknown", + ], +) def test_get_variant_string(make_blinkstick, expected_variant, expected_name): """Test get_variant method for version 0 returns BlinkStick.UNKNOWN (0)""" bs = make_blinkstick() @@ -79,9 +100,9 @@ def test_get_color_rgb_color_format(mocker: MockFixture, make_blinkstick): def test_get_color_hex_color_format(mocker: MockFixture, make_blinkstick): """Test get_color with color_format='hex'. We expect it to return the color in hex format.""" bs = make_blinkstick() - mock_get_color_hex = mocker.Mock(return_value='#ff0000') + mock_get_color_hex = mocker.Mock(return_value="#ff0000") bs._get_color_hex = mock_get_color_hex - assert bs.get_color(color_format='hex') == '#ff0000' + assert bs.get_color(color_format="hex") == "#ff0000" assert mock_get_color_hex.call_count == 1 @@ -90,7 +111,7 @@ def test_get_color_invalid_color_format(mocker: MockFixture, make_blinkstick): bs = make_blinkstick() mock_get_color_rgb = mocker.Mock(return_value=(255, 0, 0)) bs._get_color_rgb = mock_get_color_rgb - bs.get_color(color_format='invalid_format') + bs.get_color(color_format="invalid_format") assert mock_get_color_rgb.call_count == 1 @@ -99,11 +120,13 @@ def test_max_rgb_value_default(make_blinkstick): bs = make_blinkstick() assert bs.get_max_rgb_value() == 255 + def test_max_rgb_value_not_class_attribute(make_blinkstick): """Test that the max_rgb_value is not a class attribute.""" bs = make_blinkstick() - assert not hasattr(BlinkStick, 'max_rgb_value') - assert hasattr(bs, 'max_rgb_value') + assert not hasattr(BlinkStick, "max_rgb_value") + assert hasattr(bs, "max_rgb_value") + def test_set_and_get_max_rgb_value(make_blinkstick): """Test that we can set and get the max_rgb_value.""" @@ -139,6 +162,7 @@ def test_set_max_rgb_value_bounds(make_blinkstick): bs.set_max_rgb_value(256) assert bs.get_max_rgb_value() == 255 + def test_set_max_rgb_value_type_checking(make_blinkstick): """Test that set_max_rgb_value performs type checking and coercion.""" bs = make_blinkstick() @@ -169,33 +193,40 @@ def test_inverse_default(make_blinkstick): def test_inverse_not_class_attribute(make_blinkstick): """Test that the inverse is not a class attribute.""" bs = make_blinkstick() - assert not hasattr(BlinkStick, 'inverse') - assert hasattr(bs, 'inverse') + assert not hasattr(BlinkStick, "inverse") + assert hasattr(bs, "inverse") -@pytest.mark.parametrize("input_value, expected_result", [ - pytest.param(True, True, id="True==True"), - pytest.param(False, False, id="False==False"), -]) +@pytest.mark.parametrize( + "input_value, expected_result", + [ + pytest.param(True, True, id="True==True"), + pytest.param(False, False, id="False==False"), + ], +) def test_inverse_set_and_get(make_blinkstick, input_value, expected_result): """Test that we can set and get the inverse.""" bs = make_blinkstick() bs.set_inverse(input_value) assert bs.get_inverse() == expected_result -@pytest.mark.parametrize("input_value, expected_result", [ - pytest.param(True, True, id="True==True"), - pytest.param("True", True, id="StringTrue==True"), - pytest.param(1.0, True, id="1.0==True"), - pytest.param(0, False, id="0==False"), - pytest.param("False", False, id="StringFalse==False"), - pytest.param(False, False, id="False==False"), - pytest.param(0.0, False, id="0.0==False"), - pytest.param("", False, id="EmptyString==False"), - pytest.param([], False, id="EmptyList==False"), - pytest.param({}, False, id="EmptyDict==False"), - pytest.param(None, False, id="None==False"), -]) + +@pytest.mark.parametrize( + "input_value, expected_result", + [ + pytest.param(True, True, id="True==True"), + pytest.param("True", True, id="StringTrue==True"), + pytest.param(1.0, True, id="1.0==True"), + pytest.param(0, False, id="0==False"), + pytest.param("False", False, id="StringFalse==False"), + pytest.param(False, False, id="False==False"), + pytest.param(0.0, False, id="0.0==False"), + pytest.param("", False, id="EmptyString==False"), + pytest.param([], False, id="EmptyList==False"), + pytest.param({}, False, id="EmptyDict==False"), + pytest.param(None, False, id="None==False"), + ], +) def test_set_inverse_type_checking(make_blinkstick, input_value, expected_result): """Test that set_inverse performs type checking and coercion.""" bs = make_blinkstick() @@ -203,13 +234,46 @@ def test_set_inverse_type_checking(make_blinkstick, input_value, expected_result assert bs.get_inverse() == expected_result -@pytest.mark.parametrize("color_mode, ctrl_transfer_bytes, color, inverse, expected_color", [ - pytest.param(ColorFormat.RGB, (0, 255, 0, 0), (255, 0, 0), False, [255, 0, 0], id="RGB, NoInverse"), - pytest.param(ColorFormat.HEX, (0, 255, 0, 0), '#ff0000', False, '#ff0000', id="Hex, NoInverse"), - pytest.param(ColorFormat.RGB, (0, 255, 0, 0), (255, 0, 0), True, [0, 255, 255], id="RGB, Inverse"), - pytest.param(ColorFormat.HEX, (0, 255, 0, 0), '#ff0000', True, '#00ffff', id="Hex, Inverse"), -]) -def test_inverse_correctly_inverts_rgb_color(make_blinkstick, color_mode, ctrl_transfer_bytes, color, inverse, expected_color): +@pytest.mark.parametrize( + "color_mode, ctrl_transfer_bytes, color, inverse, expected_color", + [ + pytest.param( + ColorFormat.RGB, + (0, 255, 0, 0), + (255, 0, 0), + False, + [255, 0, 0], + id="RGB, NoInverse", + ), + pytest.param( + ColorFormat.HEX, + (0, 255, 0, 0), + "#ff0000", + False, + "#ff0000", + id="Hex, NoInverse", + ), + pytest.param( + ColorFormat.RGB, + (0, 255, 0, 0), + (255, 0, 0), + True, + [0, 255, 255], + id="RGB, Inverse", + ), + pytest.param( + ColorFormat.HEX, + (0, 255, 0, 0), + "#ff0000", + True, + "#00ffff", + id="Hex, Inverse", + ), + ], +) +def test_inverse_correctly_inverts_rgb_color( + make_blinkstick, color_mode, ctrl_transfer_bytes, color, inverse, expected_color +): """Test that the color is correctly inverted when the inverse flag is set.""" bs = make_blinkstick() # mock the backend control_transfer method to return the 3 bytes of the color @@ -224,4 +288,4 @@ def test_inverse_does_not_affect_max_rgb_value(make_blinkstick): bs = make_blinkstick() bs.set_max_rgb_value(100) bs.set_inverse(True) - assert bs.get_max_rgb_value() == 100 \ No newline at end of file + assert bs.get_max_rgb_value() == 100 diff --git a/tests/colors/test_colors.py b/tests/colors/test_colors.py index 0c7af05..5d9bf63 100644 --- a/tests/colors/test_colors.py +++ b/tests/colors/test_colors.py @@ -1,156 +1,222 @@ import pytest -from blinkstick.colors import remap, remap_color, remap_color_reverse, remap_rgb_value, remap_rgb_value_reverse, Color, \ - name_to_hex, normalize_hex, hex_to_rgb, name_to_rgb +from blinkstick.colors import ( + remap, + remap_color, + remap_color_reverse, + remap_rgb_value, + remap_rgb_value_reverse, + Color, + name_to_hex, + normalize_hex, + hex_to_rgb, + name_to_rgb, +) def test_remap_value_within_range(): assert remap(5, 0, 10, 0, 100) == 50 + def test_remap_value_at_minimum(): assert remap(0, 0, 10, 0, 100) == 0 + def test_remap_value_at_maximum(): assert remap(10, 0, 10, 0, 100) == 100 + def test_remap_value_below_minimum(): assert remap(-5, 0, 10, 0, 100) == -50 + def test_remap_value_above_maximum(): assert remap(15, 0, 10, 0, 100) == 150 + def test_remap_value_within_negative_range(): assert remap(-5, -10, 0, -100, 0) == -50 + def test_remap_value_within_reverse_range(): assert remap(5, 0, 10, 100, 0) == 50 + def test_remap_color_value_within_range(): assert remap_color(128, 100) == 50 + def test_remap_color_value_at_minimum(): assert remap_color(0, 100) == 0 + def test_remap_color_value_at_maximum(): assert remap_color(255, 100) == 100 + def test_remap_color_value_below_minimum(): # note: this returns -3 because of the way the remap function is implemented using int(), which always rounds down assert remap_color(-10, 100) == -3 + def test_remap_color_value_above_maximum(): assert remap_color(300, 100) == 117 + def test_remap_color_reverse_value_within_range(): assert remap_color_reverse(50, 100) == 127 + def test_remap_color_reverse_value_at_minimum(): assert remap_color_reverse(0, 100) == 0 + def test_remap_color_reverse_value_at_maximum(): assert remap_color_reverse(100, 100) == 255 + def test_remap_color_reverse_value_below_minimum(): assert remap_color_reverse(-10, 100) == -25 + def test_remap_color_reverse_value_above_maximum(): assert remap_color_reverse(150, 100) == 382 + def test_remap_rgb_value_within_range(): assert remap_rgb_value((128, 128, 128), 100) == (50, 50, 50) + def test_remap_rgb_value_at_minimum(): assert remap_rgb_value((0, 0, 0), 100) == (0, 0, 0) + def test_remap_rgb_value_at_maximum(): assert remap_rgb_value((255, 255, 255), 100) == (100, 100, 100) + def test_remap_rgb_value_below_minimum(): assert remap_rgb_value((-10, -10, -10), 100) == (-3, -3, -3) + def test_remap_rgb_value_above_maximum(): assert remap_rgb_value((300, 300, 300), 100) == (117, 117, 117) + def test_remap_rgb_value_reverse_within_range(): assert remap_rgb_value_reverse((50, 50, 50), 100) == (127, 127, 127) + def test_remap_rgb_value_reverse_at_minimum(): assert remap_rgb_value_reverse((0, 0, 0), 100) == (0, 0, 0) + def test_remap_rgb_value_reverse_at_maximum(): assert remap_rgb_value_reverse((100, 100, 100), 100) == (255, 255, 255) + def test_remap_rgb_value_reverse_below_minimum(): assert remap_rgb_value_reverse((-10, -10, -10), 100) == (-25, -25, -25) + def test_remap_rgb_value_reverse_above_maximum(): assert remap_rgb_value_reverse((150, 150, 150), 100) == (382, 382, 382) + def test_all_colors_present(w3c_colors): assert len(w3c_colors) == len(Color) + def test_color_from_name_valid_color(w3c_colors): for color_name, _ in w3c_colors: assert Color.from_name(color_name) == Color[color_name.upper()] + def test_color_from_name_invalid_color(): - with pytest.raises(ValueError, match="'invalidcolor' is not defined as a named color."): - Color.from_name('invalidcolor') + with pytest.raises( + ValueError, match="'invalidcolor' is not defined as a named color." + ): + Color.from_name("invalidcolor") + def test_color_from_name_case_insensitive(w3c_colors): for color_name, _ in w3c_colors: assert Color.from_name(color_name.upper()) == Color[color_name.upper()] assert Color.from_name(color_name.lower()) == Color[color_name.upper()] + def test_color_name_to_hex(w3c_colors): for color_name, color_hex in w3c_colors: assert name_to_hex(color_name) == color_hex + def test_name_to_rgb_white(): - assert name_to_rgb('white') == (255, 255, 255) + assert name_to_rgb("white") == (255, 255, 255) + def test_name_to_rgb_navy(): - assert name_to_rgb('navy') == (0, 0, 128) + assert name_to_rgb("navy") == (0, 0, 128) + def test_name_to_rgb_goldenrod(): - assert name_to_rgb('goldenrod') == (218, 165, 32) + assert name_to_rgb("goldenrod") == (218, 165, 32) + def test_name_to_rgb_invalid_color(): - with pytest.raises(ValueError, match="'invalidcolor' is not defined as a named color."): - name_to_rgb('invalidcolor') + with pytest.raises( + ValueError, match="'invalidcolor' is not defined as a named color." + ): + name_to_rgb("invalidcolor") + def test_normalize_hex_valid_six_digit_lowercase(): - assert normalize_hex('#0099cc') == '#0099cc' + assert normalize_hex("#0099cc") == "#0099cc" + def test_normalize_hex_valid_six_digit_uppercase(): - assert normalize_hex('#0099CC') == '#0099cc' + assert normalize_hex("#0099CC") == "#0099cc" + def test_normalize_hex_valid_three_digit_lowercase(): - assert normalize_hex('#09c') == '#0099cc' + assert normalize_hex("#09c") == "#0099cc" + def test_normalize_hex_valid_three_digit_uppercase(): - assert normalize_hex('#09C') == '#0099cc' + assert normalize_hex("#09C") == "#0099cc" + def test_normalize_hex_missing_hash(): - with pytest.raises(ValueError, match="'0099cc' is not a valid hexadecimal color value."): - normalize_hex('0099cc') + with pytest.raises( + ValueError, match="'0099cc' is not a valid hexadecimal color value." + ): + normalize_hex("0099cc") + def test_hex_to_rgb_valid_six_digit_lowercase(): - assert hex_to_rgb('#0099cc') == (0, 153, 204) + assert hex_to_rgb("#0099cc") == (0, 153, 204) + def test_hex_to_rgb_valid_six_digit_uppercase(): - assert hex_to_rgb('#0099CC') == (0, 153, 204) + assert hex_to_rgb("#0099CC") == (0, 153, 204) + def test_hex_to_rgb_valid_three_digit_lowercase(): - assert hex_to_rgb('#09c') == (0, 153, 204) + assert hex_to_rgb("#09c") == (0, 153, 204) + def test_hex_to_rgb_valid_three_digit_uppercase(): - assert hex_to_rgb('#09C') == (0, 153, 204) + assert hex_to_rgb("#09C") == (0, 153, 204) + def test_hex_to_rgb_missing_hash(): - with pytest.raises(ValueError, match="'0099cc' is not a valid hexadecimal color value."): - hex_to_rgb('0099cc') + with pytest.raises( + ValueError, match="'0099cc' is not a valid hexadecimal color value." + ): + hex_to_rgb("0099cc") + def test_hex_to_rgb_invalid_hex(): - with pytest.raises(ValueError, match="'#xyz' is not a valid hexadecimal color value."): - hex_to_rgb('#xyz') \ No newline at end of file + with pytest.raises( + ValueError, match="'#xyz' is not a valid hexadecimal color value." + ): + hex_to_rgb("#xyz") diff --git a/tests/conftest.py b/tests/conftest.py index fb49c08..bda0985 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,4 +12,5 @@ def _make_blinkstick() -> BlinkStick: bs = BlinkStick() bs.backend = MagicMock() return bs + return _make_blinkstick