PyPlumIO 0.5.21__py3-none-any.whl → 0.5.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyplumio/devices/mixer.py CHANGED
@@ -3,9 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Sequence
6
+ from collections.abc import Coroutine, Generator, Sequence
7
7
  import logging
8
- from typing import Any
8
+ from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from pyplumio.devices import AddressableDevice, SubDevice
11
11
  from pyplumio.helpers.parameter import ParameterValues
@@ -19,31 +19,35 @@ from pyplumio.structures.mixer_parameters import (
19
19
  from pyplumio.structures.mixer_sensors import ATTR_MIXER_SENSORS
20
20
  from pyplumio.structures.product_info import ATTR_PRODUCT, ProductInfo
21
21
 
22
+ if TYPE_CHECKING:
23
+ from pyplumio.frames import Frame
24
+
22
25
  _LOGGER = logging.getLogger(__name__)
23
26
 
24
27
 
25
28
  class Mixer(SubDevice):
26
29
  """Represents an mixer."""
27
30
 
28
- def __init__(self, queue: asyncio.Queue, parent: AddressableDevice, index: int = 0):
31
+ def __init__(
32
+ self, queue: asyncio.Queue[Frame], parent: AddressableDevice, index: int = 0
33
+ ):
29
34
  """Initialize a new mixer."""
30
35
  super().__init__(queue, parent, index)
31
- self.subscribe(ATTR_MIXER_SENSORS, self._handle_sensors)
32
- self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_parameters)
36
+ self.subscribe(ATTR_MIXER_SENSORS, self._handle_mixer_sensors)
37
+ self.subscribe(ATTR_MIXER_PARAMETERS, self._handle_mixer_parameters)
33
38
 
34
- async def _handle_sensors(self, sensors: dict[str, Any]) -> bool:
39
+ async def _handle_mixer_sensors(self, sensors: dict[str, Any]) -> bool:
35
40
  """Handle mixer sensors.
36
41
 
37
42
  For each sensor dispatch an event with the
38
43
  sensor's name and value.
39
44
  """
40
45
  await asyncio.gather(
41
- *[self.dispatch(name, value) for name, value in sensors.items()]
46
+ *(self.dispatch(name, value) for name, value in sensors.items())
42
47
  )
43
-
44
48
  return True
45
49
 
