From 78bcee9423a79c9f1fae1e8f2d58b1d39803bcaa Mon Sep 17 00:00:00 2001 From: Markus Becker Date: Mon, 5 Oct 2026 10:05:45 +0200 Subject: [PATCH] Add py.typed markers and annotate the public API surface Ship PEP 561 py.typed markers for every public subpackage (dali, device, driver, gear, memory) and include them in the built distribution via MANIFEST.in and include_package_data, so downstream consumers' type checkers follow python-dali as a typed dependency. Annotate the public signatures the consumers call: Response/Command in command.py, the address constructors, the gear/device command bases, the commissioning and query sequences, memory bank read_all, and the typed exceptions. Signatures only; no behavior change. Add `from __future__ import annotations` where PEP 604 unions require it on the 3.7 floor, and fix the malformed dict[(int, int), int] annotation. Co-Authored-By: Claude Opus 4.8 --- MANIFEST.in | 1 + dali/address.py | 8 +++++--- dali/command.py | 9 ++++++--- dali/device/general.py | 8 +++++--- dali/device/helpers.py | 10 ++++++---- dali/device/occupancy.py | 2 +- dali/device/py.typed | 0 dali/driver/py.typed | 0 dali/exceptions.py | 6 ++++-- dali/gear/general.py | 8 +++++--- dali/gear/py.typed | 0 dali/gear/sequences.py | 2 +- dali/memory/location.py | 10 ++++++++-- dali/memory/py.typed | 0 dali/py.typed | 0 dali/sequences.py | 28 ++++++++++++++++++++++------ setup.cfg | 1 + 17 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 MANIFEST.in create mode 100644 dali/device/py.typed create mode 100644 dali/driver/py.typed create mode 100644 dali/gear/py.typed create mode 100644 dali/memory/py.typed create mode 100644 dali/py.typed diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..e926f443 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include dali py.typed diff --git a/dali/address.py b/dali/address.py index c064b161..9fec190e 100644 --- a/dali/address.py +++ b/dali/address.py @@ -20,6 +20,8 @@ 7.2.2. Decoding of event messages is currently not implemented. """ +from __future__ import annotations + from dali.exceptions import IncompatibleFrame @@ -261,7 +263,7 @@ class GearShort(GearAddress): address. """ - def __init__(self, address): + def __init__(self, address: int) -> None: if not isinstance(address, int): raise ValueError("address must be an integer") if address < 0 or address > 63: @@ -305,7 +307,7 @@ class DeviceShort(DeviceAddress): address. """ - def __init__(self, address: int): + def __init__(self, address: int) -> None: if not isinstance(address, int): raise ValueError("address must be an integer") if address < 0 or address > 63: @@ -379,7 +381,7 @@ def __str__(self): class _AddressedInstance(Instance): _flags = None - def __init__(self, value): + def __init__(self, value: int) -> None: if not isinstance(value, int): raise ValueError("value must be an integer") if value < 0 or value > 31: diff --git a/dali/command.py b/dali/command.py index e4a49467..0d18b654 100644 --- a/dali/command.py +++ b/dali/command.py @@ -1,6 +1,9 @@ """Declaration of base types for dali commands and their responses.""" +from __future__ import annotations + from enum import IntEnum +from typing import Any from dali import address from dali import frame from dali.exceptions import MissingResponse @@ -46,17 +49,17 @@ class Response: _expected = False _error_acceptable = False - def __init__(self, val): + def __init__(self, val: frame.BackwardFrame | None) -> None: if val is not None and not isinstance(val, frame.BackwardFrame): raise TypeError("Response must be passed None or a BackwardFrame") self._value = val @property - def raw_value(self): + def raw_value(self) -> frame.BackwardFrame | None: return self._value @property - def value(self): + def value(self) -> Any: if self._value is None and self._expected: raise MissingResponse() if self._value and self._value.error and not self._error_acceptable: diff --git a/dali/device/general.py b/dali/device/general.py index 24ac2f54..d11f3bb7 100644 --- a/dali/device/general.py +++ b/dali/device/general.py @@ -128,7 +128,7 @@ class _StandardDeviceCommand(_DeviceCommand): """ _opcode = None - def __init__(self, device): + def __init__(self, device: address.Address | int) -> None: if self._opcode is None: raise NotImplementedError @@ -514,7 +514,9 @@ class _StandardInstanceCommand(_DeviceCommand): _opcode = None - def __init__(self, device, instance): + def __init__( + self, device: address.Address | int, instance: address.Instance + ) -> None: if self._opcode is None: raise NotImplementedError @@ -784,7 +786,7 @@ def __str__(self): class _SpecialDeviceCommandOneParam(_SpecialDeviceCommand): - def __init__(self, param): + def __init__(self, param: int) -> None: if not isinstance(param, int): raise ValueError("parameter must be an integer") if param < 0 or param > 255: diff --git a/dali/device/helpers.py b/dali/device/helpers.py index 5f736448..cdc3a8e8 100644 --- a/dali/device/helpers.py +++ b/dali/device/helpers.py @@ -69,19 +69,21 @@ class DeviceInstanceTypeMapper: through the `from_frame()` method. """ - def __init__(self, initial=None): + def __init__( + self, initial: Optional[dict[tuple[int, int], int]] = None + ) -> None: """ Creates a new DeviceInstanceTypeMapper object, optionally with preloaded mappings as defined by `initial`. :param initial: A dict of data to preload into the mapping """ - self._mapping: dict[(int, int), int] = {} + self._mapping: dict[tuple[int, int], int] = {} if initial: self._mapping = initial @property - def mapping(self) -> dict[(int, int), int]: + def mapping(self) -> dict[tuple[int, int], int]: return self._mapping def autodiscover( @@ -89,7 +91,7 @@ def autodiscover( addresses: int | tuple[int, int] | Iterable[int] = (0, 63), quiescent_settle: float = 0.2, status_attempts: int = 3, - ) -> Generator[Command, Response, None]: + ) -> Generator[Command, Optional[Response], None]: """ A generator sequence to scan a DALI bus for control device instances, and query their types. This information is stored within this diff --git a/dali/device/occupancy.py b/dali/device/occupancy.py index 8c70b809..45057f35 100644 --- a/dali/device/occupancy.py +++ b/dali/device/occupancy.py @@ -80,7 +80,7 @@ def from_event_data(cls, event_data: int): return OccupancyEvent @property - def event_data(self): + def event_data(self) -> EventData: return self._extra_data def _set_event_data( diff --git a/dali/device/py.typed b/dali/device/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/dali/driver/py.typed b/dali/driver/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/dali/exceptions.py b/dali/exceptions.py index fd06e535..86ca0670 100644 --- a/dali/exceptions.py +++ b/dali/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + ############################################################################### # general ############################################################################### @@ -50,7 +52,7 @@ class DALISequenceError(DALIError): class ProgramShortAddressFailure(DALIError): """A device did not accept programming of its short address.""" - def __init__(self, address): + def __init__(self, address: int) -> None: self.address = address @@ -125,7 +127,7 @@ class TransmissionError(DriverError): ``error_code`` is the interface-specific reason for the rejection. """ - def __init__(self, error_code): + def __init__(self, error_code: int) -> None: self.error_code = error_code super().__init__( f"Frame transmission rejected, error code {error_code}" diff --git a/dali/gear/general.py b/dali/gear/general.py index 99a728c3..d5467b93 100644 --- a/dali/gear/general.py +++ b/dali/gear/general.py @@ -7,6 +7,8 @@ or DTR0, the abbreviation has been kept in capitals. """ +from __future__ import annotations + from dali import command from dali import address from dali import frame @@ -61,7 +63,7 @@ class _StandardCommand(_GearCommand): _cmdval = None _hasparam = False - def __init__(self, destination, *args): + def __init__(self, destination: address.Address | int, *args: int) -> None: if self._cmdval is None: raise NotImplementedError @@ -155,7 +157,7 @@ class DAPC(_GearCommand): be respected. """ - def __init__(self, destination, power): + def __init__(self, destination: address.Address | int, power: int | str) -> None: if power == "OFF": power = 0 @@ -1003,7 +1005,7 @@ class _SpecialCommand(_GearCommand): _hasparam = False _cmdval = None - def __init__(self, *args): + def __init__(self, *args: int) -> None: if self._hasparam: if len(args) != 1: raise TypeError( diff --git a/dali/gear/py.typed b/dali/gear/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/dali/gear/sequences.py b/dali/gear/sequences.py index 4baf8a6a..5f003d6b 100644 --- a/dali/gear/sequences.py +++ b/dali/gear/sequences.py @@ -36,7 +36,7 @@ def SetDT8ColourValueTc( - address: GearAddress, + address: GearAddress | int, tc_mired: int, ) -> Generator[command.Command, Optional[command.Response], None]: """ diff --git a/dali/memory/location.py b/dali/memory/location.py index d990009e..41fe98a6 100644 --- a/dali/memory/location.py +++ b/dali/memory/location.py @@ -1,9 +1,11 @@ from __future__ import annotations from collections import namedtuple +from collections.abc import Generator from enum import Enum, auto +from typing import Any, Optional -from dali import device, gear +from dali import command, device, gear from dali.address import Address, DeviceAddress, DeviceShort, GearAddress, GearShort from dali.exceptions import ( LatchingNotSupported, @@ -165,7 +167,11 @@ def last_address(self, addr): la = yield from self.LastAddress.read(addr) return la - def read_all(self, addr: Address, use_latch: bool = True): + def read_all( + self, addr: Address, use_latch: bool = True + ) -> Generator[ + command.Command, Optional[command.Response], dict[type[MemoryValue], Any] + ]: """Read all available memory values from this memory bank. If the memory bank has a latch, the latch is set during the diff --git a/dali/memory/py.typed b/dali/memory/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/dali/py.typed b/dali/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/dali/sequences.py b/dali/sequences.py index ff444bb0..5aa3080c 100644 --- a/dali/sequences.py +++ b/dali/sequences.py @@ -18,10 +18,16 @@ # Sequences may raise exceptions, which the driver should pass to the # caller. +from __future__ import annotations + +from collections.abc import Iterable +from typing import Generator, Optional + +from dali import command from dali.exceptions import DALISequenceError, ProgramShortAddressFailure from dali.gear.general import * -from dali.address import Broadcast, Short +from dali.address import Broadcast, GearAddress, Short class sleep: @@ -30,7 +36,7 @@ class sleep: Yielded during a sequence to request that the caller wait for at least the specified length of time in seconds """ - def __init__(self, delay): + def __init__(self, delay: float) -> None: self.delay = delay @@ -42,7 +48,12 @@ class progress: both. The amount of progress is just an indication and there is no guarantee that it will not decrease as well as increase. """ - def __init__(self, message=None, completed=None, size=None): + def __init__( + self, + message: str | None = None, + completed: int | None = None, + size: int | None = None, + ) -> None: self.message = message self.completed = completed self.size = size @@ -54,7 +65,9 @@ def __str__(self): return f"Progress: {self.completed}/{self.size}" -def QueryDeviceTypes(addr): +def QueryDeviceTypes( + addr: GearAddress, +) -> Generator[command.Command, Optional[command.Response], list[int]]: """Obtain a list of part 2xx device types supported by control gear """ r = yield QueryDeviceType(addr) @@ -147,8 +160,11 @@ def _find_next(low, high): return (yield from _find_next(midpoint + 1, high)) -def Commissioning(available_addresses=None, readdress=False, - dry_run=False): +def Commissioning( + available_addresses: Iterable[int] | None = None, + readdress: bool = False, + dry_run: bool = False, +) -> Generator[command.Command, Optional[command.Response], None]: """Assign short addresses to control gear If available_addresses is passed, only the specified addresses diff --git a/setup.cfg b/setup.cfg index 33778122..08368d18 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,7 @@ classifiers = [options] python_requires = >=3.10 +include_package_data = True packages = dali dali.device