From 70e6743452e70d4b5662ce8c9a3ea5ebd0b578df Mon Sep 17 00:00:00 2001 From: Ronan Byrne Date: Fri, 3 Apr 2026 20:07:58 +0100 Subject: [PATCH 1/2] prewarm other registries --- src/bluetooth_sig/utils/prewarm.py | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/bluetooth_sig/utils/prewarm.py b/src/bluetooth_sig/utils/prewarm.py index e9e7e481..9bc2fead 100644 --- a/src/bluetooth_sig/utils/prewarm.py +++ b/src/bluetooth_sig/utils/prewarm.py @@ -13,6 +13,23 @@ from ..gatt.services.registry import GattServiceRegistry from ..registry.company_identifiers import company_identifiers_registry from ..registry.core.ad_types import ad_types_registry +from ..registry.core.appearance_values import appearance_values_registry +from ..registry.core.class_of_device import class_of_device_registry +from ..registry.core.coding_format import coding_format_registry +from ..registry.core.formattypes import format_types_registry +from ..registry.core.namespace_description import namespace_description_registry +from ..registry.core.uri_schemes import uri_schemes_registry +from ..registry.profiles.permitted_characteristics import permitted_characteristics_registry +from ..registry.profiles.profile_lookup import profile_lookup_registry +from ..registry.service_discovery.attribute_ids import service_discovery_attribute_registry +from ..registry.uuids.browse_groups import browse_groups_registry +from ..registry.uuids.declarations import declarations_registry +from ..registry.uuids.members import members_registry +from ..registry.uuids.mesh_profiles import mesh_profiles_registry +from ..registry.uuids.object_types import object_types_registry +from ..registry.uuids.protocol_identifiers import protocol_identifiers_registry +from ..registry.uuids.sdo_uuids import sdo_uuids_registry +from ..registry.uuids.service_classes import service_classes_registry from ..registry.uuids.units import units_registry from ..types.uuid import BluetoothUUID @@ -33,6 +50,23 @@ def prewarm_registries() -> None: - Units registry (unit UUID → symbol mapping) - Company identifiers registry (manufacturer ID → name) - AD types registry (advertising data type codes) + - Appearance values registry (appearance code → device type) + - Class of device registry (CoD bitfield → device type) + - Coding format registry (LE Audio codec identifiers) + - Format types registry (characteristic data format codes) + - Namespace description registry (CPF description field values) + - URI schemes registry (beacon URI scheme codes) + - Permitted characteristics registry (profile characteristic constraints) + - Profile lookup registry (profile parameter tables) + - Service discovery attribute registry (SDP attribute identifiers) + - Browse groups registry (SDP browse group UUIDs) + - Declarations registry (GATT declaration UUIDs) + - Members registry (Bluetooth member organisation UUIDs) + - Mesh profiles registry (mesh profile UUIDs) + - Object types registry (OTS object type UUIDs) + - Protocol identifiers registry (protocol UUID identifiers) + - SDO UUIDs registry (standards body UUIDs) + - Service classes registry (service class UUIDs) """ CharacteristicRegistry.get_all_characteristics() GattServiceRegistry.get_all_services() @@ -44,5 +78,22 @@ def prewarm_registries() -> None: units_registry.ensure_loaded() company_identifiers_registry.ensure_loaded() ad_types_registry.ensure_loaded() + appearance_values_registry.ensure_loaded() + class_of_device_registry.ensure_loaded() + coding_format_registry.ensure_loaded() + format_types_registry.ensure_loaded() + namespace_description_registry.ensure_loaded() + uri_schemes_registry.ensure_loaded() + permitted_characteristics_registry.ensure_loaded() + profile_lookup_registry.ensure_loaded() + service_discovery_attribute_registry.ensure_loaded() + browse_groups_registry.ensure_loaded() + declarations_registry.ensure_loaded() + members_registry.ensure_loaded() + mesh_profiles_registry.ensure_loaded() + object_types_registry.ensure_loaded() + protocol_identifiers_registry.ensure_loaded() + sdo_uuids_registry.ensure_loaded() + service_classes_registry.ensure_loaded() logger.debug("bluetooth-sig registries pre-warmed") From 77aef30f12cae696ca9754ff60ed97791b116f90 Mon Sep 17 00:00:00 2001 From: Ronan Byrne <22995167+RonanB96@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:12:38 +0100 Subject: [PATCH 2/2] Improve registry loading and import side effects --- .gitignore | 2 + .../explanation/architecture/decisions.md | 7 ++ .../architecture/registry-system.md | 6 +- docs/source/how-to/advertising-parsing.md | 8 +- docs/source/performance/performance-data.md | 11 +++ src/bluetooth_sig/advertising/__init__.py | 4 +- src/bluetooth_sig/advertising/base.py | 4 +- src/bluetooth_sig/advertising/pdu_parser.py | 12 +-- src/bluetooth_sig/advertising/registry.py | 24 ++++- src/bluetooth_sig/core/query.py | 6 +- src/bluetooth_sig/core/registration.py | 6 +- src/bluetooth_sig/gatt/__init__.py | 4 +- .../gatt/characteristics/appearance.py | 4 +- .../characteristics/characteristic_meta.py | 8 +- .../gatt/characteristics/context_lookup.py | 4 +- .../gatt/characteristics/preferred_units.py | 6 +- .../gatt/characteristics/registry.py | 10 +- src/bluetooth_sig/gatt/descriptors/base.py | 4 +- .../characteristic_presentation_format.py | 12 +-- src/bluetooth_sig/gatt/resolver.py | 8 +- src/bluetooth_sig/gatt/services/base.py | 18 ++-- src/bluetooth_sig/gatt/services/registry.py | 4 +- src/bluetooth_sig/gatt/uuid_registry.py | 55 +++++++++-- src/bluetooth_sig/registry/base.py | 35 +++---- .../registry/company_identifiers/__init__.py | 4 +- .../company_identifiers_registry.py | 4 +- src/bluetooth_sig/registry/core/__init__.py | 20 ++-- src/bluetooth_sig/registry/core/ad_types.py | 4 +- .../registry/core/appearance_values.py | 5 +- .../registry/core/class_of_device.py | 4 +- .../registry/core/coding_format.py | 8 +- .../registry/core/formattypes.py | 4 +- .../registry/core/namespace_description.py | 8 +- .../registry/core/uri_schemes.py | 8 +- src/bluetooth_sig/registry/gss.py | 18 +--- .../registry/profiles/__init__.py | 8 +- .../profiles/permitted_characteristics.py | 4 +- .../registry/profiles/profile_lookup.py | 4 +- .../registry/service_discovery/__init__.py | 4 +- .../service_discovery/attribute_ids.py | 4 +- src/bluetooth_sig/registry/uuids/__init__.py | 36 +++---- .../registry/uuids/browse_groups.py | 4 +- .../registry/uuids/declarations.py | 4 +- src/bluetooth_sig/registry/uuids/members.py | 4 +- .../registry/uuids/mesh_profiles.py | 4 +- .../registry/uuids/object_types.py | 4 +- .../registry/uuids/protocol_identifiers.py | 6 +- src/bluetooth_sig/registry/uuids/sdo_uuids.py | 4 +- .../registry/uuids/service_classes.py | 4 +- src/bluetooth_sig/registry/uuids/units.py | 7 +- src/bluetooth_sig/types/appearance.py | 4 +- src/bluetooth_sig/types/company.py | 4 +- src/bluetooth_sig/types/uri.py | 4 +- src/bluetooth_sig/utils/prewarm.py | 55 +---------- src/bluetooth_sig/utils/prewarm_catalog.py | 95 +++++++++++++++++++ tests/benchmarks/conftest.py | 10 +- tests/benchmarks/test_performance.py | 24 +++++ tests/conftest.py | 10 +- tests/gatt/test_uuid_registry_lifecycle.py | 63 ++++++++++++ tests/integration/test_custom_registration.py | 20 ++-- .../test_format_types_integration.py | 28 +++--- tests/registry/core/test_coding_format.py | 4 +- tests/registry/core/test_formattypes.py | 10 +- .../core/test_namespace_description.py | 4 +- tests/registry/core/test_uri_schemes.py | 4 +- tests/registry/test_ad_types.py | 10 +- tests/registry/test_protocol_identifiers.py | 64 ++++++------- tests/registry/test_registry_validation.py | 10 +- tests/registry/test_uri_schemes.py | 30 +++--- tests/registry/test_yaml_units.py | 12 +-- ...st_characteristic_registry_completeness.py | 8 +- .../test_service_registry_completeness.py | 8 +- .../test_yaml_implementation_coverage.py | 8 +- tests/test_browse_groups_registry.py | 38 ++++---- tests/test_declarations_registry.py | 46 ++++----- tests/test_members_registry.py | 32 +++---- tests/test_mesh_profiles_registry.py | 34 +++---- tests/test_object_types_registry.py | 39 ++++---- tests/test_sdo_uuids_registry.py | 40 ++++---- tests/test_service_classes_registry.py | 38 ++++---- tests/test_units_registry.py | 34 +++---- tests/utils/test_prewarm.py | 13 +++ 82 files changed, 756 insertions(+), 497 deletions(-) create mode 100644 src/bluetooth_sig/utils/prewarm_catalog.py create mode 100644 tests/gatt/test_uuid_registry_lifecycle.py diff --git a/.gitignore b/.gitignore index 1e87f615..e68c8108 100644 --- a/.gitignore +++ b/.gitignore @@ -239,3 +239,5 @@ benchmark-history/ docs/source/_static/performance/*.json docs/benchmarks/*.json.cache/gen_ref/ src/bluetooth_sig/_version.py + +.tmp/ diff --git a/docs/source/explanation/architecture/decisions.md b/docs/source/explanation/architecture/decisions.md index 98c61716..7ade8338 100644 --- a/docs/source/explanation/architecture/decisions.md +++ b/docs/source/explanation/architecture/decisions.md @@ -241,6 +241,13 @@ User's BLE Library → bytes → bluetooth-sig → structured data - ✅ Consistent state across application - ⚠️ Global state (acceptable for read-only registry data) +**Implementation note**: + +- Access singleton registries via explicit getter/class methods (for example + ``get_uuid_registry()`` and ``get_instance()``) instead of relying on + import-time module globals. +- Importing registry modules must be side-effect free with respect to YAML I/O. + ## Summary These architectural decisions prioritize: diff --git a/docs/source/explanation/architecture/registry-system.md b/docs/source/explanation/architecture/registry-system.md index 42b0bdb4..1a77debf 100644 --- a/docs/source/explanation/architecture/registry-system.md +++ b/docs/source/explanation/architecture/registry-system.md @@ -227,7 +227,11 @@ The registry loading process follows these steps: 5. **Store canonically**: Index by normalized UUID in main dictionaries 6. **Generate aliases**: Create name-based lookup mappings -The loading is performed only once per application lifetime, with graceful degradation if YAML files are missing. See {py:class}`~bluetooth_sig.gatt.uuid_registry.UuidRegistry` for the complete implementation. +The loading is performed only once per application lifetime. Load failures are +surfaced deterministically (sticky failure state) rather than silently +continuing with partial empty data. See +{py:class}`~bluetooth_sig.gatt.uuid_registry.UuidRegistry` for the complete +implementation. ## Alias System diff --git a/docs/source/how-to/advertising-parsing.md b/docs/source/how-to/advertising-parsing.md index 3006939c..98acbb72 100644 --- a/docs/source/how-to/advertising-parsing.md +++ b/docs/source/how-to/advertising-parsing.md @@ -331,7 +331,7 @@ Interpreters are automatically registered when defined (via `__init_subclass__`) from bluetooth_sig.advertising import ( AdvertisingPDUParser, DeviceAdvertisingState, - payload_interpreter_registry, + get_payload_interpreter_registry, ) from bluetooth_sig.advertising.base import AdvertisingData @@ -367,7 +367,7 @@ advertising_data = AdvertisingData( ) # Find interpreter for this advertisement -interpreter_class = payload_interpreter_registry.find_interpreter_class(advertising_data) +interpreter_class = get_payload_interpreter_registry().find_interpreter_class(advertising_data) if interpreter_class: # Create interpreter instance for this device @@ -435,10 +435,10 @@ class StatefulInterpreter(PayloadInterpreter[MySensorData]): ```python # SKIP: Depends on MySensorInterpreter class defined in previous examples -from bluetooth_sig.advertising import payload_interpreter_registry +from bluetooth_sig.advertising import get_payload_interpreter_registry # Remove a specific interpreter -payload_interpreter_registry.unregister(MySensorInterpreter) +get_payload_interpreter_registry().unregister(MySensorInterpreter) ``` ## See Also diff --git a/docs/source/performance/performance-data.md b/docs/source/performance/performance-data.md index 37042b06..db7a2b99 100644 --- a/docs/source/performance/performance-data.md +++ b/docs/source/performance/performance-data.md @@ -26,6 +26,17 @@ The library is optimized for typical BLE use cases: periodic sensor reads, on-de **Note**: UUID resolution dominates parsing overhead. Once registries are loaded, lookups are consistently fast (~190 μs). +### Registry Lifecycle Paths + +| Operation | Mean | StdDev | Description | +|-----------|------|--------|-------------| +| UUID registry cold load | environment-dependent | n/a | First lookup from a fresh `UuidRegistry()` instance triggers YAML load | +| UUID registry warm lookup | ~190 μs | ±10 μs | Lookup after explicit `ensure_loaded()` | + +Cold-load numbers vary based on filesystem and environment. Track regression +by comparing trends on the same CI runner class rather than hard-coding one +absolute number. + ### Characteristic Parsing | Characteristic Type | Mean | StdDev | Description | diff --git a/src/bluetooth_sig/advertising/__init__.py b/src/bluetooth_sig/advertising/__init__.py index 89fa8b3d..a78e4e58 100644 --- a/src/bluetooth_sig/advertising/__init__.py +++ b/src/bluetooth_sig/advertising/__init__.py @@ -49,8 +49,8 @@ from bluetooth_sig.advertising.registry import ( PayloadContext, PayloadInterpreterRegistry, + get_payload_interpreter_registry, parse_advertising_payloads, - payload_interpreter_registry, ) from bluetooth_sig.advertising.service_data_parser import ServiceDataParser from bluetooth_sig.advertising.service_resolver import ( @@ -98,11 +98,11 @@ "SIGCharacteristicInterpreter", "ServiceDataParser", "UnsupportedVersionError", + "get_payload_interpreter_registry", "build_ead_nonce", "bytes_to_mac_address", "decrypt_ead", "decrypt_ead_from_raw", "mac_address_to_bytes", "parse_advertising_payloads", - "payload_interpreter_registry", ] diff --git a/src/bluetooth_sig/advertising/base.py b/src/bluetooth_sig/advertising/base.py index de5f4ef4..f7330928 100644 --- a/src/bluetooth_sig/advertising/base.py +++ b/src/bluetooth_sig/advertising/base.py @@ -163,9 +163,9 @@ def __init_subclass__(cls, **kwargs: object) -> None: return # Lazy import to avoid circular dependency at module load time - from bluetooth_sig.advertising.registry import payload_interpreter_registry # noqa: PLC0415 + from bluetooth_sig.advertising.registry import get_payload_interpreter_registry # noqa: PLC0415 - payload_interpreter_registry.register(cls) + get_payload_interpreter_registry().register(cls) @classmethod @abstractmethod diff --git a/src/bluetooth_sig/advertising/pdu_parser.py b/src/bluetooth_sig/advertising/pdu_parser.py index 80b40126..56b875a4 100644 --- a/src/bluetooth_sig/advertising/pdu_parser.py +++ b/src/bluetooth_sig/advertising/pdu_parser.py @@ -15,9 +15,9 @@ from bluetooth_sig.gatt.characteristics.utils import DataParser from bluetooth_sig.gatt.constants import SIZE_UINT16, SIZE_UINT24, SIZE_UINT32, SIZE_UINT48, SIZE_UUID128 -from bluetooth_sig.registry.core.ad_types import ad_types_registry -from bluetooth_sig.registry.core.appearance_values import appearance_values_registry -from bluetooth_sig.registry.core.class_of_device import class_of_device_registry +from bluetooth_sig.registry.core.ad_types import get_ad_types_registry +from bluetooth_sig.registry.core.appearance_values import get_appearance_values_registry +from bluetooth_sig.registry.core.class_of_device import get_class_of_device_registry from bluetooth_sig.types import ManufacturerData from bluetooth_sig.types.ad_types_constants import ADType from bluetooth_sig.types.advertising.ad_structures import ( @@ -609,7 +609,7 @@ def _handle_property_ad_types(self, ad_type: int, ad_data: bytes, parsed: Advert parsed.properties.tx_power = int.from_bytes(ad_data[:1], byteorder="little", signed=True) elif ad_type == ADType.APPEARANCE and len(ad_data) >= SIZE_UINT16: raw_value = DataParser.parse_int16(ad_data, 0, signed=False) - appearance_info = appearance_values_registry.get_appearance_info(raw_value) + appearance_info = get_appearance_values_registry().get_appearance_info(raw_value) parsed.properties.appearance = AppearanceData(raw_value=raw_value, info=appearance_info) elif ad_type == ADType.LE_SUPPORTED_FEATURES: parsed.properties.le_supported_features = LEFeatures(raw_value=ad_data) @@ -617,7 +617,7 @@ def _handle_property_ad_types(self, ad_type: int, ad_data: bytes, parsed: Advert parsed.properties.le_role = ad_data[0] elif ad_type == ADType.CLASS_OF_DEVICE and len(ad_data) >= SIZE_UINT24: raw_cod = int.from_bytes(ad_data[:3], byteorder="little", signed=False) - parsed.properties.class_of_device = class_of_device_registry.decode_class_of_device(raw_cod) + parsed.properties.class_of_device = get_class_of_device_registry().decode_class_of_device(raw_cod) elif ad_type == ADType.MANUFACTURER_SPECIFIC_DATA and len(ad_data) >= SIZE_UINT16: self._parse_manufacturer_data(ad_data, parsed) else: @@ -768,7 +768,7 @@ def _parse_ad_structures(self, data: bytes) -> AdvertisingDataStructures: ad_type = data[i + 1] ad_data = data[i + 2 : i + length + 1] - if not ad_types_registry.is_known_ad_type(ad_type): + if not get_ad_types_registry().is_known_ad_type(ad_type): logger.warning("Unknown AD type encountered: 0x%02X", ad_type) # Dispatch to category handlers diff --git a/src/bluetooth_sig/advertising/registry.py b/src/bluetooth_sig/advertising/registry.py index e5ea5862..cc8d2709 100644 --- a/src/bluetooth_sig/advertising/registry.py +++ b/src/bluetooth_sig/advertising/registry.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import threading from typing import Any import msgspec @@ -41,6 +42,18 @@ class PayloadInterpreterRegistry: """ + _instance: PayloadInterpreterRegistry | None = None + _instance_lock = threading.RLock() + + @classmethod + def get_instance(cls) -> PayloadInterpreterRegistry: + """Return the process-wide PayloadInterpreterRegistry singleton instance.""" + if cls._instance is None: + with cls._instance_lock: + if cls._instance is None: + cls._instance = cls() + return cls._instance + def __init__(self) -> None: """Initialise empty registry.""" self._by_service_uuid: dict[str, list[type[PayloadInterpreter[Any]]]] = {} @@ -186,8 +199,9 @@ def clear(self) -> None: self._fallback.clear() -# Global singleton for PayloadInterpreter registration -payload_interpreter_registry = PayloadInterpreterRegistry() +def get_payload_interpreter_registry() -> PayloadInterpreterRegistry: + """Return the process-wide payload interpreter registry singleton instance.""" + return PayloadInterpreterRegistry.get_instance() def parse_advertising_payloads( @@ -208,7 +222,7 @@ def parse_advertising_payloads( service_data: Service UUID → payload bytes mapping. context: Advertisement context (MAC address, RSSI, timestamp). state: Current device advertising state (optional, created if None). - registry: Interpreter registry to use (defaults to global registry). + registry: Interpreter registry to use (defaults to process-wide registry). Returns: List of parsed data from all matching interpreters. @@ -230,9 +244,9 @@ def parse_advertising_payloads( """ results: list[Any] = [] - # Use global registry if none provided + # Use process-wide registry if none provided if registry is None: - registry = payload_interpreter_registry + registry = get_payload_interpreter_registry() # Create state if not provided if state is None: diff --git a/src/bluetooth_sig/core/query.py b/src/bluetooth_sig/core/query.py index e6fa6ec8..127acfa3 100644 --- a/src/bluetooth_sig/core/query.py +++ b/src/bluetooth_sig/core/query.py @@ -13,7 +13,7 @@ from ..gatt.characteristics.registry import CharacteristicRegistry from ..gatt.services import ServiceName from ..gatt.services.registry import GattServiceRegistry -from ..gatt.uuid_registry import uuid_registry +from ..gatt.uuid_registry import get_uuid_registry from ..types import ( CharacteristicInfo, ServiceInfo, @@ -154,7 +154,7 @@ def get_service_info_by_name(self, name: str | ServiceName) -> ServiceInfo | Non name_str = name.value if isinstance(name, ServiceName) else name try: - uuid_info = uuid_registry.get_service_info(name_str) + uuid_info = get_uuid_registry().get_service_info(name_str) if uuid_info: return ServiceInfo(uuid=uuid_info.uuid, name=uuid_info.name, characteristics=[]) except Exception: # pylint: disable=broad-exception-caught @@ -279,7 +279,7 @@ def get_sig_info_by_name(self, name: str) -> SIGInfo | None: """ try: - char_info = uuid_registry.get_characteristic_info(name) + char_info = get_uuid_registry().get_characteristic_info(name) if char_info: return CharacteristicInfo( uuid=char_info.uuid, diff --git a/src/bluetooth_sig/core/registration.py b/src/bluetooth_sig/core/registration.py index 262b0017..89e0cd03 100644 --- a/src/bluetooth_sig/core/registration.py +++ b/src/bluetooth_sig/core/registration.py @@ -12,7 +12,7 @@ from ..gatt.characteristics.registry import CharacteristicRegistry from ..gatt.services.base import BaseGattService from ..gatt.services.registry import GattServiceRegistry -from ..gatt.uuid_registry import uuid_registry +from ..gatt.uuid_registry import get_uuid_registry from ..types import ( CharacteristicInfo, ServiceInfo, @@ -49,7 +49,7 @@ def register_custom_characteristic_class( CharacteristicRegistry.register_characteristic_class(uuid_or_name, cls, override) if info: - uuid_registry.register_characteristic( + get_uuid_registry().register_characteristic( uuid=info.uuid, name=info.name or cls.__name__, identifier=info.id, @@ -81,7 +81,7 @@ def register_custom_service_class( GattServiceRegistry.register_service_class(uuid_or_name, cls, override) if info: - uuid_registry.register_service( + get_uuid_registry().register_service( uuid=info.uuid, name=info.name or cls.__name__, identifier=info.id, diff --git a/src/bluetooth_sig/gatt/__init__.py b/src/bluetooth_sig/gatt/__init__.py index 85cf0e94..fe272a9b 100644 --- a/src/bluetooth_sig/gatt/__init__.py +++ b/src/bluetooth_sig/gatt/__init__.py @@ -23,7 +23,7 @@ ValueRangeError, ) from .services.base import BaseGattService -from .uuid_registry import UuidRegistry, uuid_registry +from .uuid_registry import UuidRegistry, get_uuid_registry __all__ = [ # Constants @@ -48,5 +48,5 @@ "UnitMetadata", "UuidRegistry", "ValueRangeError", - "uuid_registry", + "get_uuid_registry", ] diff --git a/src/bluetooth_sig/gatt/characteristics/appearance.py b/src/bluetooth_sig/gatt/characteristics/appearance.py index f3b80d57..37467c40 100644 --- a/src/bluetooth_sig/gatt/characteristics/appearance.py +++ b/src/bluetooth_sig/gatt/characteristics/appearance.py @@ -2,7 +2,7 @@ from __future__ import annotations -from ...registry.core.appearance_values import appearance_values_registry +from ...registry.core.appearance_values import get_appearance_values_registry from ...types.appearance import AppearanceData from ...types.gatt_enums import CharacteristicRole from ..context import CharacteristicContext @@ -35,7 +35,7 @@ def _decode_value( AppearanceData with raw value and optional human-readable info. """ raw_value = DataParser.parse_int16(data, 0, signed=False) - appearance_info = appearance_values_registry.get_appearance_info(raw_value) + appearance_info = get_appearance_values_registry().get_appearance_info(raw_value) return AppearanceData( raw_value=raw_value, diff --git a/src/bluetooth_sig/gatt/characteristics/characteristic_meta.py b/src/bluetooth_sig/gatt/characteristics/characteristic_meta.py index 7af09112..dd427457 100644 --- a/src/bluetooth_sig/gatt/characteristics/characteristic_meta.py +++ b/src/bluetooth_sig/gatt/characteristics/characteristic_meta.py @@ -12,13 +12,13 @@ import msgspec -from ...registry.uuids.units import units_registry +from ...registry.uuids.units import get_units_registry from ...types import CharacteristicInfo from ...types.gatt_enums import WIRE_TYPE_MAP from ...types.registry import CharacteristicSpec from ..exceptions import UUIDResolutionError from ..resolver import CharacteristicRegistrySearch, NameNormalizer, NameVariantGenerator -from ..uuid_registry import uuid_registry +from ..uuid_registry import get_uuid_registry # --------------------------------------------------------------------------- # Validation configuration @@ -87,7 +87,7 @@ def resolve_yaml_spec_for_class(char_class: type) -> CharacteristicSpec | None: names_to_try = NameVariantGenerator.generate_characteristic_variants(char_class.__name__, characteristic_name) for try_name in names_to_try: - spec = uuid_registry.resolve_characteristic_spec(try_name) + spec = get_uuid_registry().resolve_characteristic_spec(try_name) if spec: return spec @@ -119,7 +119,7 @@ def _create_info_from_yaml(yaml_spec: CharacteristicSpec, char_class: type) -> C unit_info = None unit_name = getattr(yaml_spec, "unit_symbol", None) or getattr(yaml_spec, "unit", None) if unit_name: - unit_info = units_registry.get_unit_info_by_name(unit_name) + unit_info = get_units_registry().get_unit_info_by_name(unit_name) if unit_info: unit_symbol = str(getattr(unit_info, "symbol", getattr(unit_info, "name", unit_name))) else: diff --git a/src/bluetooth_sig/gatt/characteristics/context_lookup.py b/src/bluetooth_sig/gatt/characteristics/context_lookup.py index 1674e8a9..002764de 100644 --- a/src/bluetooth_sig/gatt/characteristics/context_lookup.py +++ b/src/bluetooth_sig/gatt/characteristics/context_lookup.py @@ -15,7 +15,7 @@ from ...types.gatt_enums import CharacteristicName from ...types.uuid import BluetoothUUID from ..context import CharacteristicContext -from ..uuid_registry import uuid_registry +from ..uuid_registry import get_uuid_registry class ContextLookupMixin: @@ -35,7 +35,7 @@ def _get_characteristic_uuid_by_name( name_str = ( characteristic_name.value if isinstance(characteristic_name, CharacteristicName) else characteristic_name ) - char_info = uuid_registry.get_characteristic_info(name_str) + char_info = get_uuid_registry().get_characteristic_info(name_str) return char_info.uuid if char_info else None def get_context_characteristic( diff --git a/src/bluetooth_sig/gatt/characteristics/preferred_units.py b/src/bluetooth_sig/gatt/characteristics/preferred_units.py index d7c4ab8f..e8b8c3bf 100644 --- a/src/bluetooth_sig/gatt/characteristics/preferred_units.py +++ b/src/bluetooth_sig/gatt/characteristics/preferred_units.py @@ -4,7 +4,7 @@ import msgspec -from ...registry.uuids.units import units_registry +from ...registry.uuids.units import get_units_registry from ...types.gatt_enums import CharacteristicRole from ...types.registry.units import UnitInfo from ...types.uuid import BluetoothUUID @@ -88,7 +88,7 @@ def get_units(self, data: PreferredUnitsData) -> list[UnitInfo]: """ units: list[UnitInfo] = [] for unit_uuid in data.units: - unit_info = units_registry.get_unit_info(unit_uuid) + unit_info = get_units_registry().get_unit_info(unit_uuid) if unit_info: units.append(unit_info) else: @@ -113,6 +113,6 @@ def validate_units(self, data: PreferredUnitsData) -> list[str]: """ errors: list[str] = [] for i, unit_uuid in enumerate(data.units): - if not units_registry.is_unit_uuid(unit_uuid): + if not get_units_registry().is_unit_uuid(unit_uuid): errors.append(f"Unit at index {i} ({unit_uuid.short_form}) is not a recognized Bluetooth SIG unit") return errors diff --git a/src/bluetooth_sig/gatt/characteristics/registry.py b/src/bluetooth_sig/gatt/characteristics/registry.py index b58c13cd..0936f6bd 100644 --- a/src/bluetooth_sig/gatt/characteristics/registry.py +++ b/src/bluetooth_sig/gatt/characteristics/registry.py @@ -15,7 +15,7 @@ class mappings. CharacteristicName enum is now centralized in from ...types.uuid import BluetoothUUID from ..registry_utils import ModuleDiscovery, TypeValidator from ..resolver import NameVariantGenerator -from ..uuid_registry import uuid_registry +from ..uuid_registry import get_uuid_registry from .base import BaseCharacteristic # Export for other modules to import @@ -66,14 +66,14 @@ def build_uuid_to_enum_map(cls) -> dict[str, CharacteristicName]: for enum_member in CharacteristicName: for candidate in cls.generate_candidate_keys(enum_member): - info = uuid_registry.get_characteristic_info(candidate) + info = get_uuid_registry().get_characteristic_info(candidate) if info is None: continue uuid_to_enum[info.uuid.normalized] = enum_member break for info_name, enum_member in cls._SPECIAL_INFO_NAME_TO_ENUM.items(): - info = uuid_registry.get_characteristic_info(info_name) + info = get_uuid_registry().get_characteristic_info(info_name) if info is None: continue uuid_to_enum.setdefault(info.uuid.normalized, enum_member) @@ -119,7 +119,7 @@ def _build_uuid_to_enum_map(self) -> dict[str, CharacteristicName]: for enum_member in CharacteristicName: for candidate in _RegistryKeyBuilder.generate_candidate_keys(enum_member): - info = uuid_registry.get_characteristic_info(candidate) + info = get_uuid_registry().get_characteristic_info(candidate) if info is None: continue uuid_to_enum[info.uuid.normalized] = enum_member @@ -130,7 +130,7 @@ def _build_uuid_to_enum_map(self) -> dict[str, CharacteristicName]: "CO\\textsubscript{2} Concentration": CharacteristicName.CO2_CONCENTRATION, } for info_name, enum_member in special_cases.items(): - info = uuid_registry.get_characteristic_info(info_name) + info = get_uuid_registry().get_characteristic_info(info_name) if info is None: continue uuid_to_enum.setdefault(info.uuid.normalized, enum_member) diff --git a/src/bluetooth_sig/gatt/descriptors/base.py b/src/bluetooth_sig/gatt/descriptors/base.py index 21c620c9..5ba9b6a3 100644 --- a/src/bluetooth_sig/gatt/descriptors/base.py +++ b/src/bluetooth_sig/gatt/descriptors/base.py @@ -10,7 +10,7 @@ from ...types.uuid import BluetoothUUID from ..exceptions import UUIDResolutionError from ..resolver import NameVariantGenerator -from ..uuid_registry import uuid_registry +from ..uuid_registry import get_uuid_registry logger = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def _resolve_info(self) -> DescriptorInfo: # Try each variant for variant in variants: - info = uuid_registry.get_descriptor_info(variant) + info = get_uuid_registry().get_descriptor_info(variant) if info: return info diff --git a/src/bluetooth_sig/gatt/descriptors/characteristic_presentation_format.py b/src/bluetooth_sig/gatt/descriptors/characteristic_presentation_format.py index d27f6496..3a234b4e 100644 --- a/src/bluetooth_sig/gatt/descriptors/characteristic_presentation_format.py +++ b/src/bluetooth_sig/gatt/descriptors/characteristic_presentation_format.py @@ -6,9 +6,9 @@ import msgspec -from ...registry.core.formattypes import format_types_registry -from ...registry.core.namespace_description import namespace_description_registry -from ...registry.uuids.units import units_registry +from ...registry.core.formattypes import get_format_types_registry +from ...registry.core.namespace_description import get_namespace_description_registry +from ...registry.uuids.units import get_units_registry from ...types.uuid import BluetoothUUID from ..characteristics.utils import DataParser from .base import BaseDescriptor @@ -144,18 +144,18 @@ def _parse_descriptor_value(self, data: bytes) -> CharacteristicPresentationForm description_val = DataParser.parse_int16(data, offset=5, endian="little") # Resolve format type name from registry - format_info = format_types_registry.get_format_type_info(format_val) + format_info = get_format_types_registry().get_format_type_info(format_val) format_name = format_info.short_name if format_info else None # Resolve unit name from registry (unit is stored as 16-bit UUID) unit_uuid = BluetoothUUID(unit_val) - unit_info = units_registry.get_unit_info(unit_uuid) + unit_info = get_units_registry().get_unit_info(unit_uuid) unit_name = unit_info.name if unit_info else None # Resolve description name from registry (only for Bluetooth SIG namespace) description_name: str | None = None if namespace_val == FormatNamespace.BLUETOOTH_SIG_ASSIGNED_NUMBERS: - description_name = namespace_description_registry.resolve_description_name(description_val) + description_name = get_namespace_description_registry().resolve_description_name(description_val) return CharacteristicPresentationFormatData( format=FormatType(format_val), diff --git a/src/bluetooth_sig/gatt/resolver.py b/src/bluetooth_sig/gatt/resolver.py index 12e117f5..e287c797 100644 --- a/src/bluetooth_sig/gatt/resolver.py +++ b/src/bluetooth_sig/gatt/resolver.py @@ -11,7 +11,7 @@ from typing import Generic, TypeVar from ..types import CharacteristicInfo, DescriptorInfo, ServiceInfo -from .uuid_registry import uuid_registry +from .uuid_registry import get_uuid_registry # Generic type variables for resolver return types TInfo = TypeVar("TInfo", CharacteristicInfo, ServiceInfo, DescriptorInfo) @@ -335,7 +335,7 @@ def _generate_variants(self, class_name: str, explicit_name: str | None) -> list return NameVariantGenerator.generate_characteristic_variants(class_name, explicit_name) def _lookup_in_registry(self, name: str) -> CharacteristicInfo | None: - return uuid_registry.get_characteristic_info(name) + return get_uuid_registry().get_characteristic_info(name) class ServiceRegistrySearch(RegistrySearchStrategy[ServiceInfo]): # pylint: disable=too-few-public-methods @@ -345,7 +345,7 @@ def _generate_variants(self, class_name: str, explicit_name: str | None) -> list return NameVariantGenerator.generate_service_variants(class_name, explicit_name) def _lookup_in_registry(self, name: str) -> ServiceInfo | None: - return uuid_registry.get_service_info(name) + return get_uuid_registry().get_service_info(name) class DescriptorRegistrySearch(RegistrySearchStrategy[DescriptorInfo]): # pylint: disable=too-few-public-methods @@ -355,4 +355,4 @@ def _generate_variants(self, class_name: str, explicit_name: str | None) -> list return NameVariantGenerator.generate_descriptor_variants(class_name, explicit_name) def _lookup_in_registry(self, name: str) -> DescriptorInfo | None: - return uuid_registry.get_descriptor_info(name) + return get_uuid_registry().get_descriptor_info(name) diff --git a/src/bluetooth_sig/gatt/services/base.py b/src/bluetooth_sig/gatt/services/base.py index 8219ba91..eb48ee05 100644 --- a/src/bluetooth_sig/gatt/services/base.py +++ b/src/bluetooth_sig/gatt/services/base.py @@ -15,7 +15,7 @@ from ..characteristics.unknown import UnknownCharacteristic from ..exceptions import UUIDResolutionError from ..resolver import ServiceRegistrySearch -from ..uuid_registry import uuid_registry +from ..uuid_registry import get_uuid_registry # Type aliases GattCharacteristic = BaseCharacteristic @@ -262,7 +262,7 @@ def get_name(cls) -> str: # For SIG services, resolve from registry uuid = cls.get_class_uuid() - service_info = uuid_registry.get_service_info(uuid) + service_info = get_uuid_registry().get_service_info(uuid) if service_info: return service_info.name @@ -355,7 +355,7 @@ def get_expected_characteristic_uuids(self) -> set[BluetoothUUID]: lookup_name = char_name.value except AttributeError: lookup_name = str(char_name) - char_info = uuid_registry.get_characteristic_info(lookup_name) + char_info = get_uuid_registry().get_characteristic_info(lookup_name) if char_info: expected_uuids.add(char_info.uuid) return expected_uuids @@ -368,7 +368,7 @@ def get_required_characteristic_uuids(self) -> set[BluetoothUUID]: lookup_name = char_name.value except AttributeError: lookup_name = str(char_name) - char_info = uuid_registry.get_characteristic_info(lookup_name) + char_info = get_uuid_registry().get_characteristic_info(lookup_name) if char_info: required_uuids.add(char_info.uuid) return required_uuids @@ -452,7 +452,7 @@ def validate_bluetooth_sig_compliance(cls) -> list[str]: # Check if all expected characteristics are valid expected = cls.get_expected_characteristics() for char_name, _char_spec in expected.items(): - char_info = uuid_registry.get_characteristic_info(char_name.value) + char_info = get_uuid_registry().get_characteristic_info(char_name.value) if not char_info: issues.append(f"Characteristic '{char_name.value}' not found in UUID registry") @@ -468,7 +468,7 @@ def _validate_characteristic_group( """Validate a group of characteristics (required/optional/conditional).""" for char_name, char_spec in char_dict.items(): lookup_name = char_name.value if hasattr(char_name, "value") else str(char_name) - char_info = uuid_registry.get_characteristic_info(lookup_name) + char_info = get_uuid_registry().get_characteristic_info(lookup_name) if not char_info: if is_required: @@ -493,7 +493,7 @@ def _validate_conditional_characteristics( """Validate conditional characteristics.""" for char_name, conditional_spec in conditional_chars.items(): lookup_name = char_name.value if hasattr(char_name, "value") else str(char_name) - char_info = uuid_registry.get_characteristic_info(lookup_name) + char_info = get_uuid_registry().get_characteristic_info(lookup_name) if not char_info: result.warnings.append(f"Unknown conditional characteristic: {lookup_name}") @@ -560,7 +560,7 @@ def get_missing_characteristics( conditional_chars = self.get_conditional_characteristics() for char_name, _char_spec in expected_chars.items(): - char_info = uuid_registry.get_characteristic_info(char_name.value) + char_info = get_uuid_registry().get_characteristic_info(char_name.value) if not char_info: continue @@ -662,7 +662,7 @@ def get_characteristic_status(self, characteristic_name: CharacteristicName) -> if char_enum not in expected_chars: return None - char_info = uuid_registry.get_characteristic_info(char_enum.value) + char_info = get_uuid_registry().get_characteristic_info(char_enum.value) if not char_info: return None diff --git a/src/bluetooth_sig/gatt/services/registry.py b/src/bluetooth_sig/gatt/services/registry.py index 53af3deb..6c2fe748 100644 --- a/src/bluetooth_sig/gatt/services/registry.py +++ b/src/bluetooth_sig/gatt/services/registry.py @@ -16,7 +16,7 @@ from ...types.uuid import BluetoothUUID from ..exceptions import UUIDResolutionError from ..registry_utils import ModuleDiscovery, TypeValidator -from ..uuid_registry import uuid_registry +from ..uuid_registry import get_uuid_registry from .base import BaseGattService __all__ = [ @@ -81,7 +81,7 @@ def _build_enum_map(self) -> dict[ServiceName, type[BaseGattService]]: # Find the corresponding enum member by UUID enum_member = None for candidate_enum in ServiceName: - candidate_info = uuid_registry.get_service_info(candidate_enum.value) + candidate_info = get_uuid_registry().get_service_info(candidate_enum.value) if candidate_info and candidate_info.uuid == uuid_obj: enum_member = candidate_enum break diff --git a/src/bluetooth_sig/gatt/uuid_registry.py b/src/bluetooth_sig/gatt/uuid_registry.py index 99b782ae..487bf1c4 100644 --- a/src/bluetooth_sig/gatt/uuid_registry.py +++ b/src/bluetooth_sig/gatt/uuid_registry.py @@ -2,7 +2,6 @@ from __future__ import annotations -import contextlib import logging import threading @@ -19,7 +18,7 @@ __all__ = [ "UuidRegistry", - "uuid_registry", + "get_uuid_registry", ] logger = logging.getLogger(__name__) @@ -34,9 +33,23 @@ class UuidRegistry: # pylint: disable=too-many-instance-attributes avoid noisy global configuration changes. """ + _instance: UuidRegistry | None = None + _class_lock = threading.RLock() + + @classmethod + def get_instance(cls) -> UuidRegistry: + """Return the process-wide UuidRegistry singleton instance.""" + if cls._instance is None: + with cls._class_lock: + if cls._instance is None: + cls._instance = cls() + return cls._instance + def __init__(self) -> None: """Initialize the UUID registry.""" self._lock = threading.RLock() + self._loaded = False + self._load_error: Exception | None = None # Canonical storage: normalized_uuid -> domain types (single source of truth) self._services: dict[str, ServiceInfo] = {} @@ -58,9 +71,23 @@ def __init__(self) -> None: self._gss_registry: GssRegistry | None = None - with contextlib.suppress(FileNotFoundError, Exception): - # If YAML loading fails, continue with empty registry - self._load_uuids() + def _ensure_loaded(self) -> None: + """Ensure the registry has loaded its YAML data exactly once.""" + with self._lock: + if self._loaded: + return + if self._load_error is not None: + raise RuntimeError("UUID registry failed to load SIG data") from self._load_error + try: + self._load_uuids() + except Exception as exc: # pylint: disable=broad-exception-caught + self._load_error = exc + raise RuntimeError("UUID registry failed to load SIG data") from exc + self._loaded = True + + def ensure_loaded(self) -> None: + """Public API to eagerly load UUID registry data.""" + self._ensure_loaded() def _store_service(self, info: ServiceInfo) -> None: """Store service info with canonical storage + aliases.""" @@ -312,6 +339,7 @@ def register_characteristic( # pylint: disable=too-many-arguments,too-many-posi python_type: Optional Python type for the value override: If True, allow overriding existing entries """ + self._ensure_loaded() with self._lock: canonical_key = uuid.normalized @@ -360,6 +388,7 @@ def register_service( identifier: Optional identifier (auto-generated if not provided) override: If True, allow overriding existing entries """ + self._ensure_loaded() with self._lock: canonical_key = uuid.normalized @@ -392,6 +421,7 @@ def register_service( def get_service_info(self, key: str | BluetoothUUID) -> ServiceInfo | None: """Get information about a service by UUID, name, or ID.""" + self._ensure_loaded() with self._lock: # Convert BluetoothUUID to canonical key if isinstance(key, BluetoothUUID): @@ -420,6 +450,7 @@ def get_service_info(self, key: str | BluetoothUUID) -> ServiceInfo | None: def get_characteristic_info(self, identifier: str | BluetoothUUID) -> CharacteristicInfo | None: """Get information about a characteristic by UUID, name, or ID.""" + self._ensure_loaded() with self._lock: # Convert BluetoothUUID to canonical key if isinstance(identifier, BluetoothUUID): @@ -448,6 +479,7 @@ def get_characteristic_info(self, identifier: str | BluetoothUUID) -> Characteri def get_descriptor_info(self, identifier: str | BluetoothUUID) -> DescriptorInfo | None: """Get information about a descriptor by UUID, name, or ID.""" + self._ensure_loaded() with self._lock: # Convert BluetoothUUID to canonical key if isinstance(identifier, BluetoothUUID): @@ -487,12 +519,13 @@ def get_gss_spec(self, identifier: str | BluetoothUUID) -> GssCharacteristicSpec GssCharacteristicSpec with full field structure, or None if not found Example:: - gss = uuid_registry.get_gss_spec("Location and Speed") + gss = get_uuid_registry().get_gss_spec("Location and Speed") if gss: for field in gss.structure: print(f"{field.python_name}: unit={field.unit_id}, resolution={field.resolution}") """ + self._ensure_loaded() if self._gss_registry is None: return None @@ -509,7 +542,7 @@ def get_gss_spec(self, identifier: str | BluetoothUUID) -> GssCharacteristicSpec spec = self._gss_registry.get_spec(char_info.id) if spec: return spec - elif isinstance(identifier, BluetoothUUID): + else: # Look up by UUID char_info = self.get_characteristic_info(identifier) if char_info: @@ -535,11 +568,12 @@ def resolve_characteristic_spec(self, characteristic_name: str) -> Characteristi CharacteristicSpec with full metadata, or None if not found Example:: - spec = uuid_registry.resolve_characteristic_spec("Temperature") + spec = get_uuid_registry().resolve_characteristic_spec("Temperature") if spec: print(f"UUID: {spec.uuid}, Unit: {spec.unit_symbol}, Type: {spec.data_type}") """ + self._ensure_loaded() with self._lock: # 1. Get UUID from characteristic registry char_info = self.get_characteristic_info(characteristic_name) @@ -678,5 +712,6 @@ def clear_custom_registrations(self) -> None: self._runtime_uuids.clear() -# Global instance -uuid_registry = UuidRegistry() +def get_uuid_registry() -> UuidRegistry: + """Return the process-wide UUID registry singleton instance.""" + return UuidRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/base.py b/src/bluetooth_sig/registry/base.py index 64585253..2cdf0cab 100644 --- a/src/bluetooth_sig/registry/base.py +++ b/src/bluetooth_sig/registry/base.py @@ -7,7 +7,7 @@ from collections.abc import Callable from enum import Enum from pathlib import Path -from typing import Any, Generic, TypeVar +from typing import Any, ClassVar, Generic, TypeVar, cast from bluetooth_sig.registry.utils import ( find_bluetooth_sig_path, @@ -20,6 +20,9 @@ T = TypeVar("T") E = TypeVar("E", bound=Enum) # For enum-keyed registries C = TypeVar("C") # For class types +BG = TypeVar("BG", bound="BaseGenericRegistry[Any]") +BU = TypeVar("BU", bound="BaseUUIDRegistry[Any]") +BC = TypeVar("BC", bound="BaseUUIDClassRegistry[Any, Any]") class RegistryMixin: @@ -80,8 +83,8 @@ class BaseGenericRegistry(RegistryMixin, ABC, Generic[T]): For registries that are not UUID-based. """ - _instance: BaseGenericRegistry[T] | None = None - _lock = threading.RLock() + _instance: ClassVar[BaseGenericRegistry[Any] | None] = None + _instance_lock: ClassVar[threading.RLock] = threading.RLock() def __init__(self) -> None: """Initialize the registry.""" @@ -89,13 +92,13 @@ def __init__(self) -> None: self._loaded: bool = False @classmethod - def get_instance(cls) -> BaseGenericRegistry[T]: + def get_instance(cls: type[BG]) -> BG: """Get the singleton instance of the registry.""" if cls._instance is None: - with cls._lock: + with cls._instance_lock: if cls._instance is None: cls._instance = cls() - return cls._instance + return cast("BG", cls._instance) U = TypeVar("U", bound=BaseUuidInfo) @@ -115,8 +118,8 @@ class BaseUUIDRegistry(RegistryMixin, ABC, Generic[U]): 6. Call _ensure_loaded() before accessing data (provided by base class) """ - _instance: BaseUUIDRegistry[U] | None = None - _lock = threading.RLock() + _instance: ClassVar[BaseUUIDRegistry[Any] | None] = None + _instance_lock: ClassVar[threading.RLock] = threading.RLock() def __init__(self) -> None: """Initialize the registry.""" @@ -288,13 +291,13 @@ def list_aliases(self, uuid: BluetoothUUID) -> list[str]: return [alias for alias, key in self._alias_index.items() if key == uuid.normalized] @classmethod - def get_instance(cls) -> BaseUUIDRegistry[U]: + def get_instance(cls: type[BU]) -> BU: """Get the singleton instance of the registry.""" if cls._instance is None: - with cls._lock: + with cls._instance_lock: if cls._instance is None: cls._instance = cls() - return cls._instance + return cast("BU", cls._instance) class BaseUUIDClassRegistry(RegistryMixin, ABC, Generic[E, C]): @@ -317,8 +320,8 @@ class BaseUUIDClassRegistry(RegistryMixin, ABC, Generic[E, C]): 5. Optionally override _allows_sig_override() for custom override rules """ - _instance: BaseUUIDClassRegistry[E, C] | None = None - _lock = threading.RLock() + _instance: ClassVar[BaseUUIDClassRegistry[Any, Any] | None] = None + _instance_lock: ClassVar[threading.RLock] = threading.RLock() def __init__(self) -> None: """Initialize the class registry.""" @@ -503,10 +506,10 @@ def clear_enum_map_cache(self) -> None: self._sig_class_cache = None @classmethod - def get_instance(cls) -> BaseUUIDClassRegistry[E, C]: + def get_instance(cls: type[BC]) -> BC: """Get the singleton instance of the registry.""" if cls._instance is None: - with cls._lock: + with cls._instance_lock: if cls._instance is None: cls._instance = cls() - return cls._instance + return cast("BC", cls._instance) diff --git a/src/bluetooth_sig/registry/company_identifiers/__init__.py b/src/bluetooth_sig/registry/company_identifiers/__init__.py index 35a72bd7..28e21f8a 100644 --- a/src/bluetooth_sig/registry/company_identifiers/__init__.py +++ b/src/bluetooth_sig/registry/company_identifiers/__init__.py @@ -8,10 +8,10 @@ from .company_identifiers_registry import ( CompanyIdentifiersRegistry, - company_identifiers_registry, + get_company_identifiers_registry, ) __all__ = [ "CompanyIdentifiersRegistry", - "company_identifiers_registry", + "get_company_identifiers_registry", ] diff --git a/src/bluetooth_sig/registry/company_identifiers/company_identifiers_registry.py b/src/bluetooth_sig/registry/company_identifiers/company_identifiers_registry.py index e8180a45..7f40e903 100644 --- a/src/bluetooth_sig/registry/company_identifiers/company_identifiers_registry.py +++ b/src/bluetooth_sig/registry/company_identifiers/company_identifiers_registry.py @@ -111,4 +111,6 @@ def get_company_name(self, company_id: int) -> str | None: # Singleton instance for global use -company_identifiers_registry = CompanyIdentifiersRegistry() +def get_company_identifiers_registry() -> CompanyIdentifiersRegistry: + """Return the process-wide company_identifiers_registry singleton instance.""" + return CompanyIdentifiersRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/core/__init__.py b/src/bluetooth_sig/registry/core/__init__.py index f528251b..cb26c8f6 100644 --- a/src/bluetooth_sig/registry/core/__init__.py +++ b/src/bluetooth_sig/registry/core/__init__.py @@ -10,16 +10,16 @@ from __future__ import annotations -from .ad_types import ad_types_registry -from .coding_format import coding_format_registry -from .formattypes import format_types_registry -from .namespace_description import namespace_description_registry -from .uri_schemes import uri_schemes_registry +from .ad_types import get_ad_types_registry +from .coding_format import get_coding_format_registry +from .formattypes import get_format_types_registry +from .namespace_description import get_namespace_description_registry +from .uri_schemes import get_uri_schemes_registry __all__ = [ - "ad_types_registry", - "coding_format_registry", - "format_types_registry", - "namespace_description_registry", - "uri_schemes_registry", + "get_ad_types_registry", + "get_coding_format_registry", + "get_format_types_registry", + "get_namespace_description_registry", + "get_uri_schemes_registry", ] diff --git a/src/bluetooth_sig/registry/core/ad_types.py b/src/bluetooth_sig/registry/core/ad_types.py index 47ad75fa..66c81e46 100644 --- a/src/bluetooth_sig/registry/core/ad_types.py +++ b/src/bluetooth_sig/registry/core/ad_types.py @@ -134,4 +134,6 @@ def get_all_ad_types(self) -> dict[int, AdTypeInfo]: # Global singleton instance -ad_types_registry = ADTypesRegistry() +def get_ad_types_registry() -> ADTypesRegistry: + """Return the process-wide ad_types_registry singleton instance.""" + return ADTypesRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/core/appearance_values.py b/src/bluetooth_sig/registry/core/appearance_values.py index 61b196af..05bdff5d 100644 --- a/src/bluetooth_sig/registry/core/appearance_values.py +++ b/src/bluetooth_sig/registry/core/appearance_values.py @@ -8,7 +8,6 @@ from __future__ import annotations from pathlib import Path -from typing import cast import msgspec @@ -188,4 +187,6 @@ def find_by_category_subcategory(self, category: str, subcategory: str | None = # Singleton instance for global use -appearance_values_registry = cast("AppearanceValuesRegistry", AppearanceValuesRegistry.get_instance()) +def get_appearance_values_registry() -> AppearanceValuesRegistry: + """Return the process-wide appearance_values_registry singleton instance.""" + return AppearanceValuesRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/core/class_of_device.py b/src/bluetooth_sig/registry/core/class_of_device.py index c7508e62..6aa8ce7e 100644 --- a/src/bluetooth_sig/registry/core/class_of_device.py +++ b/src/bluetooth_sig/registry/core/class_of_device.py @@ -252,4 +252,6 @@ def decode_class_of_device(self, cod: int) -> ClassOfDeviceInfo: # Module-level singleton instance -class_of_device_registry = ClassOfDeviceRegistry() +def get_class_of_device_registry() -> ClassOfDeviceRegistry: + """Return the process-wide class_of_device_registry singleton instance.""" + return ClassOfDeviceRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/core/coding_format.py b/src/bluetooth_sig/registry/core/coding_format.py index b44ded73..7fefcdc8 100644 --- a/src/bluetooth_sig/registry/core/coding_format.py +++ b/src/bluetooth_sig/registry/core/coding_format.py @@ -25,8 +25,8 @@ class CodingFormatRegistry(BaseGenericRegistry[CodingFormatInfo]): and Classic Audio profiles. Examples: - >>> from bluetooth_sig.registry.core.coding_format import coding_format_registry - >>> info = coding_format_registry.get_coding_format_info(0x06) + >>> from bluetooth_sig.registry.core.coding_format import get_coding_format_registry + >>> info = get_coding_format_registry().get_coding_format_info(0x06) >>> info.name 'LC3' """ @@ -142,4 +142,6 @@ def get_all_coding_formats(self) -> dict[int, CodingFormatInfo]: # Global singleton instance -coding_format_registry = CodingFormatRegistry() +def get_coding_format_registry() -> CodingFormatRegistry: + """Return the process-wide get_coding_format_registry singleton instance.""" + return CodingFormatRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/core/formattypes.py b/src/bluetooth_sig/registry/core/formattypes.py index 259e7aa9..714cea67 100644 --- a/src/bluetooth_sig/registry/core/formattypes.py +++ b/src/bluetooth_sig/registry/core/formattypes.py @@ -138,4 +138,6 @@ def get_all_format_types(self) -> dict[int, FormatTypeInfo]: # Global singleton instance -format_types_registry = FormatTypesRegistry() +def get_format_types_registry() -> FormatTypesRegistry: + """Return the process-wide format_types_registry singleton instance.""" + return FormatTypesRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/core/namespace_description.py b/src/bluetooth_sig/registry/core/namespace_description.py index 249ef910..8589b180 100644 --- a/src/bluetooth_sig/registry/core/namespace_description.py +++ b/src/bluetooth_sig/registry/core/namespace_description.py @@ -35,8 +35,8 @@ class NamespaceDescriptionRegistry(BaseGenericRegistry[NamespaceDescriptionInfo] - 0x0102 → "top" Examples: - >>> from bluetooth_sig.registry.core.namespace_description import namespace_description_registry - >>> info = namespace_description_registry.get_description_info(0x010D) + >>> from bluetooth_sig.registry.core.namespace_description import get_namespace_description_registry + >>> info = get_namespace_description_registry().get_description_info(0x010D) >>> info.name 'left' """ @@ -167,4 +167,6 @@ def resolve_description_name(self, value: int) -> str | None: # Global singleton instance -namespace_description_registry = NamespaceDescriptionRegistry() +def get_namespace_description_registry() -> NamespaceDescriptionRegistry: + """Return the process-wide get_namespace_description_registry singleton instance.""" + return NamespaceDescriptionRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/core/uri_schemes.py b/src/bluetooth_sig/registry/core/uri_schemes.py index c77fb3db..fa20d1a9 100644 --- a/src/bluetooth_sig/registry/core/uri_schemes.py +++ b/src/bluetooth_sig/registry/core/uri_schemes.py @@ -28,8 +28,8 @@ class UriSchemesRegistry(BaseGenericRegistry[UriSchemeInfo]): advertising data, reducing packet size for common schemes like http://. Examples: - >>> from bluetooth_sig.registry.core.uri_schemes import uri_schemes_registry - >>> info = uri_schemes_registry.get_uri_scheme_info(0x16) + >>> from bluetooth_sig.registry.core.uri_schemes import get_uri_schemes_registry + >>> info = get_uri_schemes_registry().get_uri_scheme_info(0x16) >>> info.name 'http:' """ @@ -160,4 +160,6 @@ def decode_uri_prefix(self, value: int) -> str: # Global singleton instance -uri_schemes_registry = UriSchemesRegistry() +def get_uri_schemes_registry() -> UriSchemesRegistry: + """Return the process-wide get_uri_schemes_registry singleton instance.""" + return UriSchemesRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/gss.py b/src/bluetooth_sig/registry/gss.py index a4dbf706..a285f03a 100644 --- a/src/bluetooth_sig/registry/gss.py +++ b/src/bluetooth_sig/registry/gss.py @@ -38,23 +38,12 @@ class GssRegistry(BaseGenericRegistry[GssCharacteristicSpec]): """ - _instance: GssRegistry | None = None - def __init__(self) -> None: """Initialize the GSS registry.""" super().__init__() self._specs: dict[str, GssCharacteristicSpec] = {} self._units_registry: UnitsRegistry | None = None - @classmethod - def get_instance(cls) -> GssRegistry: - """Get the singleton instance of the GSS registry.""" - if cls._instance is None: - with cls._lock: - if cls._instance is None: - cls._instance = cls() - return cls._instance - def _get_units_registry(self) -> UnitsRegistry: """Get or lazily initialize the units registry. @@ -62,8 +51,7 @@ def _get_units_registry(self) -> UnitsRegistry: The UnitsRegistry singleton instance """ if self._units_registry is None: - # get_instance() returns BaseUUIDRegistry; narrow to concrete subclass - self._units_registry = cast("UnitsRegistry", UnitsRegistry.get_instance()) + self._units_registry = UnitsRegistry.get_instance() return self._units_registry def _load(self) -> None: @@ -276,4 +264,6 @@ def _convert_bluetooth_unit_to_readable(self, unit_spec: str) -> str: # Singleton instance for convenient access -gss_registry = GssRegistry.get_instance() +def get_gss_registry() -> GssRegistry: + """Return the process-wide gss_registry singleton instance.""" + return GssRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/profiles/__init__.py b/src/bluetooth_sig/registry/profiles/__init__.py index fbfd9bfe..1fb9250b 100644 --- a/src/bluetooth_sig/registry/profiles/__init__.py +++ b/src/bluetooth_sig/registry/profiles/__init__.py @@ -9,16 +9,16 @@ from .permitted_characteristics import ( PermittedCharacteristicsRegistry, - permitted_characteristics_registry, + get_permitted_characteristics_registry, ) from .profile_lookup import ( ProfileLookupRegistry, - profile_lookup_registry, + get_profile_lookup_registry, ) __all__ = [ "PermittedCharacteristicsRegistry", "ProfileLookupRegistry", - "permitted_characteristics_registry", - "profile_lookup_registry", + "get_permitted_characteristics_registry", + "get_profile_lookup_registry", ] diff --git a/src/bluetooth_sig/registry/profiles/permitted_characteristics.py b/src/bluetooth_sig/registry/profiles/permitted_characteristics.py index d86f743e..ee69c756 100644 --- a/src/bluetooth_sig/registry/profiles/permitted_characteristics.py +++ b/src/bluetooth_sig/registry/profiles/permitted_characteristics.py @@ -131,4 +131,6 @@ def get_all_profiles(self) -> list[str]: # Singleton instance for global use -permitted_characteristics_registry = PermittedCharacteristicsRegistry() +def get_permitted_characteristics_registry() -> PermittedCharacteristicsRegistry: + """Return the process-wide permitted_characteristics_registry singleton instance.""" + return PermittedCharacteristicsRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/profiles/profile_lookup.py b/src/bluetooth_sig/registry/profiles/profile_lookup.py index 20565940..b2ca6094 100644 --- a/src/bluetooth_sig/registry/profiles/profile_lookup.py +++ b/src/bluetooth_sig/registry/profiles/profile_lookup.py @@ -219,4 +219,6 @@ def resolve_name(self, table_key: str, value: int) -> str | None: # Singleton instance for global use -profile_lookup_registry = ProfileLookupRegistry() +def get_profile_lookup_registry() -> ProfileLookupRegistry: + """Return the process-wide profile_lookup_registry singleton instance.""" + return ProfileLookupRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/service_discovery/__init__.py b/src/bluetooth_sig/registry/service_discovery/__init__.py index 672d7c10..18d92b0d 100644 --- a/src/bluetooth_sig/registry/service_discovery/__init__.py +++ b/src/bluetooth_sig/registry/service_discovery/__init__.py @@ -9,10 +9,10 @@ from .attribute_ids import ( ServiceDiscoveryAttributeRegistry, - service_discovery_attribute_registry, + get_service_discovery_attribute_registry, ) __all__ = [ "ServiceDiscoveryAttributeRegistry", - "service_discovery_attribute_registry", + "get_service_discovery_attribute_registry", ] diff --git a/src/bluetooth_sig/registry/service_discovery/attribute_ids.py b/src/bluetooth_sig/registry/service_discovery/attribute_ids.py index 4d87bddd..77bdf1dc 100644 --- a/src/bluetooth_sig/registry/service_discovery/attribute_ids.py +++ b/src/bluetooth_sig/registry/service_discovery/attribute_ids.py @@ -184,4 +184,6 @@ def resolve_attribute_name(self, category: str, value: int) -> str | None: # Singleton instance for global use -service_discovery_attribute_registry = ServiceDiscoveryAttributeRegistry() +def get_service_discovery_attribute_registry() -> ServiceDiscoveryAttributeRegistry: + """Return the process-wide service_discovery_attribute_registry singleton instance.""" + return ServiceDiscoveryAttributeRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/uuids/__init__.py b/src/bluetooth_sig/registry/uuids/__init__.py index 128838e3..5b640d48 100644 --- a/src/bluetooth_sig/registry/uuids/__init__.py +++ b/src/bluetooth_sig/registry/uuids/__init__.py @@ -2,24 +2,24 @@ from __future__ import annotations -from .browse_groups import browse_groups_registry -from .declarations import declarations_registry -from .members import members_registry -from .mesh_profiles import mesh_profiles_registry -from .object_types import object_types_registry -from .protocol_identifiers import protocol_identifiers_registry -from .sdo_uuids import sdo_uuids_registry -from .service_classes import service_classes_registry -from .units import units_registry +from .browse_groups import get_browse_groups_registry +from .declarations import get_declarations_registry +from .members import get_members_registry +from .mesh_profiles import get_mesh_profiles_registry +from .object_types import get_object_types_registry +from .protocol_identifiers import get_protocol_identifiers_registry +from .sdo_uuids import get_sdo_uuids_registry +from .service_classes import get_service_classes_registry +from .units import get_units_registry __all__ = [ - "browse_groups_registry", - "declarations_registry", - "members_registry", - "mesh_profiles_registry", - "object_types_registry", - "protocol_identifiers_registry", - "sdo_uuids_registry", - "service_classes_registry", - "units_registry", + "get_browse_groups_registry", + "get_declarations_registry", + "get_members_registry", + "get_mesh_profiles_registry", + "get_object_types_registry", + "get_protocol_identifiers_registry", + "get_sdo_uuids_registry", + "get_service_classes_registry", + "get_units_registry", ] diff --git a/src/bluetooth_sig/registry/uuids/browse_groups.py b/src/bluetooth_sig/registry/uuids/browse_groups.py index b70da49e..30906dec 100644 --- a/src/bluetooth_sig/registry/uuids/browse_groups.py +++ b/src/bluetooth_sig/registry/uuids/browse_groups.py @@ -94,4 +94,6 @@ def get_all_browse_groups(self) -> list[BrowseGroupInfo]: # Global instance for convenience -browse_groups_registry = BrowseGroupsRegistry.get_instance() +def get_browse_groups_registry() -> BrowseGroupsRegistry: + """Return the process-wide browse_groups_registry singleton instance.""" + return BrowseGroupsRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/uuids/declarations.py b/src/bluetooth_sig/registry/uuids/declarations.py index bebae709..4782c100 100644 --- a/src/bluetooth_sig/registry/uuids/declarations.py +++ b/src/bluetooth_sig/registry/uuids/declarations.py @@ -93,4 +93,6 @@ def get_all_declarations(self) -> list[DeclarationInfo]: # Global instance for convenience -declarations_registry = DeclarationsRegistry.get_instance() +def get_declarations_registry() -> DeclarationsRegistry: + """Return the process-wide declarations_registry singleton instance.""" + return DeclarationsRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/uuids/members.py b/src/bluetooth_sig/registry/uuids/members.py index 13b0fb7f..c0a68429 100644 --- a/src/bluetooth_sig/registry/uuids/members.py +++ b/src/bluetooth_sig/registry/uuids/members.py @@ -77,4 +77,6 @@ def get_member_info_by_name(self, name: str) -> MemberInfo | None: # Global instance -members_registry = MembersRegistry.get_instance() +def get_members_registry() -> MembersRegistry: + """Return the process-wide members_registry singleton instance.""" + return MembersRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/uuids/mesh_profiles.py b/src/bluetooth_sig/registry/uuids/mesh_profiles.py index b26cc4be..d979d542 100644 --- a/src/bluetooth_sig/registry/uuids/mesh_profiles.py +++ b/src/bluetooth_sig/registry/uuids/mesh_profiles.py @@ -76,4 +76,6 @@ def get_all_mesh_profiles(self) -> list[MeshProfileInfo]: # Global instance for convenience -mesh_profiles_registry = MeshProfilesRegistry.get_instance() +def get_mesh_profiles_registry() -> MeshProfilesRegistry: + """Return the process-wide mesh_profiles_registry singleton instance.""" + return MeshProfilesRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/uuids/object_types.py b/src/bluetooth_sig/registry/uuids/object_types.py index b7ad266f..fa258ee2 100644 --- a/src/bluetooth_sig/registry/uuids/object_types.py +++ b/src/bluetooth_sig/registry/uuids/object_types.py @@ -97,4 +97,6 @@ def get_all_object_types(self) -> list[ObjectTypeInfo]: # Global instance -object_types_registry = ObjectTypesRegistry.get_instance() +def get_object_types_registry() -> ObjectTypesRegistry: + """Return the process-wide object_types_registry singleton instance.""" + return ObjectTypesRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/uuids/protocol_identifiers.py b/src/bluetooth_sig/registry/uuids/protocol_identifiers.py index 124aedbc..439df983 100644 --- a/src/bluetooth_sig/registry/uuids/protocol_identifiers.py +++ b/src/bluetooth_sig/registry/uuids/protocol_identifiers.py @@ -6,7 +6,7 @@ from bluetooth_sig.types.registry.protocol_identifiers import ProtocolInfo from bluetooth_sig.types.uuid import BluetoothUUID -__all__ = ["ProtocolIdentifiersRegistry", "ProtocolInfo", "protocol_identifiers_registry"] +__all__ = ["ProtocolIdentifiersRegistry", "ProtocolInfo", "get_protocol_identifiers_registry"] class ProtocolIdentifiersRegistry(BaseUUIDRegistry[ProtocolInfo]): @@ -89,4 +89,6 @@ def get_all_protocols(self) -> list[ProtocolInfo]: # Global instance for convenience -protocol_identifiers_registry = ProtocolIdentifiersRegistry.get_instance() +def get_protocol_identifiers_registry() -> ProtocolIdentifiersRegistry: + """Return the process-wide protocol_identifiers_registry singleton instance.""" + return ProtocolIdentifiersRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/uuids/sdo_uuids.py b/src/bluetooth_sig/registry/uuids/sdo_uuids.py index 49f41bd6..8be73e66 100644 --- a/src/bluetooth_sig/registry/uuids/sdo_uuids.py +++ b/src/bluetooth_sig/registry/uuids/sdo_uuids.py @@ -102,4 +102,6 @@ def get_all_sdo_uuids(self) -> list[SdoInfo]: # Global instance for convenience -sdo_uuids_registry = SdoUuidsRegistry.get_instance() +def get_sdo_uuids_registry() -> SdoUuidsRegistry: + """Return the process-wide sdo_uuids_registry singleton instance.""" + return SdoUuidsRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/uuids/service_classes.py b/src/bluetooth_sig/registry/uuids/service_classes.py index 17c044a1..7447385c 100644 --- a/src/bluetooth_sig/registry/uuids/service_classes.py +++ b/src/bluetooth_sig/registry/uuids/service_classes.py @@ -86,4 +86,6 @@ def get_all_service_classes(self) -> list[ServiceClassInfo]: # Global instance for convenience -service_classes_registry = ServiceClassesRegistry.get_instance() +def get_service_classes_registry() -> ServiceClassesRegistry: + """Return the process-wide service_classes_registry singleton instance.""" + return ServiceClassesRegistry.get_instance() diff --git a/src/bluetooth_sig/registry/uuids/units.py b/src/bluetooth_sig/registry/uuids/units.py index e7edde47..7281dd07 100644 --- a/src/bluetooth_sig/registry/uuids/units.py +++ b/src/bluetooth_sig/registry/uuids/units.py @@ -199,7 +199,10 @@ def get_all_units(self) -> list[UnitInfo]: # Global instance -units_registry = UnitsRegistry() +def get_units_registry() -> UnitsRegistry: + """Return the process-wide units_registry singleton instance.""" + return UnitsRegistry.get_instance() + _UNIT_ID_PREFIX = "org.bluetooth.unit." @@ -221,5 +224,5 @@ def resolve_unit_symbol(unit_id: str) -> str: """ full_id = unit_id if unit_id.startswith(_UNIT_ID_PREFIX) else f"{_UNIT_ID_PREFIX}{unit_id}" - info = units_registry.get_unit_info_by_id(full_id) + info = get_units_registry().get_unit_info_by_id(full_id) return info.symbol if info and info.symbol else "" diff --git a/src/bluetooth_sig/types/appearance.py b/src/bluetooth_sig/types/appearance.py index ff4c9255..6edb5026 100644 --- a/src/bluetooth_sig/types/appearance.py +++ b/src/bluetooth_sig/types/appearance.py @@ -4,7 +4,7 @@ import msgspec -from bluetooth_sig.registry.core.appearance_values import appearance_values_registry +from bluetooth_sig.registry.core.appearance_values import get_appearance_values_registry from bluetooth_sig.types.registry.appearance_info import AppearanceInfo @@ -43,7 +43,7 @@ def from_category(cls, category: str, subcategory: str | None = None) -> Appeara 833 """ # Use public method to find appearance info - info = appearance_values_registry.find_by_category_subcategory(category, subcategory) + info = get_appearance_values_registry().find_by_category_subcategory(category, subcategory) if info is None: # If not found, raise error diff --git a/src/bluetooth_sig/types/company.py b/src/bluetooth_sig/types/company.py index c4680cad..a039f38d 100644 --- a/src/bluetooth_sig/types/company.py +++ b/src/bluetooth_sig/types/company.py @@ -9,7 +9,7 @@ import msgspec from bluetooth_sig.gatt.constants import SIZE_UINT16 -from bluetooth_sig.registry.company_identifiers import company_identifiers_registry +from bluetooth_sig.registry.company_identifiers import get_company_identifiers_registry class CompanyIdentifier(msgspec.Struct, kw_only=True, frozen=True): @@ -61,7 +61,7 @@ def from_id(cls, company_id: int) -> CompanyIdentifier: 'Apple, Inc.' """ - name = company_identifiers_registry.get_company_name(company_id) + name = get_company_identifiers_registry().get_company_name(company_id) if not name: name = f"Unknown (0x{company_id:04X})" return cls(id=company_id, name=name) diff --git a/src/bluetooth_sig/types/uri.py b/src/bluetooth_sig/types/uri.py index 33e31d0f..b7d0adc4 100644 --- a/src/bluetooth_sig/types/uri.py +++ b/src/bluetooth_sig/types/uri.py @@ -4,7 +4,7 @@ import msgspec -from bluetooth_sig.registry.core.uri_schemes import uri_schemes_registry +from bluetooth_sig.registry.core.uri_schemes import get_uri_schemes_registry from bluetooth_sig.types.registry.uri_schemes import UriSchemeInfo @@ -81,7 +81,7 @@ def from_raw_data(cls, data: bytes) -> URIData: return cls(scheme_code=0, raw_data=data) scheme_code = data[0] - scheme_info = uri_schemes_registry.get_uri_scheme_info(scheme_code) + scheme_info = get_uri_schemes_registry().get_uri_scheme_info(scheme_code) # Decode the URI suffix (remaining bytes after scheme code) try: diff --git a/src/bluetooth_sig/utils/prewarm.py b/src/bluetooth_sig/utils/prewarm.py index 9bc2fead..14d78a72 100644 --- a/src/bluetooth_sig/utils/prewarm.py +++ b/src/bluetooth_sig/utils/prewarm.py @@ -9,29 +9,7 @@ import logging -from ..gatt.characteristics.registry import CharacteristicRegistry -from ..gatt.services.registry import GattServiceRegistry -from ..registry.company_identifiers import company_identifiers_registry -from ..registry.core.ad_types import ad_types_registry -from ..registry.core.appearance_values import appearance_values_registry -from ..registry.core.class_of_device import class_of_device_registry -from ..registry.core.coding_format import coding_format_registry -from ..registry.core.formattypes import format_types_registry -from ..registry.core.namespace_description import namespace_description_registry -from ..registry.core.uri_schemes import uri_schemes_registry -from ..registry.profiles.permitted_characteristics import permitted_characteristics_registry -from ..registry.profiles.profile_lookup import profile_lookup_registry -from ..registry.service_discovery.attribute_ids import service_discovery_attribute_registry -from ..registry.uuids.browse_groups import browse_groups_registry -from ..registry.uuids.declarations import declarations_registry -from ..registry.uuids.members import members_registry -from ..registry.uuids.mesh_profiles import mesh_profiles_registry -from ..registry.uuids.object_types import object_types_registry -from ..registry.uuids.protocol_identifiers import protocol_identifiers_registry -from ..registry.uuids.sdo_uuids import sdo_uuids_registry -from ..registry.uuids.service_classes import service_classes_registry -from ..registry.uuids.units import units_registry -from ..types.uuid import BluetoothUUID +from .prewarm_catalog import get_prewarm_loaders logger = logging.getLogger(__name__) @@ -67,33 +45,10 @@ def prewarm_registries() -> None: - Protocol identifiers registry (protocol UUID identifiers) - SDO UUIDs registry (standards body UUIDs) - Service classes registry (service class UUIDs) + - UUID registry (service/characteristic/descriptor metadata hub) """ - CharacteristicRegistry.get_all_characteristics() - GattServiceRegistry.get_all_services() - - # Trigger UUID-keyed lookups to populate reverse maps. - GattServiceRegistry.get_service_class_by_uuid(BluetoothUUID("0000")) - CharacteristicRegistry.get_characteristic_class_by_uuid(BluetoothUUID("0000")) - - units_registry.ensure_loaded() - company_identifiers_registry.ensure_loaded() - ad_types_registry.ensure_loaded() - appearance_values_registry.ensure_loaded() - class_of_device_registry.ensure_loaded() - coding_format_registry.ensure_loaded() - format_types_registry.ensure_loaded() - namespace_description_registry.ensure_loaded() - uri_schemes_registry.ensure_loaded() - permitted_characteristics_registry.ensure_loaded() - profile_lookup_registry.ensure_loaded() - service_discovery_attribute_registry.ensure_loaded() - browse_groups_registry.ensure_loaded() - declarations_registry.ensure_loaded() - members_registry.ensure_loaded() - mesh_profiles_registry.ensure_loaded() - object_types_registry.ensure_loaded() - protocol_identifiers_registry.ensure_loaded() - sdo_uuids_registry.ensure_loaded() - service_classes_registry.ensure_loaded() + for loader_name, loader in get_prewarm_loaders(): + loader() + logger.debug("pre-warmed %s", loader_name) logger.debug("bluetooth-sig registries pre-warmed") diff --git a/src/bluetooth_sig/utils/prewarm_catalog.py b/src/bluetooth_sig/utils/prewarm_catalog.py new file mode 100644 index 00000000..f576941d --- /dev/null +++ b/src/bluetooth_sig/utils/prewarm_catalog.py @@ -0,0 +1,95 @@ +"""Authoritative registry prewarm catalogue. + +The catalogue derives prewarm targets from registry base-class descendants, +then appends explicit outliers that do not follow the shared registry base +hierarchy. +""" + +from __future__ import annotations + +import functools +import importlib +import pkgutil +import re +from collections.abc import Callable +from typing import Any + +from ..gatt.uuid_registry import get_uuid_registry +from ..registry.base import BaseGenericRegistry, BaseUUIDClassRegistry, BaseUUIDRegistry + +_REGISTRY_PACKAGE_ROOT = "bluetooth_sig.registry" +# These modules host BaseUUIDClassRegistry descendants used by public APIs. +_EXTRA_DISCOVERY_MODULES: tuple[str, ...] = ( + "bluetooth_sig.gatt.characteristics.registry", + "bluetooth_sig.gatt.services.registry", +) + +RegistryLoader = tuple[str, Callable[[], Any]] + + +def _to_snake_case(name: str) -> str: + """Convert CamelCase/PascalCase names to snake_case.""" + first_pass = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", first_pass).lower() + + +def _iter_subclasses(root: type[Any]) -> list[type[Any]]: + """Return all recursive subclasses of *root* (excluding the root itself).""" + discovered: list[type[Any]] = [] + stack = list(root.__subclasses__()) + seen: set[type[Any]] = set() + + while stack: + cls = stack.pop() + if cls in seen: + continue + seen.add(cls) + discovered.append(cls) + stack.extend(cls.__subclasses__()) + + return discovered + + +def _import_registry_modules() -> None: + """Import registry modules so subclass discovery has complete coverage.""" + registry_pkg = importlib.import_module(_REGISTRY_PACKAGE_ROOT) + if hasattr(registry_pkg, "__path__"): + for module_info in pkgutil.walk_packages(registry_pkg.__path__, f"{_REGISTRY_PACKAGE_ROOT}."): + importlib.import_module(module_info.name) + + for module_name in _EXTRA_DISCOVERY_MODULES: + importlib.import_module(module_name) + + +def _build_discovered_registry_loaders() -> tuple[RegistryLoader, ...]: + """Build ensure-loaded loaders from registry base-class descendants.""" + _import_registry_modules() + + registry_classes: set[type[Any]] = set() + registry_classes.update(_iter_subclasses(BaseGenericRegistry)) + registry_classes.update(_iter_subclasses(BaseUUIDRegistry)) + registry_classes.update(_iter_subclasses(BaseUUIDClassRegistry)) + + loaders: list[RegistryLoader] = [] + for registry_cls in sorted(registry_classes, key=lambda cls: f"{cls.__module__}.{cls.__qualname__}"): + loader_name = _to_snake_case(registry_cls.__name__.replace("Registry", "")) + "_registry" + loader = registry_cls.get_instance().ensure_loaded + loaders.append((loader_name, loader)) + + return tuple(loaders) + + +def _build_outlier_loaders() -> tuple[RegistryLoader, ...]: + """Build loaders for registries that do not use shared base registry classes.""" + return (("uuid_registry", lambda: get_uuid_registry().ensure_loaded()),) + + +@functools.lru_cache(maxsize=1) +def _cached_prewarm_loaders() -> tuple[RegistryLoader, ...]: + """Build and cache the full prewarm loader list.""" + return _build_discovered_registry_loaders() + _build_outlier_loaders() + + +def get_prewarm_loaders() -> tuple[RegistryLoader, ...]: + """Return cached prewarm loaders, building discovery state on first call.""" + return _cached_prewarm_loaders() diff --git a/tests/benchmarks/conftest.py b/tests/benchmarks/conftest.py index 176e5017..a1500580 100644 --- a/tests/benchmarks/conftest.py +++ b/tests/benchmarks/conftest.py @@ -57,11 +57,11 @@ def batch_characteristics_medium() -> dict[str, bytearray]: "2A19": bytearray([85]), # Battery Level "2A6E": bytearray([0x64, 0x09]), # Temperature "2A6F": bytearray([0x3A, 0x13]), # Humidity - "2A1C": bytearray([0x64, 0x09]), # Temperature Measurement - "2A1E": bytearray([0x00]), # Intermediate Temperature + "2A1C": bytearray([0x00, 0xE4, 0x00, 0x00, 0x00]), # Temperature Measurement (flags + float32) + "2A1E": bytearray([0x00, 0xE4, 0x00, 0x00, 0x00]), # Intermediate Temperature (flags + float32) "2A21": bytearray([0x3A, 0x13]), # Measurement Interval "2A23": bytearray([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]), # System ID - "2A25": bytearray([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]), # Serial Number String - "2A27": bytearray([0x01, 0x02, 0x03, 0x04]), # Hardware Revision - "2A28": bytearray([0x01, 0x02, 0x03, 0x04]), # Software Revision + "2A25": bytearray(b"SN1234"), # Serial Number String + "2A27": bytearray(b"HW1"), # Hardware Revision + "2A28": bytearray(b"SW1"), # Software Revision } diff --git a/tests/benchmarks/test_performance.py b/tests/benchmarks/test_performance.py index 05cffae4..a8a7d716 100644 --- a/tests/benchmarks/test_performance.py +++ b/tests/benchmarks/test_performance.py @@ -7,6 +7,7 @@ import pytest from bluetooth_sig.core.translator import BluetoothSIGTranslator +from bluetooth_sig.gatt.uuid_registry import UuidRegistry @pytest.mark.benchmark @@ -155,3 +156,26 @@ def batch_loop() -> list[dict[str, Any]]: results = benchmark(batch_loop) assert len(results) == 100 assert all(len(r) == 3 for r in results) + + +@pytest.mark.benchmark +class TestRegistryLifecyclePerformance: + """Benchmark registry lifecycle paths explicitly.""" + + def test_uuid_registry_cold_load(self, benchmark: Any) -> None: + """Benchmark cold UUID registry load from a fresh instance.""" + + def cold_load() -> object: + registry = UuidRegistry() + return registry.get_characteristic_info("2A19") + + result = benchmark(cold_load) + assert result is not None + + def test_uuid_registry_warm_lookup(self, benchmark: Any) -> None: + """Benchmark warm UUID registry lookup after explicit preload.""" + registry = UuidRegistry() + registry.ensure_loaded() + + result = benchmark(registry.get_characteristic_info, "2A19") + assert result is not None diff --git a/tests/conftest.py b/tests/conftest.py index 7d05ef87..b2483dbc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -61,12 +61,12 @@ def clear_module_level_registrations() -> None: """ from bluetooth_sig.gatt.characteristics.registry import CharacteristicRegistry from bluetooth_sig.gatt.services.registry import GattServiceRegistry - from bluetooth_sig.gatt.uuid_registry import uuid_registry + from bluetooth_sig.gatt.uuid_registry import get_uuid_registry # Clear all custom registrations that happened during module imports CharacteristicRegistry.clear_custom_registrations() GattServiceRegistry.clear_custom_registrations() - uuid_registry.clear_custom_registrations() + get_uuid_registry().clear_custom_registrations() @pytest.fixture(autouse=True) @@ -95,16 +95,16 @@ def reset_registries() -> Generator[None, None, None]: """ from bluetooth_sig.gatt.characteristics.registry import CharacteristicRegistry from bluetooth_sig.gatt.services.registry import GattServiceRegistry - from bluetooth_sig.gatt.uuid_registry import uuid_registry + from bluetooth_sig.gatt.uuid_registry import get_uuid_registry # Clear custom registrations BEFORE test CharacteristicRegistry.clear_custom_registrations() GattServiceRegistry.clear_custom_registrations() - uuid_registry.clear_custom_registrations() + get_uuid_registry().clear_custom_registrations() yield # Clear custom registrations AFTER test CharacteristicRegistry.clear_custom_registrations() GattServiceRegistry.clear_custom_registrations() - uuid_registry.clear_custom_registrations() + get_uuid_registry().clear_custom_registrations() diff --git a/tests/gatt/test_uuid_registry_lifecycle.py b/tests/gatt/test_uuid_registry_lifecycle.py new file mode 100644 index 00000000..05d00860 --- /dev/null +++ b/tests/gatt/test_uuid_registry_lifecycle.py @@ -0,0 +1,63 @@ +"""Lifecycle tests for UuidRegistry lazy loading semantics.""" + +from __future__ import annotations + +import pytest + +from bluetooth_sig.gatt.uuid_registry import UuidRegistry + + +class TestUuidRegistryLifecycle: + """Validate lazy loading, single-load behaviour, and sticky failures.""" + + def test_constructor_does_not_load_yaml(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Constructing registry must not trigger YAML loading.""" + calls = 0 + + def _fake_load(self: UuidRegistry) -> None: + nonlocal calls + calls += 1 + + monkeypatch.setattr(UuidRegistry, "_load_uuids", _fake_load) + + registry = UuidRegistry() + + assert calls == 0 + assert registry._loaded is False # pylint: disable=protected-access + + def test_first_lookup_loads_once(self, monkeypatch: pytest.MonkeyPatch) -> None: + """First lookup should load once, subsequent lookups should not reload.""" + calls = 0 + + def _fake_load(self: UuidRegistry) -> None: + nonlocal calls + calls += 1 + + monkeypatch.setattr(UuidRegistry, "_load_uuids", _fake_load) + registry = UuidRegistry() + + assert registry.get_service_info("2A19") is None + assert registry.get_service_info("2A19") is None + + assert calls == 1 + assert registry._loaded is True # pylint: disable=protected-access + + def test_failed_load_is_sticky(self, monkeypatch: pytest.MonkeyPatch) -> None: + """A failed load should be surfaced deterministically and not retried implicitly.""" + calls = 0 + + def _failing_load(self: UuidRegistry) -> None: + nonlocal calls + calls += 1 + raise FileNotFoundError("simulated missing YAML") + + monkeypatch.setattr(UuidRegistry, "_load_uuids", _failing_load) + registry = UuidRegistry() + + with pytest.raises(RuntimeError, match="failed to load SIG data"): + registry.get_characteristic_info("2A19") + + with pytest.raises(RuntimeError, match="failed to load SIG data"): + registry.get_characteristic_info("2A19") + + assert calls == 1 diff --git a/tests/integration/test_custom_registration.py b/tests/integration/test_custom_registration.py index b229fde7..f4234c57 100644 --- a/tests/integration/test_custom_registration.py +++ b/tests/integration/test_custom_registration.py @@ -14,7 +14,7 @@ from bluetooth_sig.gatt.context import CharacteristicContext from bluetooth_sig.gatt.services.custom import CustomBaseGattService from bluetooth_sig.gatt.services.registry import GattServiceRegistry -from bluetooth_sig.gatt.uuid_registry import uuid_registry +from bluetooth_sig.gatt.uuid_registry import get_uuid_registry from bluetooth_sig.types import CharacteristicInfo, ServiceInfo from bluetooth_sig.types.gatt_enums import GattProperty from bluetooth_sig.types.uuid import BluetoothUUID @@ -72,7 +72,7 @@ def _reset_registries(self, reset_registries: None) -> None: def test_register_custom_characteristic_metadata(self) -> None: """Test registering custom characteristic metadata.""" - uuid_registry.register_characteristic( + get_uuid_registry().register_characteristic( uuid=BluetoothUUID("abcd1234-0000-1000-8000-00805f9b34fb"), name="Test Characteristic", unit="°C", @@ -80,7 +80,7 @@ def test_register_custom_characteristic_metadata(self) -> None: ) # Verify registration - info = uuid_registry.get_characteristic_info("abcd1234-0000-1000-8000-00805f9b34fb") + info = get_uuid_registry().get_characteristic_info("abcd1234-0000-1000-8000-00805f9b34fb") assert info is not None assert info.name == "Test Characteristic" assert info.unit == "°C" @@ -88,13 +88,13 @@ def test_register_custom_characteristic_metadata(self) -> None: def test_register_custom_service_metadata(self) -> None: """Test registering custom service metadata.""" - uuid_registry.register_service( + get_uuid_registry().register_service( uuid=BluetoothUUID("12345678-1234-1234-1234-123456789abc"), name="Test Service", ) # Verify registration - info = uuid_registry.get_service_info("12345678-1234-1234-1234-123456789abc") + info = get_uuid_registry().get_service_info("12345678-1234-1234-1234-123456789abc") assert info is not None assert info.name == "Test Service" @@ -134,7 +134,7 @@ def test_translator_register_custom_characteristic(self) -> None: assert cls == CustomCharacteristicImpl # Verify metadata registration - info = uuid_registry.get_characteristic_info("abcd1234-0000-1000-8000-00805f9b34fb") + info = get_uuid_registry().get_characteristic_info("abcd1234-0000-1000-8000-00805f9b34fb") assert info is not None assert info.name == "Test Characteristic" assert info.unit == "°C" @@ -157,7 +157,7 @@ def test_translator_register_custom_service(self) -> None: assert cls == CustomServiceImpl # Verify metadata registration - info = uuid_registry.get_service_info("12345678-1234-1234-1234-123456789abc") + info = get_uuid_registry().get_service_info("12345678-1234-1234-1234-123456789abc") assert info is not None assert info.name == "Test Service" @@ -180,7 +180,7 @@ def test_conflict_detection(self) -> None: """Test that conflicts with SIG entries are detected.""" # Try to register a known SIG UUID without override with pytest.raises(ValueError, match="conflicts with existing SIG"): - uuid_registry.register_characteristic( + get_uuid_registry().register_characteristic( uuid=BluetoothUUID("2A19"), # Battery Level UUID name="Custom Battery", unit="%", @@ -190,7 +190,7 @@ def test_conflict_detection(self) -> None: def test_override_allowed(self) -> None: """Test that override=True allows replacing SIG entries.""" # Should not raise with override=True - uuid_registry.register_characteristic( + get_uuid_registry().register_characteristic( uuid=BluetoothUUID("2A19"), # Battery Level UUID name="Custom Battery", unit="%", @@ -199,7 +199,7 @@ def test_override_allowed(self) -> None: ) # Verify override worked - info = uuid_registry.get_characteristic_info("2A19") + info = get_uuid_registry().get_characteristic_info("2A19") assert info is not None assert info.name == "Custom Battery" diff --git a/tests/integration/test_format_types_integration.py b/tests/integration/test_format_types_integration.py index 9d851ce8..00e19a57 100644 --- a/tests/integration/test_format_types_integration.py +++ b/tests/integration/test_format_types_integration.py @@ -2,7 +2,7 @@ from __future__ import annotations -from bluetooth_sig.registry.core.formattypes import format_types_registry +from bluetooth_sig.registry.core.formattypes import get_format_types_registry from bluetooth_sig.types.gatt_enums import WIRE_TYPE_MAP from bluetooth_sig.types.registry.formattypes import FormatTypeInfo @@ -12,7 +12,7 @@ class TestFormatTypesIntegration: def test_registry_loads_format_types(self) -> None: """Test that registry successfully loads format types from YAML.""" - format_types = format_types_registry.get_all_format_types() + format_types = get_format_types_registry().get_all_format_types() # Should have at least the standard format types assert len(format_types) >= 28 @@ -26,17 +26,17 @@ def test_registry_loads_format_types(self) -> None: def test_format_type_info_matches_hardcoded_enum(self) -> None: """Test that registry format types match the hardcoded FormatType enum values.""" # Test some known format types - registry should be loaded - boolean_info = format_types_registry.get_format_type_info(0x01) + boolean_info = get_format_types_registry().get_format_type_info(0x01) assert boolean_info is not None assert boolean_info.value == 0x01 assert boolean_info.short_name == "boolean" - utf8s_info = format_types_registry.get_format_type_info(0x19) + utf8s_info = get_format_types_registry().get_format_type_info(0x19) assert utf8s_info is not None assert utf8s_info.value == 0x19 assert utf8s_info.short_name == "utf8s" - utf16s_info = format_types_registry.get_format_type_info(0x1A) + utf16s_info = get_format_types_registry().get_format_type_info(0x1A) assert utf16s_info is not None assert utf16s_info.value == 0x1A assert utf16s_info.short_name == "utf16s" @@ -51,17 +51,17 @@ def test_wire_type_map_integration(self) -> None: assert WIRE_TYPE_MAP["utf16s"] is WIRE_TYPE_MAP["utf8s"] def test_registry_singleton(self) -> None: - """Test that format_types_registry is a proper singleton.""" - from bluetooth_sig.registry.core import format_types_registry as imported_registry - from bluetooth_sig.registry.core import format_types_registry as main_imported_registry + """Test that get_format_types_registry is a proper singleton.""" + from bluetooth_sig.registry.core import get_format_types_registry as imported_registry + from bluetooth_sig.registry.core import get_format_types_registry as main_imported_registry # All imports should reference the same instance - assert format_types_registry is imported_registry - assert format_types_registry is main_imported_registry + assert get_format_types_registry is imported_registry + assert get_format_types_registry is main_imported_registry def test_format_type_info_structure(self) -> None: """Test that FormatTypeInfo has all required fields.""" - info = format_types_registry.get_format_type_info(0x01) + info = get_format_types_registry().get_format_type_info(0x01) assert info is not None # Should be a proper dataclass/struct assert isinstance(info, FormatTypeInfo) @@ -76,19 +76,19 @@ def test_format_type_info_structure(self) -> None: def test_utf16_format_type_present(self) -> None: """Test that UTF-16 format type is properly registered.""" - utf16s_info = format_types_registry.get_format_type_info(0x1A) + utf16s_info = get_format_types_registry().get_format_type_info(0x1A) assert utf16s_info is not None assert utf16s_info.short_name == "utf16s" assert "utf-16" in utf16s_info.description.lower() assert utf16s_info.value == 0x1A # Also check by name - utf16s_by_name = format_types_registry.get_format_type_by_name("utf16s") + utf16s_by_name = get_format_types_registry().get_format_type_by_name("utf16s") assert utf16s_by_name == utf16s_info def test_all_format_types_have_valid_data(self) -> None: """Test that all loaded format types have valid data.""" - format_types = format_types_registry.get_all_format_types() + format_types = get_format_types_registry().get_all_format_types() for value, info in format_types.items(): # Basic validation diff --git a/tests/registry/core/test_coding_format.py b/tests/registry/core/test_coding_format.py index 86e2d09c..0746745a 100644 --- a/tests/registry/core/test_coding_format.py +++ b/tests/registry/core/test_coding_format.py @@ -8,7 +8,7 @@ from bluetooth_sig.registry.core.coding_format import ( CodingFormatRegistry, - coding_format_registry, + get_coding_format_registry, ) from bluetooth_sig.types.registry.coding_format import CodingFormatInfo @@ -16,7 +16,7 @@ @pytest.fixture(scope="session") def registry() -> CodingFormatRegistry: """Create a coding format registry once per test session.""" - return coding_format_registry + return get_coding_format_registry() class TestCodingFormatRegistry: diff --git a/tests/registry/core/test_formattypes.py b/tests/registry/core/test_formattypes.py index f302bae6..5377e19a 100644 --- a/tests/registry/core/test_formattypes.py +++ b/tests/registry/core/test_formattypes.py @@ -6,14 +6,14 @@ import pytest -from bluetooth_sig.registry.core.formattypes import FormatTypesRegistry, format_types_registry +from bluetooth_sig.registry.core.formattypes import FormatTypesRegistry, get_format_types_registry from bluetooth_sig.types.registry.formattypes import FormatTypeInfo @pytest.fixture(scope="session") def registry() -> FormatTypesRegistry: """Create a format types registry once per test session.""" - return format_types_registry + return get_format_types_registry() class TestFormatTypesRegistry: @@ -167,11 +167,11 @@ def lookup_format_type(value: int) -> None: assert all(r == results[0] for r in results) # All results should be identical def test_singleton_instance(self) -> None: - """Test that format_types_registry is a singleton.""" - from bluetooth_sig.registry.core.formattypes import format_types_registry as imported_registry + """Test that get_format_types_registry is a singleton.""" + from bluetooth_sig.registry.core.formattypes import get_format_types_registry as imported_registry # Should be the same instance - assert format_types_registry is imported_registry + assert get_format_types_registry is imported_registry def test_standard_format_types_present(self, registry: FormatTypesRegistry) -> None: """Test that standard format types are present.""" diff --git a/tests/registry/core/test_namespace_description.py b/tests/registry/core/test_namespace_description.py index 5dc8827e..1d9dcd07 100644 --- a/tests/registry/core/test_namespace_description.py +++ b/tests/registry/core/test_namespace_description.py @@ -8,7 +8,7 @@ from bluetooth_sig.registry.core.namespace_description import ( NamespaceDescriptionRegistry, - namespace_description_registry, + get_namespace_description_registry, ) from bluetooth_sig.types.registry.namespace import NamespaceDescriptionInfo @@ -16,7 +16,7 @@ @pytest.fixture(scope="session") def registry() -> NamespaceDescriptionRegistry: """Create a namespace description registry once per test session.""" - return namespace_description_registry + return get_namespace_description_registry() class TestNamespaceDescriptionRegistry: diff --git a/tests/registry/core/test_uri_schemes.py b/tests/registry/core/test_uri_schemes.py index b3fcafe9..1bb0b2ef 100644 --- a/tests/registry/core/test_uri_schemes.py +++ b/tests/registry/core/test_uri_schemes.py @@ -8,7 +8,7 @@ from bluetooth_sig.registry.core.uri_schemes import ( UriSchemesRegistry, - uri_schemes_registry, + get_uri_schemes_registry, ) from bluetooth_sig.types.registry.uri_schemes import UriSchemeInfo @@ -16,7 +16,7 @@ @pytest.fixture(scope="session") def registry() -> UriSchemesRegistry: """Create a URI schemes registry once per test session.""" - return uri_schemes_registry + return get_uri_schemes_registry() class TestUriSchemesRegistry: diff --git a/tests/registry/test_ad_types.py b/tests/registry/test_ad_types.py index 0e8b321a..5f4e2042 100644 --- a/tests/registry/test_ad_types.py +++ b/tests/registry/test_ad_types.py @@ -6,14 +6,14 @@ import pytest -from bluetooth_sig.registry.core.ad_types import ADTypesRegistry, ad_types_registry +from bluetooth_sig.registry.core.ad_types import ADTypesRegistry, get_ad_types_registry from bluetooth_sig.types.registry.ad_types import AdTypeInfo @pytest.fixture(scope="session") def registry() -> ADTypesRegistry: """Create an AD types registry once per test session.""" - return ad_types_registry + return get_ad_types_registry() class TestADTypesRegistry: @@ -148,11 +148,11 @@ def lookup_ad_type(ad_type: int) -> None: assert all(r == results[0] for r in results) def test_singleton_instance(self) -> None: - """Test that ad_types_registry is a singleton.""" - from bluetooth_sig.registry.core.ad_types import ad_types_registry as imported_registry + """Test that get_ad_types_registry is a singleton.""" + from bluetooth_sig.registry.core.ad_types import get_ad_types_registry as imported_registry # Should be the same instance - assert ad_types_registry is imported_registry + assert get_ad_types_registry is imported_registry def test_standard_ad_types_present(self, registry: ADTypesRegistry) -> None: """Test that standard AD types are present if YAML is loaded.""" diff --git a/tests/registry/test_protocol_identifiers.py b/tests/registry/test_protocol_identifiers.py index a163e24f..ec13f042 100644 --- a/tests/registry/test_protocol_identifiers.py +++ b/tests/registry/test_protocol_identifiers.py @@ -2,20 +2,20 @@ from __future__ import annotations -from typing import cast - import pytest -from bluetooth_sig.registry.uuids.protocol_identifiers import ProtocolIdentifiersRegistry, ProtocolInfo +from bluetooth_sig.registry.uuids.protocol_identifiers import ( + ProtocolIdentifiersRegistry, + ProtocolInfo, + get_protocol_identifiers_registry, +) from bluetooth_sig.types.uuid import BluetoothUUID @pytest.fixture(scope="session") def protocol_identifiers_registry() -> ProtocolIdentifiersRegistry: """Get the protocol identifiers registry singleton instance.""" - # NOTE: cast required because BaseRegistry.get_instance() returns BaseRegistry[T] - # but we know the concrete type is ProtocolIdentifiersRegistry - return cast("ProtocolIdentifiersRegistry", ProtocolIdentifiersRegistry.get_instance()) + return ProtocolIdentifiersRegistry.get_instance() class TestProtocolIdentifiersRegistry: # pylint: disable=too-many-public-methods @@ -25,7 +25,7 @@ def test_registry_initialization(self, protocol_identifiers_registry: ProtocolId """Test that the registry initializes properly.""" assert isinstance(protocol_identifiers_registry, ProtocolIdentifiersRegistry) # Should have loaded some protocols if YAML exists - protocols = protocol_identifiers_registry.get_all_protocols() + protocols = get_protocol_identifiers_registry().get_all_protocols() assert isinstance(protocols, list) # If submodule is initialized, should have protocols if protocols: @@ -34,7 +34,7 @@ def test_registry_initialization(self, protocol_identifiers_registry: ProtocolId def test_get_protocol_info_l2cap(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test lookup of L2CAP protocol by UUID.""" # Test with L2CAP UUID (0x0100) - info = protocol_identifiers_registry.get_protocol_info("0x0100") + info = get_protocol_identifiers_registry().get_protocol_info("0x0100") if info: # Only if YAML loaded assert isinstance(info, ProtocolInfo) assert info.name == "L2CAP" @@ -43,7 +43,7 @@ def test_get_protocol_info_l2cap(self, protocol_identifiers_registry: ProtocolId def test_get_protocol_info_rfcomm(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test lookup of RFCOMM protocol by UUID.""" # Test with RFCOMM UUID (0x0003) - info = protocol_identifiers_registry.get_protocol_info("0x0003") + info = get_protocol_identifiers_registry().get_protocol_info("0x0003") if info: # Only if YAML loaded assert isinstance(info, ProtocolInfo) assert info.name == "RFCOMM" @@ -52,7 +52,7 @@ def test_get_protocol_info_rfcomm(self, protocol_identifiers_registry: ProtocolI def test_get_protocol_info_avdtp(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test lookup of AVDTP protocol by UUID.""" # Test with AVDTP UUID (0x0019) - info = protocol_identifiers_registry.get_protocol_info("0x0019") + info = get_protocol_identifiers_registry().get_protocol_info("0x0019") if info: # Only if YAML loaded assert isinstance(info, ProtocolInfo) assert info.name == "AVDTP" @@ -61,7 +61,7 @@ def test_get_protocol_info_avdtp(self, protocol_identifiers_registry: ProtocolId def test_get_protocol_info_bnep(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test lookup of BNEP protocol by UUID.""" # Test with BNEP UUID (0x000F) - info = protocol_identifiers_registry.get_protocol_info("0x000F") + info = get_protocol_identifiers_registry().get_protocol_info("0x000F") if info: # Only if YAML loaded assert isinstance(info, ProtocolInfo) assert info.name == "BNEP" @@ -70,35 +70,35 @@ def test_get_protocol_info_bnep(self, protocol_identifiers_registry: ProtocolIde def test_get_protocol_info_by_name(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test lookup by protocol name.""" # Test with known protocol name (L2CAP) - info = protocol_identifiers_registry.get_protocol_info("L2CAP") + info = get_protocol_identifiers_registry().get_protocol_info("L2CAP") if info: # Only if YAML loaded assert isinstance(info, ProtocolInfo) assert info.name == "L2CAP" assert info.uuid.short_form.upper() == "0100" # Test case insensitive - info_lower = protocol_identifiers_registry.get_protocol_info("l2cap") + info_lower = get_protocol_identifiers_registry().get_protocol_info("l2cap") assert info_lower == info # Test not found - info_none = protocol_identifiers_registry.get_protocol_info("Nonexistent Protocol") + info_none = get_protocol_identifiers_registry().get_protocol_info("Nonexistent Protocol") assert info_none is None def test_get_protocol_info_by_name_method(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test dedicated get_protocol_info_by_name method.""" # Test with known protocol name - info = protocol_identifiers_registry.get_protocol_info_by_name("RFCOMM") + info = get_protocol_identifiers_registry().get_protocol_info_by_name("RFCOMM") if info: # Only if YAML loaded assert isinstance(info, ProtocolInfo) assert info.name == "RFCOMM" assert info.uuid.short_form.upper() == "0003" # Test case insensitive - info_lower = protocol_identifiers_registry.get_protocol_info_by_name("rfcomm") + info_lower = get_protocol_identifiers_registry().get_protocol_info_by_name("rfcomm") assert info_lower == info # Test not found - info_none = protocol_identifiers_registry.get_protocol_info_by_name("Nonexistent Protocol") + info_none = get_protocol_identifiers_registry().get_protocol_info_by_name("Nonexistent Protocol") assert info_none is None def test_get_protocol_info_by_bluetooth_uuid( @@ -107,7 +107,7 @@ def test_get_protocol_info_by_bluetooth_uuid( """Test lookup by BluetoothUUID object.""" # Create a BluetoothUUID for L2CAP bt_uuid = BluetoothUUID("0100") - info = protocol_identifiers_registry.get_protocol_info(bt_uuid) + info = get_protocol_identifiers_registry().get_protocol_info(bt_uuid) if info: # Only if YAML loaded assert isinstance(info, ProtocolInfo) assert info.name == "L2CAP" @@ -115,40 +115,40 @@ def test_get_protocol_info_by_bluetooth_uuid( def test_get_protocol_info_by_int(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test lookup by integer UUID.""" # Test with RFCOMM as integer (0x0003 = 3) - info = protocol_identifiers_registry.get_protocol_info("0003") + info = get_protocol_identifiers_registry().get_protocol_info("0003") if info: # Only if YAML loaded assert isinstance(info, ProtocolInfo) assert info.name == "RFCOMM" # Test with L2CAP as integer (0x0100 = 256) - info = protocol_identifiers_registry.get_protocol_info("0100") + info = get_protocol_identifiers_registry().get_protocol_info("0100") if info: # Only if YAML loaded assert isinstance(info, ProtocolInfo) assert info.name == "L2CAP" def test_get_protocol_info_not_found(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test lookup for non-existent protocol.""" - info = protocol_identifiers_registry.get_protocol_info("nonexistent") + info = get_protocol_identifiers_registry().get_protocol_info("nonexistent") assert info is None - info = protocol_identifiers_registry.get_protocol_info("0xFFFF") # Not a protocol UUID + info = get_protocol_identifiers_registry().get_protocol_info("0xFFFF") # Not a protocol UUID assert info is None def test_is_known_protocol(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test protocol UUID validation.""" # Known protocol UUID - has_protocols = bool(protocol_identifiers_registry.get_all_protocols()) - assert protocol_identifiers_registry.is_known_protocol("0x0100") or not has_protocols # L2CAP + has_protocols = bool(get_protocol_identifiers_registry().get_all_protocols()) + assert get_protocol_identifiers_registry().is_known_protocol("0x0100") or not has_protocols # L2CAP # Non-protocol UUID - assert not protocol_identifiers_registry.is_known_protocol("0xFFFF") + assert not get_protocol_identifiers_registry().is_known_protocol("0xFFFF") # Invalid UUID - assert not protocol_identifiers_registry.is_known_protocol("invalid") + assert not get_protocol_identifiers_registry().is_known_protocol("invalid") def test_get_all_protocols(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test getting all protocols.""" - protocols = protocol_identifiers_registry.get_all_protocols() + protocols = get_protocol_identifiers_registry().get_all_protocols() assert isinstance(protocols, list) if protocols: @@ -162,7 +162,7 @@ def test_get_all_protocols(self, protocol_identifiers_registry: ProtocolIdentifi def test_protocol_info_structure(self, protocol_identifiers_registry: ProtocolIdentifiersRegistry) -> None: """Test ProtocolInfo dataclass structure.""" - protocols = protocol_identifiers_registry.get_all_protocols() + protocols = get_protocol_identifiers_registry().get_all_protocols() if protocols: protocol = protocols[0] assert hasattr(protocol, "uuid") @@ -174,8 +174,8 @@ def test_uuid_formats(self, protocol_identifiers_registry: ProtocolIdentifiersRe """Test various UUID input formats.""" formats: list[str | BluetoothUUID] = ["0100", "0x0100", "0X0100", BluetoothUUID(256)] for fmt in formats: - info = protocol_identifiers_registry.get_protocol_info(fmt) - if protocol_identifiers_registry.is_known_protocol("0100"): + info = get_protocol_identifiers_registry().get_protocol_info(fmt) + if get_protocol_identifiers_registry().is_known_protocol("0100"): assert info is not None assert info.name == "L2CAP" @@ -192,7 +192,7 @@ def test_well_known_protocols(self, protocol_identifiers_registry: ProtocolIdent } for name, uuid in expected_protocols.items(): - info = protocol_identifiers_registry.get_protocol_info(name) + info = get_protocol_identifiers_registry().get_protocol_info(name) # Only assert if the protocol exists (may not in minimal test data) if info: assert info.uuid.short_form.upper() == uuid, f"Expected {name} to have UUID {uuid}" @@ -215,7 +215,7 @@ def test_thread_safety(self, protocol_identifiers_registry: ProtocolIdentifiersR def lookup_protocol() -> None: try: time.sleep(0.001) # Small delay to increase concurrency - info = protocol_identifiers_registry.get_protocol_info("L2CAP") + info = get_protocol_identifiers_registry().get_protocol_info("L2CAP") results.put(info) except Exception as e: errors.append(e) diff --git a/tests/registry/test_registry_validation.py b/tests/registry/test_registry_validation.py index b7ae95e1..bf6920a0 100644 --- a/tests/registry/test_registry_validation.py +++ b/tests/registry/test_registry_validation.py @@ -15,7 +15,7 @@ from bluetooth_sig.gatt.characteristics.base import BaseCharacteristic from bluetooth_sig.gatt.context import CharacteristicContext from bluetooth_sig.gatt.services.base import BaseGattService -from bluetooth_sig.gatt.uuid_registry import uuid_registry +from bluetooth_sig.gatt.uuid_registry import get_uuid_registry from bluetooth_sig.types import CharacteristicInfo from bluetooth_sig.types.uuid import BluetoothUUID @@ -138,7 +138,7 @@ def test_characteristic_in_yaml_registry(self, char_class: type[BaseCharacterist name = char.name # Check if UUID exists in registry - char_info = uuid_registry.get_characteristic_info(uuid) + char_info = get_uuid_registry().get_characteristic_info(uuid) assert char_info is not None, ( f"Characteristic UUID '{uuid}' for {char_class.__name__} not found in YAML registry" ) @@ -203,7 +203,7 @@ def test_characteristic_name_resolution(self, char_class: type[BaseCharacteristi assert name, f"Characteristic {char_class.__name__} has empty name" # Try to look up by name in registry - char_info = uuid_registry.get_characteristic_info(name) + char_info = get_uuid_registry().get_characteristic_info(name) assert char_info is not None, ( f"Characteristic name '{name}' for {char_class.__name__} not found in YAML registry" ) @@ -277,7 +277,7 @@ def test_yaml_completeness(self) -> None: try: service = service_class() service_uuid = service.uuid - service_info = uuid_registry.get_service_info(service_uuid) + service_info = get_uuid_registry().get_service_info(service_uuid) if service_info is None: missing_services.append(f"{service_class.__name__} (UUID: {service_uuid})") except Exception: @@ -292,7 +292,7 @@ def test_yaml_completeness(self) -> None: missing_characteristics.append(f"{char_class.__name__} (failed to resolve UUID)") continue # char_uuid is now narrowed to BluetoothUUID (non-None) - char_info = uuid_registry.get_characteristic_info(char_uuid) + char_info = get_uuid_registry().get_characteristic_info(char_uuid) if char_info is None: missing_characteristics.append(f"{char_class.__name__} (UUID: {char_uuid})") except Exception: diff --git a/tests/registry/test_uri_schemes.py b/tests/registry/test_uri_schemes.py index 9a9ac9d3..64870ba5 100644 --- a/tests/registry/test_uri_schemes.py +++ b/tests/registry/test_uri_schemes.py @@ -4,7 +4,7 @@ import pytest -from bluetooth_sig.registry.core.uri_schemes import uri_schemes_registry +from bluetooth_sig.registry.core.uri_schemes import get_uri_schemes_registry from bluetooth_sig.types.uri import URIData @@ -13,52 +13,52 @@ class TestUriSchemesRegistry: def test_registry_loads(self) -> None: """Test that URI schemes registry loads successfully.""" - schemes = uri_schemes_registry.get_all_uri_schemes() + schemes = get_uri_schemes_registry().get_all_uri_schemes() assert len(schemes) > 0, "No URI schemes loaded" # Should have common schemes like http, https assert len(schemes) >= 50, f"Expected at least 50 schemes, got {len(schemes)}" def test_get_http_scheme(self) -> None: """Test getting http scheme info.""" - info = uri_schemes_registry.get_uri_scheme_info(0x16) + info = get_uri_schemes_registry().get_uri_scheme_info(0x16) assert info is not None assert info.name == "http:" assert info.value == 0x16 def test_get_https_scheme(self) -> None: """Test getting https scheme info.""" - info = uri_schemes_registry.get_uri_scheme_info(0x17) + info = get_uri_schemes_registry().get_uri_scheme_info(0x17) assert info is not None assert info.name == "https:" assert info.value == 0x17 def test_get_scheme_by_name(self) -> None: """Test getting scheme info by name.""" - info = uri_schemes_registry.get_uri_scheme_by_name("http:") + info = get_uri_schemes_registry().get_uri_scheme_by_name("http:") assert info is not None assert info.value == 0x16 def test_get_scheme_by_name_case_insensitive(self) -> None: """Test case insensitive name lookup.""" - info = uri_schemes_registry.get_uri_scheme_by_name("HTTP:") + info = get_uri_schemes_registry().get_uri_scheme_by_name("HTTP:") assert info is not None assert info.value == 0x16 def test_unknown_scheme(self) -> None: """Test unknown scheme returns None.""" - info = uri_schemes_registry.get_uri_scheme_info(0xFFFF) + info = get_uri_schemes_registry().get_uri_scheme_info(0xFFFF) assert info is None def test_is_known_scheme(self) -> None: """Test is_known_uri_scheme method.""" - assert uri_schemes_registry.is_known_uri_scheme(0x16) is True - assert uri_schemes_registry.is_known_uri_scheme(0xFFFF) is False + assert get_uri_schemes_registry().is_known_uri_scheme(0x16) is True + assert get_uri_schemes_registry().is_known_uri_scheme(0xFFFF) is False def test_decode_uri_prefix(self) -> None: """Test decode_uri_prefix convenience method.""" - assert uri_schemes_registry.decode_uri_prefix(0x16) == "http:" - assert uri_schemes_registry.decode_uri_prefix(0x17) == "https:" - assert uri_schemes_registry.decode_uri_prefix(0xFFFF) == "" + assert get_uri_schemes_registry().decode_uri_prefix(0x16) == "http:" + assert get_uri_schemes_registry().decode_uri_prefix(0x17) == "https:" + assert get_uri_schemes_registry().decode_uri_prefix(0xFFFF) == "" class TestURIData: @@ -135,7 +135,7 @@ def test_from_plain_uri(self) -> None: def test_ftp_scheme(self) -> None: """Test FTP scheme parsing.""" - info = uri_schemes_registry.get_uri_scheme_info(0x11) + info = get_uri_schemes_registry().get_uri_scheme_info(0x11) assert info is not None assert info.name == "ftp:" @@ -147,7 +147,7 @@ def test_ftp_scheme(self) -> None: def test_tel_scheme(self) -> None: """Test tel: scheme for phone numbers.""" # Find tel: scheme value - info = uri_schemes_registry.get_uri_scheme_by_name("tel:") + info = get_uri_schemes_registry().get_uri_scheme_by_name("tel:") if info: raw_data = bytes([info.value]) + b"+1-555-123-4567" uri_data = URIData.from_raw_data(raw_data) @@ -181,6 +181,6 @@ def test_uri_data_in_advertising(self) -> None: ) def test_common_schemes(self, scheme_code: int, expected_scheme: str) -> None: """Test common URI schemes are correctly resolved.""" - info = uri_schemes_registry.get_uri_scheme_info(scheme_code) + info = get_uri_schemes_registry().get_uri_scheme_info(scheme_code) assert info is not None assert info.name == expected_scheme diff --git a/tests/registry/test_yaml_units.py b/tests/registry/test_yaml_units.py index 7ab7a5fb..dae0e184 100644 --- a/tests/registry/test_yaml_units.py +++ b/tests/registry/test_yaml_units.py @@ -8,7 +8,7 @@ from bluetooth_sig.gatt.characteristics.battery_level import BatteryLevelCharacteristic from bluetooth_sig.gatt.characteristics.humidity import HumidityCharacteristic from bluetooth_sig.gatt.characteristics.temperature import TemperatureCharacteristic -from bluetooth_sig.gatt.uuid_registry import uuid_registry +from bluetooth_sig.gatt.uuid_registry import get_uuid_registry class TestYAMLUnitParsing: @@ -19,20 +19,20 @@ def test_yaml_unit_loading_basic(self) -> None: characteristics. """ # Test Temperature characteristic - should get "°C" from YAML - temp_info = uuid_registry.get_characteristic_info("Temperature") + temp_info = get_uuid_registry().get_characteristic_info("Temperature") assert temp_info is not None, "Temperature characteristic should be found in registry" # Units may come from YAML or be None if YAML loading failed if temp_info.unit: assert temp_info.unit == "°C", f"Temperature unit should be °C, got {temp_info.unit}" # Test Battery Level characteristic - should get "%" from YAML - battery_info = uuid_registry.get_characteristic_info("Battery Level") + battery_info = get_uuid_registry().get_characteristic_info("Battery Level") assert battery_info is not None, "Battery Level characteristic should be found in registry" if battery_info.unit: assert battery_info.unit == "%", f"Battery Level unit should be %, got {battery_info.unit}" # Test Humidity characteristic - should get "%" from YAML - humidity_info = uuid_registry.get_characteristic_info("Humidity") + humidity_info = get_uuid_registry().get_characteristic_info("Humidity") assert humidity_info is not None, "Humidity characteristic should be found in registry" if humidity_info.unit: assert humidity_info.unit == "%", f"Humidity unit should be %, got {humidity_info.unit}" @@ -67,7 +67,7 @@ def test_manual_unit_priority(self) -> None: def test_unknown_characteristic_unit(self) -> None: """Test behaviour with characteristics not in YAML.""" # Try to get info for a characteristic that doesn't exist - unknown_info = uuid_registry.get_characteristic_info("NonExistentCharacteristic") + unknown_info = get_uuid_registry().get_characteristic_info("NonExistentCharacteristic") assert unknown_info is None, "Unknown characteristic should return None" def test_unit_conversion_mappings(self) -> None: @@ -75,7 +75,7 @@ def test_unit_conversion_mappings(self) -> None: correctly. """ # Test the conversion function directly (if accessible) - registry_instance = uuid_registry + registry_instance = get_uuid_registry() # Test common unit conversions test_mappings = [ diff --git a/tests/static_analysis/test_characteristic_registry_completeness.py b/tests/static_analysis/test_characteristic_registry_completeness.py index 7d4b3ac2..42215989 100644 --- a/tests/static_analysis/test_characteristic_registry_completeness.py +++ b/tests/static_analysis/test_characteristic_registry_completeness.py @@ -11,7 +11,7 @@ import pytest from bluetooth_sig.gatt.characteristics.registry import CharacteristicRegistry -from bluetooth_sig.gatt.uuid_registry import uuid_registry +from bluetooth_sig.gatt.uuid_registry import get_uuid_registry from bluetooth_sig.types.gatt_enums import CharacteristicName @@ -39,7 +39,7 @@ def test_all_sig_characteristics_have_enum_entries(self) -> None: enum_uuids = set() for enum_member in CharacteristicName: # Try to resolve UUID for this enum member - info = uuid_registry.get_characteristic_info(enum_member.value) + info = get_uuid_registry().get_characteristic_info(enum_member.value) if info: enum_uuids.add(info.uuid.normalized) @@ -58,7 +58,7 @@ def test_all_sig_characteristics_have_enum_entries(self) -> None: if uuid_obj.normalized not in enum_uuids: char_name = char_cls.__name__ # Try to get the name from YAML - yaml_info = uuid_registry.get_characteristic_info(str(uuid_obj)) + yaml_info = get_uuid_registry().get_characteristic_info(str(uuid_obj)) yaml_name = yaml_info.name if yaml_info else "Unknown" missing_from_enum.append( @@ -98,7 +98,7 @@ def test_characteristic_enum_names_match_yaml(self) -> None: for enum_member in CharacteristicName: # Try to resolve this enum value in YAML - info = uuid_registry.get_characteristic_info(enum_member.value) + info = get_uuid_registry().get_characteristic_info(enum_member.value) if info is None: mismatches.append( diff --git a/tests/static_analysis/test_service_registry_completeness.py b/tests/static_analysis/test_service_registry_completeness.py index 2a0cdab3..0669c86d 100644 --- a/tests/static_analysis/test_service_registry_completeness.py +++ b/tests/static_analysis/test_service_registry_completeness.py @@ -11,7 +11,7 @@ import pytest from bluetooth_sig.gatt.services.registry import GattServiceRegistry -from bluetooth_sig.gatt.uuid_registry import uuid_registry +from bluetooth_sig.gatt.uuid_registry import get_uuid_registry from bluetooth_sig.types.gatt_enums import ServiceName @@ -35,7 +35,7 @@ def test_all_sig_services_have_enum_entries(self) -> None: enum_uuids = set() for enum_member in ServiceName: # Try to resolve UUID for this enum member - info = uuid_registry.get_service_info(enum_member.value) + info = get_uuid_registry().get_service_info(enum_member.value) if info: enum_uuids.add(info.uuid.normalized) @@ -52,7 +52,7 @@ def test_all_sig_services_have_enum_entries(self) -> None: if uuid_obj.normalized not in enum_uuids: service_name = service_cls.__name__ # Try to get the name from YAML - yaml_info = uuid_registry.get_service_info(str(uuid_obj)) + yaml_info = get_uuid_registry().get_service_info(str(uuid_obj)) yaml_name = yaml_info.name if yaml_info else "Unknown" missing_from_enum.append( @@ -92,7 +92,7 @@ def test_service_enum_names_match_yaml(self) -> None: for enum_member in ServiceName: # Try to resolve this enum value in YAML - info = uuid_registry.get_service_info(enum_member.value) + info = get_uuid_registry().get_service_info(enum_member.value) if info is None: mismatches.append( diff --git a/tests/static_analysis/test_yaml_implementation_coverage.py b/tests/static_analysis/test_yaml_implementation_coverage.py index 5df37bf2..92d05f5e 100644 --- a/tests/static_analysis/test_yaml_implementation_coverage.py +++ b/tests/static_analysis/test_yaml_implementation_coverage.py @@ -16,7 +16,7 @@ from bluetooth_sig.gatt.characteristics.registry import CharacteristicRegistry from bluetooth_sig.gatt.descriptors import DescriptorRegistry from bluetooth_sig.gatt.services.registry import GattServiceRegistry -from bluetooth_sig.gatt.uuid_registry import uuid_registry +from bluetooth_sig.gatt.uuid_registry import get_uuid_registry from bluetooth_sig.types.uuid import BluetoothUUID @@ -30,7 +30,7 @@ def test_all_implemented_characteristics_exist_in_yaml(self) -> None: can never be resolved by UUID lookup, so it is invisible to the registry and will fail in batch parsing scenarios. """ - yaml_uuids = set(uuid_registry._characteristics.keys()) + yaml_uuids = set(get_uuid_registry()._characteristics.keys()) registry = CharacteristicRegistry.get_instance() impl_uuids = {u.normalized for u in registry._get_sig_classes_map()} @@ -52,7 +52,7 @@ class TestOrphanServices: def test_all_implemented_services_exist_in_yaml(self) -> None: """Every implemented service UUID must have a YAML entry.""" - yaml_uuids = set(uuid_registry._services.keys()) + yaml_uuids = set(get_uuid_registry()._services.keys()) registry = GattServiceRegistry.get_instance() impl_uuids = {u.normalized for u in registry._get_sig_classes_map()} @@ -74,7 +74,7 @@ class TestOrphanDescriptors: def test_all_implemented_descriptors_exist_in_yaml(self) -> None: """Every implemented descriptor UUID must have a YAML entry.""" - yaml_uuids = set(uuid_registry._descriptors.keys()) + yaml_uuids = set(get_uuid_registry()._descriptors.keys()) impl_uuids = {BluetoothUUID(uuid_str).normalized for uuid_str in DescriptorRegistry._registry} orphans = impl_uuids - yaml_uuids diff --git a/tests/test_browse_groups_registry.py b/tests/test_browse_groups_registry.py index 2d9446fa..7522390f 100644 --- a/tests/test_browse_groups_registry.py +++ b/tests/test_browse_groups_registry.py @@ -4,7 +4,7 @@ import pytest -from bluetooth_sig.registry.uuids.browse_groups import BrowseGroupsRegistry +from bluetooth_sig.registry.uuids.browse_groups import BrowseGroupsRegistry, get_browse_groups_registry from bluetooth_sig.types.registry.browse_group_identifiers import BrowseGroupInfo from bluetooth_sig.types.uuid import BluetoothUUID @@ -22,7 +22,7 @@ def test_registry_initialization(self, browse_groups_registry: BrowseGroupsRegis """Test that the registry initializes properly.""" assert isinstance(browse_groups_registry, BrowseGroupsRegistry) # Should have loaded some browse groups if YAML exists - browse_groups = browse_groups_registry.get_all_browse_groups() + browse_groups = get_browse_groups_registry().get_all_browse_groups() assert isinstance(browse_groups, list) # If submodule is initialized, should have browse groups if browse_groups: @@ -31,7 +31,7 @@ def test_registry_initialization(self, browse_groups_registry: BrowseGroupsRegis def test_get_browse_group_info(self, browse_groups_registry: BrowseGroupsRegistry) -> None: """Test lookup by UUID string.""" # Test with a known browse group UUID (PublicBrowseRoot) - info = browse_groups_registry.get_browse_group_info("0x1002") + info = get_browse_groups_registry().get_browse_group_info("0x1002") if info: # Only if YAML loaded assert isinstance(info, BrowseGroupInfo) assert info.name == "PublicBrowseRoot" @@ -40,65 +40,65 @@ def test_get_browse_group_info(self, browse_groups_registry: BrowseGroupsRegistr def test_get_browse_group_info_by_name(self, browse_groups_registry: BrowseGroupsRegistry) -> None: """Test lookup by browse group name.""" # Test with known browse group name (PublicBrowseRoot) - info = browse_groups_registry.get_browse_group_info_by_name("PublicBrowseRoot") + info = get_browse_groups_registry().get_browse_group_info_by_name("PublicBrowseRoot") if info: # Only if YAML loaded assert isinstance(info, BrowseGroupInfo) assert info.name == "PublicBrowseRoot" assert info.uuid.short_form.upper() == "1002" # Test case insensitive - info_lower = browse_groups_registry.get_browse_group_info_by_name("publicbrowseroot") + info_lower = get_browse_groups_registry().get_browse_group_info_by_name("publicbrowseroot") assert info_lower == info # Test not found - info_none = browse_groups_registry.get_browse_group_info_by_name("Nonexistent Browse Group") + info_none = get_browse_groups_registry().get_browse_group_info_by_name("Nonexistent Browse Group") assert info_none is None def test_get_browse_group_info_by_id(self, browse_groups_registry: BrowseGroupsRegistry) -> None: """Test lookup by browse group ID.""" # Test with known browse group ID - info = browse_groups_registry.get_browse_group_info_by_id("org.bluetooth.browse_group.public_browse_root") + info = get_browse_groups_registry().get_browse_group_info_by_id("org.bluetooth.browse_group.public_browse_root") if info: # Only if YAML loaded assert isinstance(info, BrowseGroupInfo) assert info.name == "PublicBrowseRoot" assert info.uuid.short_form.upper() == "1002" # Test not found - info_none = browse_groups_registry.get_browse_group_info_by_id("org.bluetooth.browse_group.nonexistent") + info_none = get_browse_groups_registry().get_browse_group_info_by_id("org.bluetooth.browse_group.nonexistent") assert info_none is None def test_get_browse_group_info_by_bluetooth_uuid(self, browse_groups_registry: BrowseGroupsRegistry) -> None: """Test lookup by BluetoothUUID object.""" # Create a BluetoothUUID for a known browse group bt_uuid = BluetoothUUID("1002") - info = browse_groups_registry.get_browse_group_info(bt_uuid) + info = get_browse_groups_registry().get_browse_group_info(bt_uuid) if info: # Only if YAML loaded assert isinstance(info, BrowseGroupInfo) assert info.name == "PublicBrowseRoot" def test_get_browse_group_info_not_found(self, browse_groups_registry: BrowseGroupsRegistry) -> None: """Test lookup for non-existent browse group.""" - info = browse_groups_registry.get_browse_group_info("nonexistent") + info = get_browse_groups_registry().get_browse_group_info("nonexistent") assert info is None - info = browse_groups_registry.get_browse_group_info("0x0000") # Not a browse group UUID + info = get_browse_groups_registry().get_browse_group_info("0x0000") # Not a browse group UUID assert info is None def test_is_browse_group_uuid(self, browse_groups_registry: BrowseGroupsRegistry) -> None: """Test browse group UUID validation.""" # Known browse group UUID - has_groups = bool(browse_groups_registry.get_all_browse_groups()) - assert browse_groups_registry.is_browse_group_uuid("0x1002") or not has_groups + has_groups = bool(get_browse_groups_registry().get_all_browse_groups()) + assert get_browse_groups_registry().is_browse_group_uuid("0x1002") or not has_groups # Non-browse group UUID - assert not browse_groups_registry.is_browse_group_uuid("0x0000") + assert not get_browse_groups_registry().is_browse_group_uuid("0x0000") # Invalid UUID - assert not browse_groups_registry.is_browse_group_uuid("invalid") + assert not get_browse_groups_registry().is_browse_group_uuid("invalid") def test_get_all_browse_groups(self, browse_groups_registry: BrowseGroupsRegistry) -> None: """Test getting all browse groups.""" - browse_groups = browse_groups_registry.get_all_browse_groups() + browse_groups = get_browse_groups_registry().get_all_browse_groups() assert isinstance(browse_groups, list) if browse_groups: @@ -113,7 +113,7 @@ def test_get_all_browse_groups(self, browse_groups_registry: BrowseGroupsRegistr def test_browse_group_info_structure(self, browse_groups_registry: BrowseGroupsRegistry) -> None: """Test BrowseGroupInfo dataclass structure.""" - browse_groups = browse_groups_registry.get_all_browse_groups() + browse_groups = get_browse_groups_registry().get_all_browse_groups() if browse_groups: browse_group = browse_groups[0] assert hasattr(browse_group, "uuid") @@ -127,7 +127,7 @@ def test_uuid_formats(self, browse_groups_registry: BrowseGroupsRegistry) -> Non """Test various UUID input formats.""" formats: list[str | BluetoothUUID] = ["1002", "0x1002", "0X1002", BluetoothUUID("1002")] for fmt in formats: - info = browse_groups_registry.get_browse_group_info(fmt) - if browse_groups_registry.is_browse_group_uuid("1002"): + info = get_browse_groups_registry().get_browse_group_info(fmt) + if get_browse_groups_registry().is_browse_group_uuid("1002"): assert info is not None assert info.name == "PublicBrowseRoot" diff --git a/tests/test_declarations_registry.py b/tests/test_declarations_registry.py index 9d6cb342..dc2769dc 100644 --- a/tests/test_declarations_registry.py +++ b/tests/test_declarations_registry.py @@ -4,7 +4,7 @@ import pytest -from bluetooth_sig.registry.uuids.declarations import DeclarationsRegistry +from bluetooth_sig.registry.uuids.declarations import DeclarationsRegistry, get_declarations_registry from bluetooth_sig.types.registry.declarations import DeclarationInfo from bluetooth_sig.types.uuid import BluetoothUUID @@ -22,7 +22,7 @@ def test_registry_initialization(self, declarations_registry: DeclarationsRegist """Test that the registry initializes properly.""" assert isinstance(declarations_registry, DeclarationsRegistry) # Should have loaded some declarations if YAML exists - declarations = declarations_registry.get_all_declarations() + declarations = get_declarations_registry().get_all_declarations() assert isinstance(declarations, list) # If submodule is initialized, should have declarations if declarations: @@ -31,7 +31,7 @@ def test_registry_initialization(self, declarations_registry: DeclarationsRegist def test_get_declaration_info(self, declarations_registry: DeclarationsRegistry) -> None: """Test lookup by UUID string.""" # Test with a known declaration UUID (Primary Service) - info = declarations_registry.get_declaration_info("0x2800") + info = get_declarations_registry().get_declaration_info("0x2800") if info: # Only if YAML loaded assert isinstance(info, DeclarationInfo) assert info.name == "Primary Service" @@ -40,24 +40,24 @@ def test_get_declaration_info(self, declarations_registry: DeclarationsRegistry) def test_get_declaration_info_by_name(self, declarations_registry: DeclarationsRegistry) -> None: """Test lookup by declaration name.""" # Test with known declaration name (Primary Service) - info = declarations_registry.get_declaration_info_by_name("Primary Service") + info = get_declarations_registry().get_declaration_info_by_name("Primary Service") if info: # Only if YAML loaded assert isinstance(info, DeclarationInfo) assert info.name == "Primary Service" assert info.uuid.short_form.upper() == "2800" # Test case insensitive - info_lower = declarations_registry.get_declaration_info_by_name("primary service") + info_lower = get_declarations_registry().get_declaration_info_by_name("primary service") assert info_lower == info # Test not found - info_none = declarations_registry.get_declaration_info_by_name("Nonexistent Declaration") + info_none = get_declarations_registry().get_declaration_info_by_name("Nonexistent Declaration") assert info_none is None def test_get_declaration_info_by_id(self, declarations_registry: DeclarationsRegistry) -> None: """Test lookup by declaration ID.""" # Test with known declaration ID - info = declarations_registry.get_declaration_info_by_id( + info = get_declarations_registry().get_declaration_info_by_id( "org.bluetooth.attribute.gatt.primary_service_declaration" ) if info: # Only if YAML loaded @@ -66,41 +66,41 @@ def test_get_declaration_info_by_id(self, declarations_registry: DeclarationsReg assert info.uuid.short_form.upper() == "2800" # Test not found - info_none = declarations_registry.get_declaration_info_by_id("org.bluetooth.attribute.gatt.nonexistent") + info_none = get_declarations_registry().get_declaration_info_by_id("org.bluetooth.attribute.gatt.nonexistent") assert info_none is None def test_get_declaration_info_by_bluetooth_uuid(self, declarations_registry: DeclarationsRegistry) -> None: """Test lookup by BluetoothUUID object.""" # Create a BluetoothUUID for a known declaration bt_uuid = BluetoothUUID("2800") - info = declarations_registry.get_declaration_info(bt_uuid) + info = get_declarations_registry().get_declaration_info(bt_uuid) if info: # Only if YAML loaded assert isinstance(info, DeclarationInfo) assert info.name == "Primary Service" def test_get_declaration_info_not_found(self, declarations_registry: DeclarationsRegistry) -> None: """Test lookup for non-existent declaration.""" - info = declarations_registry.get_declaration_info("nonexistent") + info = get_declarations_registry().get_declaration_info("nonexistent") assert info is None - info = declarations_registry.get_declaration_info("0x0000") # Not a declaration UUID + info = get_declarations_registry().get_declaration_info("0x0000") # Not a declaration UUID assert info is None def test_is_declaration_uuid(self, declarations_registry: DeclarationsRegistry) -> None: """Test declaration UUID validation.""" # Known declaration UUID - has_declarations = bool(declarations_registry.get_all_declarations()) - assert declarations_registry.is_declaration_uuid("0x2800") or not has_declarations + has_declarations = bool(get_declarations_registry().get_all_declarations()) + assert get_declarations_registry().is_declaration_uuid("0x2800") or not has_declarations # Non-declaration UUID - assert not declarations_registry.is_declaration_uuid("0x0000") + assert not get_declarations_registry().is_declaration_uuid("0x0000") # Invalid UUID - assert not declarations_registry.is_declaration_uuid("invalid") + assert not get_declarations_registry().is_declaration_uuid("invalid") def test_get_all_declarations(self, declarations_registry: DeclarationsRegistry) -> None: """Test getting all declarations.""" - declarations = declarations_registry.get_all_declarations() + declarations = get_declarations_registry().get_all_declarations() assert isinstance(declarations, list) if declarations: @@ -115,7 +115,7 @@ def test_get_all_declarations(self, declarations_registry: DeclarationsRegistry) def test_declaration_info_structure(self, declarations_registry: DeclarationsRegistry) -> None: """Test DeclarationInfo dataclass structure.""" - declarations = declarations_registry.get_all_declarations() + declarations = get_declarations_registry().get_all_declarations() if declarations: declaration = declarations[0] assert hasattr(declaration, "uuid") @@ -129,33 +129,33 @@ def test_uuid_formats(self, declarations_registry: DeclarationsRegistry) -> None """Test various UUID input formats.""" formats: list[str | BluetoothUUID] = ["2800", "0x2800", "0X2800", BluetoothUUID("2800")] for fmt in formats: - info = declarations_registry.get_declaration_info(fmt) - if declarations_registry.is_declaration_uuid("2800"): + info = get_declarations_registry().get_declaration_info(fmt) + if get_declarations_registry().is_declaration_uuid("2800"): assert info is not None assert info.name == "Primary Service" def test_known_declarations(self, declarations_registry: DeclarationsRegistry) -> None: """Test lookup for known GATT declarations.""" # Test Primary Service - primary_service = declarations_registry.get_declaration_info("0x2800") + primary_service = get_declarations_registry().get_declaration_info("0x2800") if primary_service: assert primary_service.name == "Primary Service" assert primary_service.id == "org.bluetooth.attribute.gatt.primary_service_declaration" # Test Secondary Service - secondary_service = declarations_registry.get_declaration_info("0x2801") + secondary_service = get_declarations_registry().get_declaration_info("0x2801") if secondary_service: assert secondary_service.name == "Secondary Service" assert secondary_service.id == "org.bluetooth.attribute.gatt.secondary_service_declaration" # Test Include - include = declarations_registry.get_declaration_info("0x2802") + include = get_declarations_registry().get_declaration_info("0x2802") if include: assert include.name == "Include" assert include.id == "org.bluetooth.attribute.gatt.include_declaration" # Test Characteristic - characteristic = declarations_registry.get_declaration_info("0x2803") + characteristic = get_declarations_registry().get_declaration_info("0x2803") if characteristic: assert characteristic.name == "Characteristic" assert characteristic.id == "org.bluetooth.attribute.gatt.characteristic_declaration" diff --git a/tests/test_members_registry.py b/tests/test_members_registry.py index 5bcdcd65..21798b34 100644 --- a/tests/test_members_registry.py +++ b/tests/test_members_registry.py @@ -4,7 +4,7 @@ import pytest -from bluetooth_sig.registry.uuids.members import MembersRegistry +from bluetooth_sig.registry.uuids.members import MembersRegistry, get_members_registry from bluetooth_sig.types.registry.member_uuids import MemberInfo from bluetooth_sig.types.uuid import BluetoothUUID @@ -22,7 +22,7 @@ def test_registry_initialization(self, members_registry: MembersRegistry) -> Non """Test that the registry initializes properly.""" assert isinstance(members_registry, MembersRegistry) # Should have loaded some members if YAML exists - members = members_registry.get_all_members() + members = get_members_registry().get_all_members() assert isinstance(members, list) # If submodule is initialized, should have members if members: @@ -31,7 +31,7 @@ def test_registry_initialization(self, members_registry: MembersRegistry) -> Non def test_get_member_info_by_uuid(self, members_registry: MembersRegistry) -> None: """Test lookup by UUID string.""" # Test with a known member UUID (GN Netcom) - name = members_registry.get_member_name("0xFEFF") + name = get_members_registry().get_member_name("0xFEFF") if name: # Only if YAML loaded assert isinstance(name, str) assert name == "GN Netcom" @@ -39,51 +39,51 @@ def test_get_member_info_by_uuid(self, members_registry: MembersRegistry) -> Non def test_get_member_info_by_name(self, members_registry: MembersRegistry) -> None: """Test lookup by company name.""" # Test with known member name (GN Netcom) - info = members_registry.get_member_info_by_name("GN Netcom") + info = get_members_registry().get_member_info_by_name("GN Netcom") if info: # Only if YAML loaded assert isinstance(info, MemberInfo) assert info.name == "GN Netcom" assert info.uuid.short_form.upper() == "FEFF" # Test case insensitive - info_lower = members_registry.get_member_info_by_name("gn netcom") + info_lower = get_members_registry().get_member_info_by_name("gn netcom") assert info_lower == info # Test not found - info_none = members_registry.get_member_info_by_name("Nonexistent Company") + info_none = get_members_registry().get_member_info_by_name("Nonexistent Company") assert info_none is None def test_get_member_info_by_bluetooth_uuid(self, members_registry: MembersRegistry) -> None: """Test lookup by BluetoothUUID object.""" # Create a BluetoothUUID for a known member bt_uuid = BluetoothUUID("FEFF") - name = members_registry.get_member_name(bt_uuid) + name = get_members_registry().get_member_name(bt_uuid) if name: # Only if YAML loaded assert isinstance(name, str) assert name == "GN Netcom" def test_get_member_info_not_found(self, members_registry: MembersRegistry) -> None: """Test lookup for non-existent member.""" - name = members_registry.get_member_name("nonexistent") + name = get_members_registry().get_member_name("nonexistent") assert name is None - name = members_registry.get_member_name("0x0000") # Not a member UUID + name = get_members_registry().get_member_name("0x0000") # Not a member UUID assert name is None def test_is_member_uuid(self, members_registry: MembersRegistry) -> None: """Test member UUID validation.""" # Known member UUID - assert members_registry.is_member_uuid("0xFEFF") or not members_registry.get_all_members() + assert get_members_registry().is_member_uuid("0xFEFF") or not get_members_registry().get_all_members() # Non-member UUID - assert not members_registry.is_member_uuid("0x0000") + assert not get_members_registry().is_member_uuid("0x0000") # Invalid UUID - assert not members_registry.is_member_uuid("invalid") + assert not get_members_registry().is_member_uuid("invalid") def test_get_all_members(self, members_registry: MembersRegistry) -> None: """Test getting all members.""" - members = members_registry.get_all_members() + members = get_members_registry().get_all_members() assert isinstance(members, list) if members: @@ -97,7 +97,7 @@ def test_get_all_members(self, members_registry: MembersRegistry) -> None: def test_member_info_structure(self, members_registry: MembersRegistry) -> None: """Test MemberInfo dataclass structure.""" - members = members_registry.get_all_members() + members = get_members_registry().get_all_members() if members: member = members[0] assert hasattr(member, "uuid") @@ -109,6 +109,6 @@ def test_uuid_formats(self, members_registry: MembersRegistry) -> None: """Test various UUID input formats.""" formats: list[str | BluetoothUUID] = ["FEFF", "0xFEFF", "0XFEFF", BluetoothUUID("FEFF")] for fmt in formats: - name = members_registry.get_member_name(fmt) - if members_registry.is_member_uuid("FEFF"): + name = get_members_registry().get_member_name(fmt) + if get_members_registry().is_member_uuid("FEFF"): assert name == "GN Netcom" diff --git a/tests/test_mesh_profiles_registry.py b/tests/test_mesh_profiles_registry.py index a04ec3e4..b283c331 100644 --- a/tests/test_mesh_profiles_registry.py +++ b/tests/test_mesh_profiles_registry.py @@ -4,7 +4,7 @@ import pytest -from bluetooth_sig.registry.uuids.mesh_profiles import MeshProfilesRegistry +from bluetooth_sig.registry.uuids.mesh_profiles import MeshProfilesRegistry, get_mesh_profiles_registry from bluetooth_sig.types.registry.mesh_profile_uuids import MeshProfileInfo from bluetooth_sig.types.uuid import BluetoothUUID @@ -22,7 +22,7 @@ def test_registry_initialization(self, mesh_profiles_registry: MeshProfilesRegis """Test that the registry initializes properly.""" assert isinstance(mesh_profiles_registry, MeshProfilesRegistry) # Should have loaded some mesh profiles if YAML exists - mesh_profiles = mesh_profiles_registry.get_all_mesh_profiles() + mesh_profiles = get_mesh_profiles_registry().get_all_mesh_profiles() assert isinstance(mesh_profiles, list) # If submodule is initialized, should have mesh profiles if mesh_profiles: @@ -31,7 +31,7 @@ def test_registry_initialization(self, mesh_profiles_registry: MeshProfilesRegis def test_get_mesh_profile_info(self, mesh_profiles_registry: MeshProfilesRegistry) -> None: """Test lookup by UUID string.""" # Test with a known mesh profile UUID (Generic OnOff Server) - info = mesh_profiles_registry.get_mesh_profile_info("0x1000") + info = get_mesh_profiles_registry().get_mesh_profile_info("0x1000") if info: # Only if YAML loaded assert isinstance(info, MeshProfileInfo) assert info.name == "Generic OnOff Server" @@ -39,52 +39,52 @@ def test_get_mesh_profile_info(self, mesh_profiles_registry: MeshProfilesRegistr def test_get_mesh_profile_info_by_name(self, mesh_profiles_registry: MeshProfilesRegistry) -> None: """Test lookup by mesh profile name.""" # Test with known mesh profile name (Generic OnOff Server) - info = mesh_profiles_registry.get_mesh_profile_info_by_name("Generic OnOff Server") + info = get_mesh_profiles_registry().get_mesh_profile_info_by_name("Generic OnOff Server") if info: # Only if YAML loaded assert isinstance(info, MeshProfileInfo) assert info.name == "Generic OnOff Server" assert info.uuid.short_form.upper() == "1000" # Test case insensitive - info_lower = mesh_profiles_registry.get_mesh_profile_info_by_name("generic onoff server") + info_lower = get_mesh_profiles_registry().get_mesh_profile_info_by_name("generic onoff server") assert info_lower == info # Test not found - info_none = mesh_profiles_registry.get_mesh_profile_info_by_name("Nonexistent Mesh Profile") + info_none = get_mesh_profiles_registry().get_mesh_profile_info_by_name("Nonexistent Mesh Profile") assert info_none is None def test_get_mesh_profile_info_by_bluetooth_uuid(self, mesh_profiles_registry: MeshProfilesRegistry) -> None: """Test lookup by BluetoothUUID object.""" # Create a BluetoothUUID for a known mesh profile bt_uuid = BluetoothUUID("1000") - info = mesh_profiles_registry.get_mesh_profile_info(bt_uuid) + info = get_mesh_profiles_registry().get_mesh_profile_info(bt_uuid) if info: # Only if YAML loaded assert isinstance(info, MeshProfileInfo) assert info.name == "Generic OnOff Server" def test_get_mesh_profile_info_not_found(self, mesh_profiles_registry: MeshProfilesRegistry) -> None: """Test lookup for non-existent mesh profile.""" - info = mesh_profiles_registry.get_mesh_profile_info("nonexistent") + info = get_mesh_profiles_registry().get_mesh_profile_info("nonexistent") assert info is None - info = mesh_profiles_registry.get_mesh_profile_info("0x0000") # Not a mesh profile UUID + info = get_mesh_profiles_registry().get_mesh_profile_info("0x0000") # Not a mesh profile UUID assert info is None def test_is_mesh_profile_uuid(self, mesh_profiles_registry: MeshProfilesRegistry) -> None: """Test mesh profile UUID validation.""" # Known mesh profile UUID - has_profiles = bool(mesh_profiles_registry.get_all_mesh_profiles()) - assert mesh_profiles_registry.is_mesh_profile_uuid("0x1000") or not has_profiles + has_profiles = bool(get_mesh_profiles_registry().get_all_mesh_profiles()) + assert get_mesh_profiles_registry().is_mesh_profile_uuid("0x1000") or not has_profiles # Non-mesh profile UUID - assert not mesh_profiles_registry.is_mesh_profile_uuid("0x0000") + assert not get_mesh_profiles_registry().is_mesh_profile_uuid("0x0000") # Invalid UUID - assert not mesh_profiles_registry.is_mesh_profile_uuid("invalid") + assert not get_mesh_profiles_registry().is_mesh_profile_uuid("invalid") def test_get_all_mesh_profiles(self, mesh_profiles_registry: MeshProfilesRegistry) -> None: """Test getting all mesh profiles.""" - mesh_profiles = mesh_profiles_registry.get_all_mesh_profiles() + mesh_profiles = get_mesh_profiles_registry().get_all_mesh_profiles() assert isinstance(mesh_profiles, list) if mesh_profiles: @@ -98,7 +98,7 @@ def test_get_all_mesh_profiles(self, mesh_profiles_registry: MeshProfilesRegistr def test_mesh_profile_info_structure(self, mesh_profiles_registry: MeshProfilesRegistry) -> None: """Test MeshProfileInfo dataclass structure.""" - mesh_profiles = mesh_profiles_registry.get_all_mesh_profiles() + mesh_profiles = get_mesh_profiles_registry().get_all_mesh_profiles() if mesh_profiles: mesh_profile = mesh_profiles[0] assert hasattr(mesh_profile, "uuid") @@ -111,7 +111,7 @@ def test_uuid_formats(self, mesh_profiles_registry: MeshProfilesRegistry) -> Non """Test various UUID input formats.""" formats: list[str | BluetoothUUID] = ["1000", "0x1000", "0X1000", BluetoothUUID("1000")] for fmt in formats: - info = mesh_profiles_registry.get_mesh_profile_info(fmt) - if mesh_profiles_registry.is_mesh_profile_uuid("1000"): + info = get_mesh_profiles_registry().get_mesh_profile_info(fmt) + if get_mesh_profiles_registry().is_mesh_profile_uuid("1000"): assert info is not None assert info.name == "Generic OnOff Server" diff --git a/tests/test_object_types_registry.py b/tests/test_object_types_registry.py index 0af9c441..9d0f33f6 100644 --- a/tests/test_object_types_registry.py +++ b/tests/test_object_types_registry.py @@ -4,7 +4,7 @@ import pytest -from bluetooth_sig.registry.uuids.object_types import ObjectTypesRegistry +from bluetooth_sig.registry.uuids.object_types import ObjectTypesRegistry, get_object_types_registry from bluetooth_sig.types.registry.object_types import ObjectTypeInfo from bluetooth_sig.types.uuid import BluetoothUUID @@ -22,7 +22,7 @@ def test_registry_initialization(self, object_types_registry: ObjectTypesRegistr """Test that the registry initializes properly.""" assert isinstance(object_types_registry, ObjectTypesRegistry) # Should have loaded some object types if YAML exists - object_types = object_types_registry.get_all_object_types() + object_types = get_object_types_registry().get_all_object_types() assert isinstance(object_types, list) # If submodule is initialized, should have object types if object_types: @@ -31,7 +31,7 @@ def test_registry_initialization(self, object_types_registry: ObjectTypesRegistr def test_get_object_type_info(self, object_types_registry: ObjectTypesRegistry) -> None: """Test lookup by UUID string.""" # Test with a known object type UUID (Unspecified) - info = object_types_registry.get_object_type_info("0x2ACA") + info = get_object_types_registry().get_object_type_info("0x2ACA") if info: # Only if YAML loaded assert isinstance(info, ObjectTypeInfo) assert info.name == "Unspecified" @@ -40,64 +40,67 @@ def test_get_object_type_info(self, object_types_registry: ObjectTypesRegistry) def test_get_object_type_info_by_name(self, object_types_registry: ObjectTypesRegistry) -> None: """Test lookup by object type name.""" # Test with known object type name (Unspecified) - info = object_types_registry.get_object_type_info_by_name("Unspecified") + info = get_object_types_registry().get_object_type_info_by_name("Unspecified") if info: # Only if YAML loaded assert isinstance(info, ObjectTypeInfo) assert info.name == "Unspecified" assert info.uuid.short_form.upper() == "2ACA" # Test case insensitive - info_lower = object_types_registry.get_object_type_info_by_name("unspecified") + info_lower = get_object_types_registry().get_object_type_info_by_name("unspecified") assert info_lower == info # Test not found - info_none = object_types_registry.get_object_type_info_by_name("Nonexistent Object Type") + info_none = get_object_types_registry().get_object_type_info_by_name("Nonexistent Object Type") assert info_none is None def test_get_object_type_info_by_id(self, object_types_registry: ObjectTypesRegistry) -> None: """Test lookup by object type ID.""" # Test with known object type ID - info = object_types_registry.get_object_type_info_by_id("org.bluetooth.object.unspecified") + info = get_object_types_registry().get_object_type_info_by_id("org.bluetooth.object.unspecified") if info: # Only if YAML loaded assert isinstance(info, ObjectTypeInfo) assert info.name == "Unspecified" assert info.uuid.short_form.upper() == "2ACA" # Test not found - info_none = object_types_registry.get_object_type_info_by_id("org.bluetooth.object.nonexistent") + info_none = get_object_types_registry().get_object_type_info_by_id("org.bluetooth.object.nonexistent") assert info_none is None def test_get_object_type_info_by_bluetooth_uuid(self, object_types_registry: ObjectTypesRegistry) -> None: """Test lookup by BluetoothUUID object.""" # Create a BluetoothUUID for a known object type bt_uuid = BluetoothUUID("2ACA") - info = object_types_registry.get_object_type_info(bt_uuid) + info = get_object_types_registry().get_object_type_info(bt_uuid) if info: # Only if YAML loaded assert isinstance(info, ObjectTypeInfo) assert info.name == "Unspecified" def test_get_object_type_info_not_found(self, object_types_registry: ObjectTypesRegistry) -> None: """Test lookup for non-existent object type.""" - info = object_types_registry.get_object_type_info("nonexistent") + info = get_object_types_registry().get_object_type_info("nonexistent") assert info is None - info = object_types_registry.get_object_type_info("0x0000") # Not an object type UUID + info = get_object_types_registry().get_object_type_info("0x0000") # Not an object type UUID assert info is None def test_is_object_type_uuid(self, object_types_registry: ObjectTypesRegistry) -> None: """Test object type UUID validation.""" # Known object type UUID - assert object_types_registry.is_object_type_uuid("0x2ACA") or not object_types_registry.get_all_object_types() + assert ( + get_object_types_registry().is_object_type_uuid("0x2ACA") + or not get_object_types_registry().get_all_object_types() + ) # Non-object type UUID - assert not object_types_registry.is_object_type_uuid("0x0000") + assert not get_object_types_registry().is_object_type_uuid("0x0000") # Invalid UUID - assert not object_types_registry.is_object_type_uuid("invalid") + assert not get_object_types_registry().is_object_type_uuid("invalid") def test_get_all_object_types(self, object_types_registry: ObjectTypesRegistry) -> None: """Test getting all object types.""" - object_types = object_types_registry.get_all_object_types() + object_types = get_object_types_registry().get_all_object_types() assert isinstance(object_types, list) if object_types: @@ -112,7 +115,7 @@ def test_get_all_object_types(self, object_types_registry: ObjectTypesRegistry) def test_object_type_info_structure(self, object_types_registry: ObjectTypesRegistry) -> None: """Test ObjectTypeInfo dataclass structure.""" - object_types = object_types_registry.get_all_object_types() + object_types = get_object_types_registry().get_all_object_types() if object_types: object_type = object_types[0] assert hasattr(object_type, "uuid") @@ -126,7 +129,7 @@ def test_uuid_formats(self, object_types_registry: ObjectTypesRegistry) -> None: """Test various UUID input formats.""" formats: list[str | BluetoothUUID] = ["2ACA", "0x2ACA", "0X2ACA", BluetoothUUID("2ACA")] for fmt in formats: - info = object_types_registry.get_object_type_info(fmt) - if object_types_registry.is_object_type_uuid("2ACA"): + info = get_object_types_registry().get_object_type_info(fmt) + if get_object_types_registry().is_object_type_uuid("2ACA"): assert info is not None assert info.name == "Unspecified" diff --git a/tests/test_sdo_uuids_registry.py b/tests/test_sdo_uuids_registry.py index a756a754..46f2c9fa 100644 --- a/tests/test_sdo_uuids_registry.py +++ b/tests/test_sdo_uuids_registry.py @@ -4,7 +4,7 @@ import pytest -from bluetooth_sig.registry.uuids.sdo_uuids import SdoUuidsRegistry +from bluetooth_sig.registry.uuids.sdo_uuids import SdoUuidsRegistry, get_sdo_uuids_registry from bluetooth_sig.types.registry.sdo_uuids import SdoUuidInfo as SdoInfo from bluetooth_sig.types.uuid import BluetoothUUID @@ -22,7 +22,7 @@ def test_registry_initialization(self, sdo_uuids_registry: SdoUuidsRegistry) -> """Test that the registry initializes properly.""" assert isinstance(sdo_uuids_registry, SdoUuidsRegistry) # Should have loaded some SDO UUIDs if YAML exists - sdo_uuids = sdo_uuids_registry.get_all_sdo_uuids() + sdo_uuids = get_sdo_uuids_registry().get_all_sdo_uuids() assert isinstance(sdo_uuids, list) # If submodule is initialized, should have SDO UUIDs if sdo_uuids: @@ -31,7 +31,7 @@ def test_registry_initialization(self, sdo_uuids_registry: SdoUuidsRegistry) -> def test_get_sdo_info(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: """Test lookup by UUID string.""" # Test with a known SDO UUID (Wireless Power Transfer) - info = sdo_uuids_registry.get_sdo_info("0xFFFE") + info = get_sdo_uuids_registry().get_sdo_info("0xFFFE") if info: # Only if YAML loaded assert isinstance(info, SdoInfo) assert info.name == "Wireless Power Transfer" @@ -39,65 +39,65 @@ def test_get_sdo_info(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: def test_get_sdo_info_by_name(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: """Test lookup by SDO name.""" # Test with known SDO name (Wireless Power Transfer) - info = sdo_uuids_registry.get_sdo_info_by_name("Wireless Power Transfer") + info = get_sdo_uuids_registry().get_sdo_info_by_name("Wireless Power Transfer") if info: # Only if YAML loaded assert isinstance(info, SdoInfo) assert info.name == "Wireless Power Transfer" assert info.uuid.short_form.upper() == "FFFE" # Test case insensitive - info_lower = sdo_uuids_registry.get_sdo_info_by_name("wireless power transfer") + info_lower = get_sdo_uuids_registry().get_sdo_info_by_name("wireless power transfer") assert info_lower == info # Test not found - info_none = sdo_uuids_registry.get_sdo_info_by_name("Nonexistent SDO") + info_none = get_sdo_uuids_registry().get_sdo_info_by_name("Nonexistent SDO") assert info_none is None def test_get_sdo_info_by_id(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: """Test lookup by SDO ID.""" # Test with known SDO ID - info = sdo_uuids_registry.get_sdo_info_by_id("org.bluetooth.sdo.wireless_power_transfer") + info = get_sdo_uuids_registry().get_sdo_info_by_id("org.bluetooth.sdo.wireless_power_transfer") if info: # Only if YAML loaded assert isinstance(info, SdoInfo) assert info.name == "Wireless Power Transfer" assert info.uuid.short_form.upper() == "FFFE" # Test not found - info_none = sdo_uuids_registry.get_sdo_info_by_id("org.bluetooth.sdo.nonexistent") + info_none = get_sdo_uuids_registry().get_sdo_info_by_id("org.bluetooth.sdo.nonexistent") assert info_none is None def test_get_sdo_info_by_bluetooth_uuid(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: """Test lookup by BluetoothUUID object.""" # Create a BluetoothUUID for a known SDO bt_uuid = BluetoothUUID("FFFE") - info = sdo_uuids_registry.get_sdo_info(bt_uuid) + info = get_sdo_uuids_registry().get_sdo_info(bt_uuid) if info: # Only if YAML loaded assert isinstance(info, SdoInfo) assert info.name == "Wireless Power Transfer" def test_get_sdo_info_not_found(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: """Test lookup for non-existent SDO.""" - info = sdo_uuids_registry.get_sdo_info("nonexistent") + info = get_sdo_uuids_registry().get_sdo_info("nonexistent") assert info is None - info = sdo_uuids_registry.get_sdo_info("0x0000") # Not an SDO UUID + info = get_sdo_uuids_registry().get_sdo_info("0x0000") # Not an SDO UUID assert info is None def test_is_sdo_uuid(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: """Test SDO UUID validation.""" # Known SDO UUID - has_sdos = bool(sdo_uuids_registry.get_all_sdo_uuids()) - assert sdo_uuids_registry.is_sdo_uuid("0xFFFE") or not has_sdos + has_sdos = bool(get_sdo_uuids_registry().get_all_sdo_uuids()) + assert get_sdo_uuids_registry().is_sdo_uuid("0xFFFE") or not has_sdos # Non-SDO UUID - assert not sdo_uuids_registry.is_sdo_uuid("0x0000") + assert not get_sdo_uuids_registry().is_sdo_uuid("0x0000") # Invalid UUID - assert not sdo_uuids_registry.is_sdo_uuid("invalid") + assert not get_sdo_uuids_registry().is_sdo_uuid("invalid") def test_get_all_sdo_uuids(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: """Test getting all SDO UUIDs.""" - sdo_uuids = sdo_uuids_registry.get_all_sdo_uuids() + sdo_uuids = get_sdo_uuids_registry().get_all_sdo_uuids() assert isinstance(sdo_uuids, list) if sdo_uuids: @@ -111,7 +111,7 @@ def test_get_all_sdo_uuids(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: def test_sdo_info_structure(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: """Test SdoInfo dataclass structure.""" - sdo_uuids = sdo_uuids_registry.get_all_sdo_uuids() + sdo_uuids = get_sdo_uuids_registry().get_all_sdo_uuids() if sdo_uuids: sdo = sdo_uuids[0] assert hasattr(sdo, "uuid") @@ -124,8 +124,8 @@ def test_uuid_formats(self, sdo_uuids_registry: SdoUuidsRegistry) -> None: """Test various UUID input formats.""" formats: list[str | BluetoothUUID] = ["FFFE", "0xFFFE", "0XFFFE", BluetoothUUID("FFFE")] for fmt in formats: - info = sdo_uuids_registry.get_sdo_info(fmt) - if sdo_uuids_registry.is_sdo_uuid("FFFE"): + info = get_sdo_uuids_registry().get_sdo_info(fmt) + if get_sdo_uuids_registry().is_sdo_uuid("FFFE"): assert info is not None assert info.name == "Wireless Power Transfer" @@ -141,5 +141,5 @@ def test_normalize_name_for_id(self, sdo_uuids_registry: SdoUuidsRegistry) -> No ] for name, expected_suffix in test_cases: - normalized = sdo_uuids_registry._normalize_name_for_id(name) + normalized = get_sdo_uuids_registry()._normalize_name_for_id(name) assert normalized == expected_suffix diff --git a/tests/test_service_classes_registry.py b/tests/test_service_classes_registry.py index 42e7a26a..93828499 100644 --- a/tests/test_service_classes_registry.py +++ b/tests/test_service_classes_registry.py @@ -4,7 +4,7 @@ import pytest -from bluetooth_sig.registry.uuids.service_classes import ServiceClassesRegistry +from bluetooth_sig.registry.uuids.service_classes import ServiceClassesRegistry, get_service_classes_registry from bluetooth_sig.types.registry.service_class import ServiceClassInfo from bluetooth_sig.types.uuid import BluetoothUUID @@ -22,7 +22,7 @@ def test_registry_initialization(self, service_classes_registry: ServiceClassesR """Test that the registry initializes properly.""" assert isinstance(service_classes_registry, ServiceClassesRegistry) # Should have loaded some service classes if YAML exists - service_classes = service_classes_registry.get_all_service_classes() + service_classes = get_service_classes_registry().get_all_service_classes() assert isinstance(service_classes, list) # If submodule is initialized, should have service classes if service_classes: @@ -31,7 +31,7 @@ def test_registry_initialization(self, service_classes_registry: ServiceClassesR def test_get_service_class_info(self, service_classes_registry: ServiceClassesRegistry) -> None: """Test lookup by UUID string.""" # Test with a known service class UUID (Generic Access) - info = service_classes_registry.get_service_class_info("0x1800") + info = get_service_classes_registry().get_service_class_info("0x1800") if info: # Only if YAML loaded assert isinstance(info, ServiceClassInfo) assert info.name == "Generic Access" @@ -40,65 +40,65 @@ def test_get_service_class_info(self, service_classes_registry: ServiceClassesRe def test_get_service_class_info_by_name(self, service_classes_registry: ServiceClassesRegistry) -> None: """Test lookup by service class name.""" # Test with known service class name (Generic Access) - info = service_classes_registry.get_service_class_info_by_name("Generic Access") + info = get_service_classes_registry().get_service_class_info_by_name("Generic Access") if info: # Only if YAML loaded assert isinstance(info, ServiceClassInfo) assert info.name == "Generic Access" assert info.uuid.short_form.upper() == "1800" # Test case insensitive - info_lower = service_classes_registry.get_service_class_info_by_name("generic access") + info_lower = get_service_classes_registry().get_service_class_info_by_name("generic access") assert info_lower == info # Test not found - info_none = service_classes_registry.get_service_class_info_by_name("Nonexistent Service Class") + info_none = get_service_classes_registry().get_service_class_info_by_name("Nonexistent Service Class") assert info_none is None def test_get_service_class_info_by_id(self, service_classes_registry: ServiceClassesRegistry) -> None: """Test lookup by service class ID.""" # Test with known service class ID - info = service_classes_registry.get_service_class_info_by_id("org.bluetooth.service.generic_access") + info = get_service_classes_registry().get_service_class_info_by_id("org.bluetooth.service.generic_access") if info: # Only if YAML loaded assert isinstance(info, ServiceClassInfo) assert info.name == "Generic Access" assert info.uuid.short_form.upper() == "1800" # Test not found - info_none = service_classes_registry.get_service_class_info_by_id("org.bluetooth.service.nonexistent") + info_none = get_service_classes_registry().get_service_class_info_by_id("org.bluetooth.service.nonexistent") assert info_none is None def test_get_service_class_info_by_bluetooth_uuid(self, service_classes_registry: ServiceClassesRegistry) -> None: """Test lookup by BluetoothUUID object.""" # Create a BluetoothUUID for a known service class bt_uuid = BluetoothUUID("1800") - info = service_classes_registry.get_service_class_info(bt_uuid) + info = get_service_classes_registry().get_service_class_info(bt_uuid) if info: # Only if YAML loaded assert isinstance(info, ServiceClassInfo) assert info.name == "Generic Access" def test_get_service_class_info_not_found(self, service_classes_registry: ServiceClassesRegistry) -> None: """Test lookup for non-existent service class.""" - info = service_classes_registry.get_service_class_info("nonexistent") + info = get_service_classes_registry().get_service_class_info("nonexistent") assert info is None - info = service_classes_registry.get_service_class_info("0x0000") # Not a service class UUID + info = get_service_classes_registry().get_service_class_info("0x0000") # Not a service class UUID assert info is None def test_is_service_class_uuid(self, service_classes_registry: ServiceClassesRegistry) -> None: """Test service class UUID validation.""" # Known service class UUID - has_service_classes = bool(service_classes_registry.get_all_service_classes()) - assert service_classes_registry.is_service_class_uuid("0x1800") or not has_service_classes + has_service_classes = bool(get_service_classes_registry().get_all_service_classes()) + assert get_service_classes_registry().is_service_class_uuid("0x1800") or not has_service_classes # Non-service class UUID - assert not service_classes_registry.is_service_class_uuid("0x0000") + assert not get_service_classes_registry().is_service_class_uuid("0x0000") # Invalid UUID - assert not service_classes_registry.is_service_class_uuid("invalid") + assert not get_service_classes_registry().is_service_class_uuid("invalid") def test_get_all_service_classes(self, service_classes_registry: ServiceClassesRegistry) -> None: """Test getting all service classes.""" - service_classes = service_classes_registry.get_all_service_classes() + service_classes = get_service_classes_registry().get_all_service_classes() assert isinstance(service_classes, list) if service_classes: @@ -113,7 +113,7 @@ def test_get_all_service_classes(self, service_classes_registry: ServiceClassesR def test_service_class_info_structure(self, service_classes_registry: ServiceClassesRegistry) -> None: """Test ServiceClassInfo dataclass structure.""" - service_classes = service_classes_registry.get_all_service_classes() + service_classes = get_service_classes_registry().get_all_service_classes() if service_classes: service_class = service_classes[0] assert hasattr(service_class, "uuid") @@ -127,7 +127,7 @@ def test_uuid_formats(self, service_classes_registry: ServiceClassesRegistry) -> """Test various UUID input formats.""" formats: list[str | BluetoothUUID] = ["1800", "0x1800", "0X1800", BluetoothUUID("1800")] for fmt in formats: - info = service_classes_registry.get_service_class_info(fmt) - if service_classes_registry.is_service_class_uuid("1800"): + info = get_service_classes_registry().get_service_class_info(fmt) + if get_service_classes_registry().is_service_class_uuid("1800"): assert info is not None assert info.name == "Generic Access" diff --git a/tests/test_units_registry.py b/tests/test_units_registry.py index b7ea27a2..069bb359 100644 --- a/tests/test_units_registry.py +++ b/tests/test_units_registry.py @@ -4,7 +4,7 @@ import pytest -from bluetooth_sig.registry.uuids.units import UnitsRegistry +from bluetooth_sig.registry.uuids.units import UnitsRegistry, get_units_registry from bluetooth_sig.types.registry import UuidIdInfo from bluetooth_sig.types.uuid import BluetoothUUID @@ -22,7 +22,7 @@ def test_registry_initialization(self, units_registry: UnitsRegistry) -> None: """Test that the registry initializes properly.""" assert isinstance(units_registry, UnitsRegistry) # Should have loaded some units if YAML exists - units = units_registry.get_all_units() + units = get_units_registry().get_all_units() assert isinstance(units, list) # If submodule is initialized, should have units if units: @@ -31,7 +31,7 @@ def test_registry_initialization(self, units_registry: UnitsRegistry) -> None: def test_get_unit_info(self, units_registry: UnitsRegistry) -> None: """Test lookup by UUID string.""" # Test with a known unit UUID (unitless) - info = units_registry.get_unit_info("0x2700") + info = get_units_registry().get_unit_info("0x2700") if info: # Only if YAML loaded assert isinstance(info, UuidIdInfo) assert info.name == "unitless" @@ -40,64 +40,64 @@ def test_get_unit_info(self, units_registry: UnitsRegistry) -> None: def test_get_unit_info_by_name(self, units_registry: UnitsRegistry) -> None: """Test lookup by unit name.""" # Test with known unit name (unitless) - info = units_registry.get_unit_info_by_name("unitless") + info = get_units_registry().get_unit_info_by_name("unitless") if info: # Only if YAML loaded assert isinstance(info, UuidIdInfo) assert info.name == "unitless" assert info.uuid.short_form.upper() == "2700" # Test case insensitive - info_lower = units_registry.get_unit_info_by_name("Unitless") + info_lower = get_units_registry().get_unit_info_by_name("Unitless") assert info_lower == info # Test not found - info_none = units_registry.get_unit_info_by_name("Nonexistent Unit") + info_none = get_units_registry().get_unit_info_by_name("Nonexistent Unit") assert info_none is None def test_get_unit_info_by_id(self, units_registry: UnitsRegistry) -> None: """Test lookup by unit ID.""" # Test with known unit ID - info = units_registry.get_unit_info_by_id("org.bluetooth.unit.unitless") + info = get_units_registry().get_unit_info_by_id("org.bluetooth.unit.unitless") if info: # Only if YAML loaded assert isinstance(info, UuidIdInfo) assert info.name == "unitless" assert info.uuid.short_form.upper() == "2700" # Test not found - info_none = units_registry.get_unit_info_by_id("org.bluetooth.unit.nonexistent") + info_none = get_units_registry().get_unit_info_by_id("org.bluetooth.unit.nonexistent") assert info_none is None def test_get_unit_info_by_bluetooth_uuid(self, units_registry: UnitsRegistry) -> None: """Test lookup by BluetoothUUID object.""" # Create a BluetoothUUID for a known unit bt_uuid = BluetoothUUID("2700") - info = units_registry.get_unit_info(bt_uuid) + info = get_units_registry().get_unit_info(bt_uuid) if info: # Only if YAML loaded assert isinstance(info, UuidIdInfo) assert info.name == "unitless" def test_get_unit_info_not_found(self, units_registry: UnitsRegistry) -> None: """Test lookup for non-existent unit.""" - info = units_registry.get_unit_info("nonexistent") + info = get_units_registry().get_unit_info("nonexistent") assert info is None - info = units_registry.get_unit_info("0x0000") # Not a unit UUID + info = get_units_registry().get_unit_info("0x0000") # Not a unit UUID assert info is None def test_is_unit_uuid(self, units_registry: UnitsRegistry) -> None: """Test unit UUID validation.""" # Known unit UUID - assert units_registry.is_unit_uuid("0x2700") or not units_registry.get_all_units() + assert get_units_registry().is_unit_uuid("0x2700") or not get_units_registry().get_all_units() # Non-unit UUID - assert not units_registry.is_unit_uuid("0x0000") + assert not get_units_registry().is_unit_uuid("0x0000") # Invalid UUID - assert not units_registry.is_unit_uuid("invalid") + assert not get_units_registry().is_unit_uuid("invalid") def test_get_all_units(self, units_registry: UnitsRegistry) -> None: """Test getting all units.""" - units = units_registry.get_all_units() + units = get_units_registry().get_all_units() assert isinstance(units, list) if units: @@ -112,7 +112,7 @@ def test_get_all_units(self, units_registry: UnitsRegistry) -> None: def test_unit_info_structure(self, units_registry: UnitsRegistry) -> None: """Test UuidIdInfo dataclass structure.""" - units = units_registry.get_all_units() + units = get_units_registry().get_all_units() if units: unit = units[0] assert hasattr(unit, "uuid") @@ -126,6 +126,6 @@ def test_uuid_formats(self, units_registry: UnitsRegistry) -> None: """Test various UUID input formats.""" formats: list[str] = ["2700", "0x2700", "0X2700"] for fmt in formats: - info = units_registry.get_unit_info(fmt) + info = get_units_registry().get_unit_info(fmt) assert info is not None assert info.name == "unitless" diff --git a/tests/utils/test_prewarm.py b/tests/utils/test_prewarm.py index fe82c35a..af674fe7 100644 --- a/tests/utils/test_prewarm.py +++ b/tests/utils/test_prewarm.py @@ -2,7 +2,9 @@ from __future__ import annotations +from bluetooth_sig.gatt.uuid_registry import get_uuid_registry from bluetooth_sig.utils.prewarm import prewarm_registries +from bluetooth_sig.utils.prewarm_catalog import get_prewarm_loaders class TestPrewarmRegistries: @@ -31,3 +33,14 @@ def test_registries_populated_after_prewarm(self) -> None: services = GattServiceRegistry.get_all_services() assert len(services) > 0 + + def test_prewarm_catalogue_contains_uuid_registry(self) -> None: + """Prewarm catalogue must include UUID metadata hub warmup.""" + loader_names = {name for name, _ in get_prewarm_loaders()} + assert "uuid_registry" in loader_names + + def test_prewarm_loads_uuid_registry(self) -> None: + """Pre-warming must load UUID registry metadata.""" + prewarm_registries() + info = get_uuid_registry().get_characteristic_info("2A19") + assert info is not None