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
« 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."""
3from __future__ import annotations
5from typing import Any, ClassVar
7from ...core.registration import RegistrationManager
8from ...types import CharacteristicInfo
9from .base import BaseCharacteristic, ValidationConfig
12class CustomBaseCharacteristic(BaseCharacteristic[Any]):
13 r"""Helper base class for custom characteristic implementations.
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__.
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!
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 """
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
43 @classmethod
44 def get_configured_info(cls) -> CharacteristicInfo | None:
45 """Get the class-level configured CharacteristicInfo.
47 Returns:
48 CharacteristicInfo if configured, None otherwise
50 """
51 return cls._configured_info
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.
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.
66 Raises:
67 ValueError: If class uses SIG UUID without override permission.
69 """
70 super().__init_subclass__(**kwargs)
72 # Store override permission for registry validation
73 cls._allows_sig_override = allow_sig_override
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 )
86 cls._configured_info = info
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.
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)
101 Raises:
102 ValueError: If no valid info available from class or parameter
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)
110 """
111 # Use provided info, or fall back to class-configured _info
112 final_info = info or self.__class__.get_configured_info()
114 if not final_info:
115 raise ValueError(f"{self.__class__.__name__} requires either 'info' parameter or '_info' class attribute")
117 if not final_info.uuid or str(final_info.uuid) == "0000":
118 raise ValueError("Valid UUID is required for custom characteristics")
120 # Auto-register if requested and not already registered
121 if auto_register:
122 uuid_str = str(final_info.uuid)
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)
133 # Call parent constructor with our info to maintain consistency
134 super().__init__(info=final_info, validation=validation)
136 def __post_init__(self) -> None:
137 """Override BaseCharacteristic.__post_init__ to use custom info management.
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")
155 # Set _provided_info so parent __post_init__ will use it instead of trying SIG resolution
156 self._provided_info = final_info
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__()