Coverage for src / bluetooth_sig / gatt / registry_utils.py: 98%
45 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Common utilities for GATT registries.
3This module contains shared utility classes used by both characteristic and
4service registries to avoid code duplication.
5"""
7from __future__ import annotations
9import inspect
10import pkgutil
11from collections.abc import Callable
12from importlib import import_module
13from typing import Any, TypeGuard, TypeVar
16class TypeValidator: # pylint: disable=too-few-public-methods
17 """Utility class for type validation operations.
19 Note: Utility class pattern with static methods - pylint disable justified.
20 """
22 @staticmethod
23 def is_subclass_of(candidate: object, base_class: type) -> TypeGuard[type]:
24 """Return True when candidate is a subclass of base_class.
26 Args:
27 candidate: Object to check
28 base_class: Base class to check against
30 Returns:
31 True if candidate is a subclass of base_class
33 """
34 return isinstance(candidate, type) and issubclass(candidate, base_class)
37T = TypeVar("T")
40class ModuleDiscovery:
41 """Base class for discovering classes in a package using pkgutil.walk_packages.
43 This utility provides a common pattern for discovering and validating classes
44 across different GATT packages (characteristics and services).
45 """
47 @staticmethod
48 def iter_module_names(
49 package_name: str,
50 module_exclusions: set[str],
51 ) -> list[str]:
52 """Return sorted module names discovered via pkgutil.walk_packages.
54 Args:
55 package_name: Fully qualified package name (e.g., "bluetooth_sig.gatt.characteristics")
56 module_exclusions: Set of module basenames to exclude (e.g., {"__init__", "base"})
58 Returns:
59 Sorted list of module names found in the package
61 References:
62 Python standard library documentation, pkgutil.walk_packages,
63 https://docs.python.org/3/library/pkgutil.html#pkgutil.walk_packages
65 """
66 package = import_module(package_name)
67 module_names: list[str] = []
68 prefix = f"{package_name}."
69 for module_info in pkgutil.walk_packages(package.__path__, prefix):
70 module_basename = module_info.name.rsplit(".", 1)[-1]
71 if module_basename in module_exclusions:
72 continue
73 module_names.append(module_info.name)
74 module_names.sort()
75 return module_names
77 @staticmethod
78 def discover_classes(
79 module_names: list[str],
80 base_class: type[T],
81 validator: Callable[[Any], bool],
82 ) -> list[type[T]]:
83 """Discover all concrete classes in modules that pass validation.
85 Args:
86 module_names: List of module names to search
87 base_class: Base class type for filtering
88 validator: Function to validate if object is a valid subclass
90 Returns:
91 Sorted list of discovered classes
93 """
94 discovered: list[type[T]] = []
95 for module_name in module_names:
96 module = import_module(module_name)
97 candidates: list[type[T]] = []
98 for _, obj in inspect.getmembers(module, inspect.isclass):
99 if not validator(obj):
100 continue
101 if obj is base_class or getattr(obj, "_is_template", False):
102 continue
103 if getattr(obj, "_is_base_class", False):
104 continue # Skip base classes that require parameters
105 if obj.__module__ != module.__name__:
106 continue
108 # Validate that the class has required methods
109 if not hasattr(obj, "get_class_uuid") or not callable(obj.get_class_uuid):
110 continue # Skip classes without proper UUID resolution
112 candidates.append(obj)
113 candidates.sort(key=lambda cls: cls.__name__)
114 discovered.extend(candidates)
115 return discovered