Coverage for src/bluetooth_sig/gatt/characteristics/custom.py: 89%

45 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-28 01:26 +0000

1"""Custom characteristic base class with auto-registration support.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any, ClassVar 

6 

7from ...core.registration import RegistrationManager 

8from ...types import CharacteristicInfo 

9from .base import BaseCharacteristic, ValidationConfig 

10 

11 

12class CustomBaseCharacteristic(BaseCharacteristic[Any]): 

13 r"""Helper base class for custom characteristic implementations. 

14 

15 This class provides a wrapper around physical BLE characteristics that are not 

16 defined in the Bluetooth SIG specification. It supports both manual info passing 

17 and automatic class-level _info binding via __init_subclass__. 

18 

19 Auto-Registration: 

20 Custom characteristics automatically register with the global 

21 characteristic registry via :class:`~bluetooth_sig.core.registration.RegistrationManager` 

22 when first instantiated. No manual registration needed! 

23 

24 Examples: 

25 >>> from bluetooth_sig.types.data_types import CharacteristicInfo 

26 >>> from bluetooth_sig.types.uuid import BluetoothUUID 

27 >>> class MyCharacteristic(CustomBaseCharacteristic): 

28 ... _info = CharacteristicInfo(uuid=BluetoothUUID("AAAA"), name="My Char") 

29 >>> # Auto-registers with singleton on first instantiation 

30 >>> char = MyCharacteristic() # Auto-registered! 

31 >>> # Now accessible via the global translator 

32 >>> from bluetooth_sig import BluetoothSIGTranslator 

33 >>> translator = BluetoothSIGTranslator.get_instance() 

34 >>> result = translator.parse_characteristic("AAAA", b"\x42") 

35 """ 

36 

37 _is_custom = True 

38 _is_base_class = True # Exclude from registry validation tests 

39 _configured_info: CharacteristicInfo | None = None # Stores class-level _info 

40 _allows_sig_override = False # Default: no SIG override permission 

41 _registry_tracker: ClassVar[set[str]] = set() # Track registered UUIDs to avoid duplicates 

42 

43 @classmethod 

44 def get_configured_info(cls) -> CharacteristicInfo | None: 

45 """Get the class-level configured CharacteristicInfo. 

46 

47 Returns: 

48 CharacteristicInfo if configured, None otherwise 

49 

50 """ 

51 return cls._configured_info 

52 

53 # pylint: disable=duplicate-code 

54 # NOTE: __init_subclass__ and __init__ patterns are intentionally similar to CustomBaseService. 

55 # This is by design - both custom characteristic and service classes need identical validation 

56 # and info management patterns. Consolidation not possible due to different base types and info types. 

57 def __init_subclass__(cls, allow_sig_override: bool = False, **kwargs: Any) -> None: # noqa: ANN401 # Receives subclass kwargs 

58 """Automatically set up _info if provided as class attribute. 

59 

60 Args: 

61 allow_sig_override: Set to True when intentionally overriding SIG UUIDs. 

62 **kwargs: Additional subclass keyword arguments passed by callers or 

63 metaclasses; these are accepted for compatibility and ignored 

64 unless explicitly handled. 

65 

66 Raises: 

67 ValueError: If class uses SIG UUID without override permission. 

68 

69 """ 

70 super().__init_subclass__(**kwargs) 

71 

72 # Store override permission for registry validation 

73 cls._allows_sig_override = allow_sig_override 

74 

75 # If class has _info attribute, validate and store it 

76 if hasattr(cls, "_info"): 

77 info = getattr(cls, "_info", None) 

78 if info is not None: 

79 # Check for SIG UUID override (unless explicitly allowed) 

80 if not allow_sig_override and info.uuid.is_sig_characteristic(): 

81 raise ValueError( 

82 f"{cls.__name__} uses SIG UUID {info.uuid} without override flag. " 

83 "Use custom UUID or add allow_sig_override=True parameter." 

84 ) 

85 

86 cls._configured_info = info 

87 

88 def __init__( 

89 self, 

90 info: CharacteristicInfo | None = None, 

91 auto_register: bool = True, 

92 validation: ValidationConfig | None = None, 

93 ) -> None: 

94 """Initialize a custom characteristic with automatic _info resolution and registration. 

95 

96 Args: 

97 info: Optional override for class-configured _info 

98 auto_register: If True (default), register via RegistrationManager on first init 

99 validation: Validation constraints configuration (optional) 

100 

101 Raises: 

102 ValueError: If no valid info available from class or parameter 

103 

104 Examples: 

105 >>> # Simple usage - auto-registers with global registry 

106 >>> char = MyCharacteristic() # Auto-registered! 

107 >>> # Opt-out of auto-registration if needed 

108 >>> char = MyCharacteristic(auto_register=False) 

109 

110 """ 

111 # Use provided info, or fall back to class-configured _info 

112 final_info = info or self.__class__.get_configured_info() 

113 

114 if not final_info: 

115 raise ValueError(f"{self.__class__.__name__} requires either 'info' parameter or '_info' class attribute") 

116 

117 if not final_info.uuid or str(final_info.uuid) == "0000": 

118 raise ValueError("Valid UUID is required for custom characteristics") 

119 

120 # Auto-register if requested and not already registered 

121 if auto_register: 

122 uuid_str = str(final_info.uuid) 

123 

124 if uuid_str not in CustomBaseCharacteristic._registry_tracker: 

125 RegistrationManager.register_custom_characteristic_class( 

126 uuid_str, 

127 self.__class__, 

128 info=final_info, 

129 override=True, 

130 ) 

131 CustomBaseCharacteristic._registry_tracker.add(uuid_str) 

132 

133 # Call parent constructor with our info to maintain consistency 

134 super().__init__(info=final_info, validation=validation) 

135 

136 def __post_init__(self) -> None: 

137 """Override BaseCharacteristic.__post_init__ to use custom info management. 

138 

139 CustomBaseCharacteristic manages _info manually from provided or configured info, 

140 bypassing SIG resolution that would fail for custom characteristics. 

141 Then delegates to parent for remaining initialization. 

142 """ 

143 # Use provided info if available (from manual override), otherwise use configured info 

144 final_info = None 

145 if hasattr(self, "_provided_info") and self._provided_info: 

146 final_info = self._provided_info 

147 else: 

148 configured_info = self.__class__.get_configured_info() 

149 if configured_info: 

150 final_info = configured_info 

151 else: 

152 # This shouldn't happen if class setup is correct 

153 raise ValueError(f"CustomBaseCharacteristic {self.__class__.__name__} has no valid info source") 

154 

155 # Set _provided_info so parent __post_init__ will use it instead of trying SIG resolution 

156 self._provided_info = final_info 

157 

158 # Call parent to complete initialization (sets up _special_resolver, applies overrides, etc.) 

159 # This is critical - without it, _special_resolver won't exist 

160 super().__post_init__()