Coverage for src / bluetooth_sig / gatt / characteristics / templates / base.py: 98%
43 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1# mypy: warn_unused_ignores=False
2"""Base coding template abstract class and shared constants.
4All coding templates MUST inherit from CodingTemplate defined here.
5"""
7from __future__ import annotations
9from abc import ABC, abstractmethod
10from typing import Any, Generic, TypeVar, get_args
12from ....types.gatt_enums import AdjustReason, DayOfWeek # noqa: F401 # Re-export for sub-modules
13from ...context import CharacteristicContext
14from ..utils.extractors import RawExtractor
15from ..utils.translators import ValueTranslator
17# =============================================================================
18# TYPE VARIABLES
19# =============================================================================
21# Type variable for CodingTemplate generic - represents the decoded value type
22T_co = TypeVar("T_co", covariant=True)
24# Resolution constants for common measurement scales
25_RESOLUTION_INTEGER = 1.0 # Integer resolution (10^0)
26_RESOLUTION_TENTH = 0.1 # 0.1 resolution (10^-1)
27_RESOLUTION_HUNDREDTH = 0.01 # 0.01 resolution (10^-2)
29# Sentinel for per-class cache of resolved python_type (distinguishes None from "not yet resolved")
30_SENTINEL = object()
33# =============================================================================
34# LEVEL 4 BASE CLASS
35# =============================================================================
38class CodingTemplate(ABC, Generic[T_co]):
39 """Abstract base class for coding templates.
41 Templates are pure coding utilities that don't inherit from BaseCharacteristic.
42 They provide coding strategies that can be injected into characteristics.
43 All templates MUST inherit from this base class and implement the required methods.
45 Generic over T_co, the type of value produced by _decode_value.
46 Concrete templates specify their return type, e.g., CodingTemplate[int].
48 Pipeline Integration:
49 Simple templates (single-field) expose `extractor` and `translator` properties
50 for the decode/encode pipeline. Complex templates return None for these properties.
51 """
53 @abstractmethod
54 def decode_value(
55 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
56 ) -> T_co:
57 """Decode raw bytes to typed value.
59 Args:
60 data: Raw bytes to parse
61 offset: Byte offset to start parsing from
62 ctx: Optional context for parsing
63 validate: Whether to validate ranges (default True)
65 Returns:
66 Parsed value of type T_co
68 """
70 @abstractmethod
71 def encode_value(self, value: T_co, *, validate: bool = True) -> bytearray: # type: ignore[misc] # Covariant T_co in parameter is intentional for encode/decode symmetry
72 """Encode typed value to raw bytes.
74 Args:
75 value: Typed value to encode
76 validate: Whether to validate ranges (default True)
78 Returns:
79 Raw bytes representing the value
81 """
83 @property
84 @abstractmethod
85 def data_size(self) -> int:
86 """Size of data in bytes that this template handles."""
88 @property
89 def extractor(self) -> RawExtractor | None:
90 """Get the raw byte extractor for pipeline access.
92 Returns None for complex templates where extraction isn't separable.
93 """
94 return None
96 @property
97 def translator(self) -> ValueTranslator[Any] | None:
98 """Get the value translator for pipeline access.
100 Returns None for complex templates where translation isn't separable.
101 """
102 return None
104 @classmethod
105 def resolve_python_type(cls) -> type | None:
106 """Resolve the decoded Python type from the template's generic parameter.
108 Walks the MRO to find the concrete type argument bound to
109 ``CodingTemplate[T_co]``. Returns ``None`` when the parameter is
110 still an unbound ``TypeVar`` (e.g. ``EnumTemplate[T]`` before
111 instantiation with a concrete enum).
113 The result is cached per-class in ``_resolved_python_type`` to avoid
114 repeated MRO introspection.
115 """
116 cached = cls.__dict__.get("_resolved_python_type", _SENTINEL)
117 if cached is not _SENTINEL:
118 return cached # type: ignore[no-any-return]
120 resolved: type | None = None
121 for klass in cls.__mro__:
122 for base in getattr(klass, "__orig_bases__", ()):
123 if getattr(base, "__origin__", None) is CodingTemplate:
124 args = get_args(base)
125 if args and not isinstance(args[0], TypeVar):
126 resolved = args[0]
127 break
128 if resolved is not None:
129 break
131 cls._resolved_python_type = resolved # type: ignore[attr-defined]
132 return resolved