Coverage for src / bluetooth_sig / gatt / characteristics / characteristic_meta.py: 94%

77 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Helper classes for GATT characteristic infrastructure. 

2 

3Contains the SIG resolver, validation configuration, and metaclass used by 

4:class:`BaseCharacteristic`. Extracted to keep the base module focused on 

5the characteristic API itself. 

6""" 

7 

8from __future__ import annotations 

9 

10from abc import ABCMeta 

11from typing import Any 

12 

13import msgspec 

14 

15from ...registry.uuids.units import units_registry 

16from ...types import CharacteristicInfo 

17from ...types.gatt_enums import WIRE_TYPE_MAP 

18from ...types.registry import CharacteristicSpec 

19from ..exceptions import UUIDResolutionError 

20from ..resolver import CharacteristicRegistrySearch, NameNormalizer, NameVariantGenerator 

21from ..uuid_registry import uuid_registry 

22 

23# --------------------------------------------------------------------------- 

24# Validation configuration 

25# --------------------------------------------------------------------------- 

26 

27 

28class ValidationConfig(msgspec.Struct, kw_only=True): 

29 """Configuration for characteristic validation constraints. 

30 

31 Groups validation parameters into a single, optional configuration object 

32 to simplify BaseCharacteristic constructor signatures. 

33 """ 

34 

35 min_value: int | float | None = None 

36 max_value: int | float | None = None 

37 expected_length: int | None = None 

38 min_length: int | None = None 

39 max_length: int | None = None 

40 allow_variable_length: bool = False 

41 expected_type: type | None = None 

42 

43 

44# --------------------------------------------------------------------------- 

45# SIG characteristic resolver 

46# --------------------------------------------------------------------------- 

47 

48 

49class SIGCharacteristicResolver: 

50 """Resolves SIG characteristic information from YAML and registry. 

51 

52 This class handles all SIG characteristic resolution logic, separating 

53 concerns from the BaseCharacteristic constructor. Uses shared utilities 

54 from the resolver module to avoid code duplication. 

55 """ 

56 

57 camel_case_to_display_name = staticmethod(NameNormalizer.camel_case_to_display_name) 

58 

59 @staticmethod 

60 def resolve_for_class(char_class: type) -> CharacteristicInfo: 

61 """Resolve CharacteristicInfo for a SIG characteristic class. 

62 

63 Args: 

64 char_class: The characteristic class to resolve info for. 

65 

66 Returns: 

67 CharacteristicInfo with resolved UUID, name, value_type, unit. 

68 

69 Raises: 

70 UUIDResolutionError: If no UUID can be resolved for the class. 

71 

72 """ 

73 yaml_spec = SIGCharacteristicResolver.resolve_yaml_spec_for_class(char_class) 

74 if yaml_spec: 

75 return SIGCharacteristicResolver._create_info_from_yaml(yaml_spec, char_class) 

76 

77 registry_info = SIGCharacteristicResolver.resolve_from_registry(char_class) 

78 if registry_info: 

79 return registry_info 

80 

81 raise UUIDResolutionError(char_class.__name__, [char_class.__name__]) 

82 

83 @staticmethod 

84 def resolve_yaml_spec_for_class(char_class: type) -> CharacteristicSpec | None: 

85 """Resolve YAML spec for a characteristic class using shared name variant logic.""" 

86 characteristic_name = getattr(char_class, "_characteristic_name", None) 

87 names_to_try = NameVariantGenerator.generate_characteristic_variants(char_class.__name__, characteristic_name) 

88 

89 for try_name in names_to_try: 

90 spec = uuid_registry.resolve_characteristic_spec(try_name) 

91 if spec: 

92 return spec 

93 

94 return None 

95 

96 @staticmethod 

97 def _create_info_from_yaml(yaml_spec: CharacteristicSpec, char_class: type) -> CharacteristicInfo: 

98 """Create CharacteristicInfo from YAML spec, resolving metadata via registry classes.""" 

99 python_type: type | None = None 

100 is_bitfield = False 

101 

102 if yaml_spec.data_type: 

103 raw_dt = yaml_spec.data_type.lower().strip() 

104 

105 # boolean[N] (N > 1) → bitfield stored as int 

106 if raw_dt.startswith("boolean["): 

107 python_type = int 

108 is_bitfield = True 

109 elif raw_dt == "struct": 

110 python_type = dict 

111 else: 

112 # Strip range/array qualifiers: "uint16 [1-256]" → "uint16" 

113 base_dt = raw_dt.split("[")[0].split(" ")[0] 

114 python_type = WIRE_TYPE_MAP.get(base_dt) 

115 elif yaml_spec.structure and len(yaml_spec.structure) > 1: 

116 # Multi-field characteristics decode to dict 

117 python_type = dict 

118 

119 unit_info = None 

120 unit_name = getattr(yaml_spec, "unit_symbol", None) or getattr(yaml_spec, "unit", None) 

121 if unit_name: 

122 unit_info = units_registry.get_unit_info_by_name(unit_name) 

123 if unit_info: 

124 unit_symbol = str(getattr(unit_info, "symbol", getattr(unit_info, "name", unit_name))) 

125 else: 

126 unit_symbol = str(unit_name or "") 

127 

128 return CharacteristicInfo( 

129 uuid=yaml_spec.uuid, 

130 name=yaml_spec.name or char_class.__name__, 

131 unit=unit_symbol, 

132 python_type=python_type, 

133 is_bitfield=is_bitfield, 

134 ) 

135 

136 @staticmethod 

137 def resolve_from_registry(char_class: type) -> CharacteristicInfo | None: 

138 """Fallback to registry resolution using shared search strategy.""" 

139 search_strategy = CharacteristicRegistrySearch() 

140 characteristic_name = getattr(char_class, "_characteristic_name", None) 

141 return search_strategy.search(char_class, characteristic_name) 

142 

143 

144# --------------------------------------------------------------------------- 

145# Metaclass 

146# --------------------------------------------------------------------------- 

147 

148 

149class CharacteristicMeta(ABCMeta): 

150 """Metaclass to automatically handle template flags for characteristics.""" 

151 

152 def __new__( 

153 mcs, 

154 name: str, 

155 bases: tuple[type, ...], 

156 namespace: dict[str, Any], 

157 **kwargs: Any, # noqa: ANN401 # Metaclass receives arbitrary keyword arguments 

158 ) -> type: 

159 """Create the characteristic class and handle template markers. 

160 

161 This metaclass hook ensures template classes and concrete 

162 implementations are correctly annotated with the ``_is_template`` 

163 attribute before the class object is created. 

164 """ 

165 if bases: 

166 module_name = namespace.get("__module__", "") 

167 is_in_templates = "templates" in module_name 

168 

169 if not is_in_templates and not namespace.get("_is_template_override"): 

170 has_template_parent = any(getattr(base, "_is_template", False) for base in bases) 

171 if has_template_parent and "_is_template" not in namespace: 

172 namespace["_is_template"] = False 

173 

174 return super().__new__(mcs, name, bases, namespace, **kwargs)