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

1# mypy: warn_unused_ignores=False 

2"""Base coding template abstract class and shared constants. 

3 

4All coding templates MUST inherit from CodingTemplate defined here. 

5""" 

6 

7from __future__ import annotations 

8 

9from abc import ABC, abstractmethod 

10from typing import Any, Generic, TypeVar, get_args 

11 

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 

16 

17# ============================================================================= 

18# TYPE VARIABLES 

19# ============================================================================= 

20 

21# Type variable for CodingTemplate generic - represents the decoded value type 

22T_co = TypeVar("T_co", covariant=True) 

23 

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) 

28 

29# Sentinel for per-class cache of resolved python_type (distinguishes None from "not yet resolved") 

30_SENTINEL = object() 

31 

32 

33# ============================================================================= 

34# LEVEL 4 BASE CLASS 

35# ============================================================================= 

36 

37 

38class CodingTemplate(ABC, Generic[T_co]): 

39 """Abstract base class for coding templates. 

40 

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. 

44 

45 Generic over T_co, the type of value produced by _decode_value. 

46 Concrete templates specify their return type, e.g., CodingTemplate[int]. 

47 

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 """ 

52 

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. 

58 

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) 

64 

65 Returns: 

66 Parsed value of type T_co 

67 

68 """ 

69 

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. 

73 

74 Args: 

75 value: Typed value to encode 

76 validate: Whether to validate ranges (default True) 

77 

78 Returns: 

79 Raw bytes representing the value 

80 

81 """ 

82 

83 @property 

84 @abstractmethod 

85 def data_size(self) -> int: 

86 """Size of data in bytes that this template handles.""" 

87 

88 @property 

89 def extractor(self) -> RawExtractor | None: 

90 """Get the raw byte extractor for pipeline access. 

91 

92 Returns None for complex templates where extraction isn't separable. 

93 """ 

94 return None 

95 

96 @property 

97 def translator(self) -> ValueTranslator[Any] | None: 

98 """Get the value translator for pipeline access. 

99 

100 Returns None for complex templates where translation isn't separable. 

101 """ 

102 return None 

103 

104 @classmethod 

105 def resolve_python_type(cls) -> type | None: 

106 """Resolve the decoded Python type from the template's generic parameter. 

107 

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). 

112 

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] 

119 

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 

130 

131 cls._resolved_python_type = resolved # type: ignore[attr-defined] 

132 return resolved