46
- async def _handle_parameters(
50
+ async def _handle_mixer_parameters(
47
51
  self, parameters: Sequence[tuple[int, ParameterValues]]
48
52
  ) -> bool:
49
53
  """Handle mixer parameters.
@@ -52,39 +56,41 @@ class Mixer(SubDevice):
52
56
  parameter's name and value.
53
57
  """
54
58
  product: ProductInfo = await self.parent.get(ATTR_PRODUCT)
55
- for index, values in parameters:
56
- try:
57
- description = MIXER_PARAMETERS[product.type][index]
58
- except IndexError:
59
- _LOGGER.warning(
60
- (
61
- "Encountered unknown mixer parameter (%i): %s. "
62
- "Your device isn't fully compatible with this software and "
63
- "may not work properly. "
64
- "Please visit the issue tracker and open a feature "
65
- "request to support %s"
66
- ),
67
- index,
68
- values,
69
- product.model,
70
- )
71
- return False
72
59
 
73
- name = description.name
74
- if name in self.data:
75
- parameter: MixerParameter = self.data[name]
76
- parameter.values = values
77
- await self.dispatch(name, parameter)
78
- continue
60
+ def _mixer_parameter_events() -> Generator[Coroutine, Any, None]:
61
+ """Get dispatch calls for mixer parameter events."""
62
+ for index, values in parameters:
63
+ try:
64
+ description = MIXER_PARAMETERS[product.type][index]
65
+ except IndexError:
66
+ _LOGGER.warning(
67
+ (
68
+ "Encountered unknown mixer parameter (%i): %s. "
69
+ "Your device isn't fully compatible with this software and "
70
+ "may not work properly. "
71
+ "Please visit the issue tracker and open a feature "
72
+ "request to support %s"
73
+ ),
74
+ index,
75
+ values,
76
+ product.model,
77
+ )
78
+ return
79
79
 
80
- cls = (
81
- MixerBinaryParameter
82
- if isinstance(description, MixerBinaryParameterDescription)
83
- else MixerParameter
84
- )
85
- await self.dispatch(
86
- name,
87
- cls(device=self, values=values, description=description, index=index),
88
- )
80
+ handler = (
81
+ MixerBinaryParameter
82
+ if isinstance(description, MixerBinaryParameterDescription)
83
+ else MixerParameter
84
+ )
85
+ yield self.dispatch(
86
+ description.name,
87
+ handler.create_or_update(
88
+ device=self,
89
+ description=description,
90
+ values=values,
91
+ index=index,
92
+ ),
93
+ )
89
94
 
95
+ await asyncio.gather(*_mixer_parameter_events())
90
96
  return True
@@ -3,8 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Sequence
7
- from typing import Any
6
+ from collections.abc import Coroutine, Generator, Sequence
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from pyplumio.devices import AddressableDevice, SubDevice
10
10
  from pyplumio.helpers.parameter import ParameterValues
@@ -17,29 +17,33 @@ from pyplumio.structures.thermostat_parameters import (
17
17
  )
18
18
  from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
19
19
 
20
+ if TYPE_CHECKING:
21
+ from pyplumio.frames import Frame
22
+
20
23
 
21
24
  class Thermostat(SubDevice):
22
25
  """Represents a thermostat."""
23
26
 
24
- def __init__(self, queue: asyncio.Queue, parent: AddressableDevice, index: int = 0):
27
+ def __init__(
28
+ self, queue: asyncio.Queue[Frame], parent: AddressableDevice, index: int = 0
29
+ ):
25
30
  """Initialize a new thermostat."""
26
31
  super().__init__(queue, parent, index)
27
- self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_sensors)
28
- self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_parameters)
32
+ self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
33
+ self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_thermostat_parameters)
29
34
 
30
- async def _handle_sensors(self, sensors: dict[str, Any]) -> bool:
35
+ async def _handle_thermostat_sensors(self, sensors: dict[str, Any]) -> bool:
31
36
  """Handle thermostat sensors.
32
37
 
33
38
  For each sensor dispatch an event with the
34
39
  sensor's name and value.
35
40
  """
36
41
  await asyncio.gather(
37
- *[self.dispatch(name, value) for name, value in sensors.items()]
42
+ *(self.dispatch(name, value) for name, value in sensors.items())
38
43
  )
39
-
40
44
  return True
41
45
 
42
- async def _handle_parameters(
46
+ async def _handle_thermostat_parameters(
43
47
  self, parameters: Sequence[tuple[int, ParameterValues]]
44
48
  ) -> bool:
45
49
  """Handle thermostat parameters.
@@ -47,29 +51,26 @@ class Thermostat(SubDevice):
47
51
  For each parameter dispatch an event with the
48
52
  parameter's name and value.
49
53
  """
50
- for index, values in parameters:
51
- description = THERMOSTAT_PARAMETERS[index]
52
- name = description.name
53
- if name in self.data:
54
- parameter: ThermostatParameter = self.data[name]
55
- parameter.values = values
56
- await self.dispatch(name, parameter)
57
- continue
58
54
 
59
- cls = (
60
- ThermostatBinaryParameter
61
- if isinstance(description, ThermostatBinaryParameterDescription)
62
- else ThermostatParameter
63
- )
64
- await self.dispatch(
65
- name,
66
- cls(
67
- device=self,
68
- values=values,
69
- description=description,
70
- index=index,
71
- offset=(self.index * len(parameters)),
72
- ),
73
- )
55
+ def _thermostat_parameter_events() -> Generator[Coroutine, Any, None]:
56
+ """Get dispatch calls for thermostat parameter events."""
57
+ for index, values in parameters:
58
+ description = THERMOSTAT_PARAMETERS[index]
59
+ handler = (
60
+ ThermostatBinaryParameter
61
+ if isinstance(description, ThermostatBinaryParameterDescription)
62
+ else ThermostatParameter
63
+ )
64
+ yield self.dispatch(
65
+ description.name,
66
+ handler.create_or_update(
67
+ device=self,
68
+ description=description,
69
+ values=values,
70
+ index=index,
71
+ offset=(self.index * len(parameters)),
72
+ ),
73
+ )
74
74
 
75
+ await asyncio.gather(*_thermostat_parameter_events())
75
76
  return True
pyplumio/exceptions.py CHANGED
@@ -11,25 +11,25 @@ class ConnectionFailedError(PyPlumIOError):
11
11
  """Raised on connection failure."""
12
12
 
13
13
 
14
- class UnknownDeviceError(PyPlumIOError):
15
- """Raised on unsupported device."""
14
+ class ProtocolError(PyPlumIOError):
15
+ """Base class for protocol-related errors."""
16
16
 
17
17
 
18
- class ReadError(PyPlumIOError):
18
+ class ReadError(ProtocolError):
19
19
  """Raised on read error."""
20
20
 
21
21
 
22
- class FrameError(PyPlumIOError):
23
- """Base class for frame errors."""
22
+ class ChecksumError(ProtocolError):
23
+ """Raised on incorrect frame checksum."""
24
24
 
25
25
 
26
- class ChecksumError(FrameError):
27
- """Raised on checksum error while parsing frame content."""
26
+ class UnknownDeviceError(ProtocolError):
27
+ """Raised on unknown device."""
28
28
 
29
29
 
30
- class UnknownFrameError(FrameError):
30
+ class UnknownFrameError(ProtocolError):
31
31
  """Raised on unknown frame type."""
32
32
 
33
33
 
34
- class FrameDataError(FrameError):
34
+ class FrameDataError(ProtocolError):
35
35
  """Raised on incorrect frame data."""
pyplumio/filters.py CHANGED
@@ -3,18 +3,49 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
- from collections.abc import Awaitable, Callable
6
+ from collections.abc import Callable
7
7
  from copy import copy
8
8
  import math
9
9
  import time
10
- from typing import Any, Final, SupportsFloat, TypeVar, overload
11
-
10
+ from typing import (
11
+ Any,
12
+ Final,
13
+ Protocol,
14
+ SupportsFloat,
15
+ TypeVar,
16
+ overload,
17
+ runtime_checkable,
18
+ )
19
+
20
+ from pyplumio.helpers.event_manager import Callback
12
21
  from pyplumio.helpers.parameter import Parameter
13
- from pyplumio.helpers.typing import SupportsComparison, SupportsSubtraction
14
22
 
15
23
  UNDEFINED: Final = "undefined"
16
24
  TOLERANCE: Final = 0.1
17
25
 
26
+
27
+ @runtime_checkable
28
+ class SupportsSubtraction(Protocol):
29
+ """Supports subtraction operation."""
30
+
31
+ __slots__ = ()
32
+
33
+ def __sub__(
34
+ self: SupportsSubtraction, other: SupportsSubtraction
35
+ ) -> SupportsSubtraction:
36
+ """Subtract a value."""
37
+
38
+
39
+ @runtime_checkable
40
+ class SupportsComparison(Protocol):
41
+ """Supports comparison."""
42
+
43
+ __slots__ = ()
44
+
45
+ def __eq__(self: SupportsComparison, other: SupportsComparison) -> bool:
46
+ """Compare a value."""
47
+
48
+
18
49
  Comparable = TypeVar("Comparable", Parameter, SupportsFloat, SupportsComparison)
19
50
 
20
51
 
@@ -71,23 +102,23 @@ class Filter(ABC):
71
102
 
72
103
  __slots__ = ("_callback", "_value")
73
104
 
74
- _callback: Callable[[Any], Awaitable[Any]]
105
+ _callback: Callback
75
106
  _value: Any
76
107
 
77
- def __init__(self, callback: Callable[[Any], Awaitable[Any]]) -> None:
108
+ def __init__(self, callback: Callback) -> None:
78
109
  """Initialize a new filter."""
79
110
  self._callback = callback
80
111
  self._value = UNDEFINED
81
112
 
82
- def __eq__(self, other: Any) -> Any:
113
+ def __eq__(self, other: Any) -> bool:
83
114
  """Compare callbacks."""
84
115
  if isinstance(other, Filter):
85
116
  return self._callback == other._callback
86
117
 
87
118
  if callable(other):
88
- return self._callback == other
119
+ return bool(self._callback == other)
89
120
 
90
- raise TypeError
121
+ return NotImplemented
91
122
 
92
123
  @abstractmethod
93
124
  async def __call__(self, new_value: Any) -> Any:
@@ -112,14 +143,14 @@ class _OnChange(Filter):
112
143
  return await self._callback(new_value)
113
144
 
114
145
 
115
- def on_change(callback: Callable[[Any], Awaitable[Any]]) -> _OnChange:
146
+ def on_change(callback: Callback) -> _OnChange:
116
147
  """Return a value changed filter.
117
148
 
118
149
  A callback function will only be called if value is changed from the
119
150
  previous call.
120
151
 
121
152
  :param callback: A callback function to be awaited on value change
122
- :type callback: Callable[[Any], Awaitable[Any]]
153
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
123
154
  :return: A instance of callable filter
124
155
  :rtype: _OnChange
125
156
  """
@@ -138,9 +169,7 @@ class _Debounce(Filter):
138
169
  _calls: int
139
170
  _min_calls: int
140
171
 
141
- def __init__(
142
- self, callback: Callable[[Any], Awaitable[Any]], min_calls: int
143
- ) -> None:
172
+ def __init__(self, callback: Callback, min_calls: int) -> None:
144
173
  """Initialize a new debounce filter."""
145
174
  super().__init__(callback)
146
175
  self._calls = 0
@@ -161,14 +190,14 @@ class _Debounce(Filter):
161
190
  return await self._callback(new_value)
162
191
 
163
192
 
164
- def debounce(callback: Callable[[Any], Awaitable[Any]], min_calls: int) -> _Debounce:
193
+ def debounce(callback: Callback, min_calls: int) -> _Debounce:
165
194
  """Return a debounce filter.
166
195
 
167
196
  A callback function will only called once value is stabilized
168
197
  across multiple filter calls.
169
198
 
170
199
  :param callback: A callback function to be awaited on value change
171
- :type callback: Callable[[Any], Awaitable[Any]]
200
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
172
201
  :param min_calls: Value shouldn't change for this amount of
173
202
  filter calls
174
203
  :type min_calls: int
@@ -190,9 +219,7 @@ class _Throttle(Filter):
190
219
  _last_called: float | None
191
220
  _timeout: float
192
221
 
193
- def __init__(
194
- self, callback: Callable[[Any], Awaitable[Any]], seconds: float
195
- ) -> None:
222
+ def __init__(self, callback: Callback, seconds: float) -> None:
196
223
  """Initialize a new throttle filter."""
197
224
  super().__init__(callback)
198
225
  self._last_called = None
@@ -209,7 +236,7 @@ class _Throttle(Filter):
209
236
  return await self._callback(new_value)
210
237
 
211
238
 
212
- def throttle(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Throttle:
239
+ def throttle(callback: Callback, seconds: float) -> _Throttle:
213
240
  """Return a throttle filter.
214
241
 
215
242
  A callback function will only be called once a certain amount of
@@ -217,7 +244,7 @@ def throttle(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Thro
217
244
 
218
245
  :param callback: A callback function that will be awaited once
219
246
  filter conditions are fulfilled
220
- :type callback: Callable[[Any], Awaitable[Any]]
247
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
221
248
  :param seconds: A callback will be awaited at most once per
222
249
  this amount of seconds
223
250
  :type seconds: float
@@ -249,7 +276,7 @@ class _Delta(Filter):
249
276
  return await self._callback(difference)
250
277
 
251
278
 
252
- def delta(callback: Callable[[Any], Awaitable[Any]]) -> _Delta:
279
+ def delta(callback: Callback) -> _Delta:
253
280
  """Return a difference filter.
254
281
 
255
282
  A callback function will be called with a difference between two
@@ -257,7 +284,7 @@ def delta(callback: Callable[[Any], Awaitable[Any]]) -> _Delta:
257
284
 
258
285
  :param callback: A callback function that will be awaited with
259
286
  difference between values in two subsequent calls
260
- :type callback: Callable[[Any], Awaitable[Any]]
287
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
261
288
  :return: A instance of callable filter
262
289
  :rtype: _Delta
263
290
  """
@@ -277,9 +304,7 @@ class _Aggregate(Filter):
277
304
  _last_update: float
278
305
  _timeout: float
279
306
 
280
- def __init__(
281
- self, callback: Callable[[Any], Awaitable[Any]], seconds: float
282
- ) -> None:
307
+ def __init__(self, callback: Callback, seconds: float) -> None:
283
308
  """Initialize a new aggregate filter."""
284
309
  super().__init__(callback)
285
310
  self._last_update = time.monotonic()
@@ -303,7 +328,7 @@ class _Aggregate(Filter):
303
328
  return result
304
329
 
305
330
 
306
- def aggregate(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Aggregate:
331
+ def aggregate(callback: Callback, seconds: float) -> _Aggregate:
307
332
  """Return an aggregate filter.
308
333
 
309
334
  A callback function will be called with a sum of values collected
@@ -311,7 +336,7 @@ def aggregate(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Agg
311
336
 
312
337
  :param callback: A callback function to be awaited once filter
313
338
  conditions are fulfilled
314
- :type callback: Callable[[Any], Awaitable[Any]]
339
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
315
340
  :param seconds: A callback will be awaited with a sum of values
316
341
  aggregated over this amount of seconds.
317
342
  :type seconds: float
@@ -333,11 +358,7 @@ class _Custom(Filter):
333
358
 
334
359
  filter_fn: Callable[[Any], bool]
335
360
 
336
- def __init__(
337
- self,
338
- callback: Callable[[Any], Awaitable[Any]],
339
- filter_fn: Callable[[Any], bool],
340
- ) -> None:
361
+ def __init__(self, callback: Callback, filter_fn: Callable[[Any], bool]) -> None:
341
362
  """Initialize a new custom filter."""
342
363
  super().__init__(callback)
343
364
  self._filter_fn = filter_fn
@@ -348,9 +369,7 @@ class _Custom(Filter):
348
369
  await self._callback(new_value)
349
370
 
350
371
 
351
- def custom(
352
- callback: Callable[[Any], Awaitable[Any]], filter_fn: Callable[[Any], bool]
353
- ) -> _Custom:
372
+ def custom(callback: Callback, filter_fn: Callable[[Any], bool]) -> _Custom:
354
373
  """Return a custom filter.
355
374
 
356
375
  A callback function will be called when user-defined filter
@@ -359,7 +378,7 @@ def custom(
359
378
 
360
379
  :param callback: A callback function to be awaited when
361
380
  filter function return true
362
- :type callback: Callable[[Any], Awaitable[Any]]
381
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
363
382
  :param filter_fn: Filter function, that will be called with a
364
383
  value and should return `True` to await filter's callback
365
384
  :type filter_fn: Callable[[Any], bool]
@@ -29,9 +29,6 @@ if TYPE_CHECKING:
29
29
  from pyplumio.devices import AddressableDevice
30
30
 
31
31
 
32
- T = TypeVar("T")
33
-
34
-
35
32
  def bcc(data: bytes) -> int:
36
33
  """Return a block check character."""
37
34
  return reduce(lambda x, y: x ^ y, data)
@@ -112,7 +109,7 @@ class Frame(ABC):
112
109
  self._data = data if not kwargs else ensure_dict(data, kwargs)
113
110
  self._message = message
114
111
 
115
- def __eq__(self, other: Any) -> bool:
112
+ def __eq__(self, other: object) -> bool:
116
113
  """Compare if this frame is equal to other."""
117
114
  if isinstance(other, Frame):
118
115
  return (
@@ -131,7 +128,7 @@ class Frame(ABC):
131
128
  self._data,
132
129
  )
133
130
 
134
- raise TypeError
131
+ return NotImplemented
135
132
 
136
133
  def __repr__(self) -> str:
137
134
  """Return a serializable string representation."""
@@ -224,7 +221,7 @@ class Frame(ABC):
224
221
  return bytes(data)
225
222
 
226
223
  @classmethod
227
- async def create(cls: type[T], frame_type: int, **kwargs: Any) -> T:
224
+ async def create(cls: type[FrameT], frame_type: int, **kwargs: Any) -> FrameT:
228
225
  """Create a frame handler object from frame type."""
229
226
  return await create_instance(get_frame_handler(frame_type), cls=cls, **kwargs)
230
227
 
@@ -237,6 +234,9 @@ class Frame(ABC):
237
234
  """Decode frame message."""
238
235
 
239
236
 
237
+ FrameT = TypeVar("FrameT", bound=Frame)
238
+
239
+
240
240
  class Request(Frame):
241
241
  """Represents a request."""
242
242
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from contextlib import suppress
5
6
  from typing import Any, ClassVar
6
7
 
7
8
  from pyplumio.const import (
@@ -53,12 +54,7 @@ class SensorDataMessage(Message):
53
54
  def decode_message(self, message: bytearray) -> dict[str, Any]:
54
55
  """Decode a frame message."""
55
56
  sensors, offset = FrameVersionsStructure(self).decode(message, offset=0)
56
- try:
57
- sensors[ATTR_STATE] = message[offset]
58
- sensors[ATTR_STATE] = DeviceState(sensors[ATTR_STATE])
59
- except ValueError:
60
- pass
61
-
57
+ sensors[ATTR_STATE] = message[offset]
62
58
  sensors, offset = OutputsStructure(self).decode(message, offset + 1, sensors)
63
59
  sensors, offset = OutputFlagsStructure(self).decode(message, offset, sensors)
64
60
  sensors, offset = TemperaturesStructure(self).decode(message, offset, sensors)
@@ -79,5 +75,7 @@ class SensorDataMessage(Message):
79
75
  message, offset, sensors
80
76
  )
81
77
  sensors, offset = MixerSensorsStructure(self).decode(message, offset, sensors)
78
+ with suppress(ValueError):
79
+ sensors[ATTR_STATE] = DeviceState(sensors[ATTR_STATE])
82
80
 
83
81
  return {ATTR_SENSORS: sensors}
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
  import socket
7
7
  import struct
8
- from typing import Any, ClassVar, Final
8
+ from typing import Any, ClassVar, Final, TypeVar
9
9
 
10
10
 
11
11
  class DataType(ABC):
@@ -25,21 +25,19 @@ class DataType(ABC):
25
25
  """Return serializable string representation of the class."""
26
26
  return f"{self.__class__.__name__}(value={self._value})"
27
27
 
28
- def __eq__(self, other: Any) -> bool:
28
+ def __eq__(self, other: object) -> bool:
29
29
  """Compare if this data type is equal to other."""
30
30
  if isinstance(other, DataType):
31
- result: bool = self._value == other._value
32
- else:
33
- result = self._value == other
31
+ return bool(self._value == other._value)
34
32
 
35
- return result
33
+ return bool(self._value == other)
36
34
 
37
35
  def _slice_data(self, data: bytes) -> bytes:
38
36
  """Slice the data to data type size."""
39
37
  return data[: self.size] if self.size is not None else data
40
38
 
41
39
  @classmethod
42
- def from_bytes(cls, data: bytes, offset: int = 0) -> DataType:
40
+ def from_bytes(cls: type[DataTypeT], data: bytes, offset: int = 0) -> DataTypeT:
43
41
  """Initialize a new data type from bytes."""
44
42
  data_type = cls()
45
43
  data_type.unpack(data[offset:])
@@ -68,6 +66,9 @@ class DataType(ABC):
68
66
  """Unpack the data."""
69
67
 
70
68
 
69
+ DataTypeT = TypeVar("DataTypeT", bound=DataType)
70
+
71
+
71
72
  class Undefined(DataType):
72
73
  """Represents an undefined."""
73
74