Coverage for src/bluetooth_sig/gatt/registry_utils.py: 98%
43 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +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 importlib import import_module
12from typing import Any, Callable, TypeVar
14from typing_extensions import TypeGuard
17class TypeValidator: # pylint: disable=too-few-public-methods
18 """Utility class for type validation operations.
20 Note: Utility class pattern with static methods - pylint disable justified.
21 """
23 @staticmethod
24 def is_subclass_of(candidate: object, base_class: type) -> TypeGuard[type]:
25 """Return True when candidate is a subclass of base_class.
27 Args:
28 candidate: Object to check
29 base_class: Base class to check against
31 Returns:
32 True if candidate is a subclass of base_class
34 """
35 return isinstance(candidate, type) and issubclass(candidate, base_class)
38T = TypeVar("T")
41class ModuleDiscovery:
42 """Base class for discovering classes in a package using pkgutil.walk_packages.
44 This utility provides a common pattern for discovering and validating classes
45 across different GATT packages (characteristics and services).
46 """
48 @staticmethod
49 def iter_module_names(
50 package_name: str,
51 module_exclusions: set[str],
52 ) -> list[str]:
53 """Return sorted module names discovered via pkgutil.walk_packages.
55 Args:
56 package_name: Fully qualified package name (e.g., "bluetooth_sig.gatt.characteristics")
57 module_exclusions: Set of module basenames to exclude (e.g., {"__init__", "base"})
59 Returns:
60 Sorted list of module names found in the package
62 References:
63 Python standard library documentation, pkgutil.walk_packages,
64 https://docs.python.org/3/library/pkgutil.html#pkgutil.walk_packages
66 """
67 package = import_module(package_name)
68 module_names: list[str] = []
69 prefix = f"{package_name}."
70 for module_info in pkgutil.walk_packages(package.__path__, prefix):
71 module_basename = module_info.name.rsplit(".", 1)[-1]
72 if module_basename in module_exclusions:
73 continue
74 module_names.append(module_info.name)
75 module_names.sort()
76 return module_names
78 @staticmethod
79 def discover_classes(
80 module_names: list[str],
81 base_class: type[T],
82 validator: Callable[[Any], bool],
83 ) -> list[type[T]]:
84 """Discover all concrete classes in modules that pass validation.
86 Args:
87 module_names: List of module names to search
88 base_class: Base class type for filtering
89 validator: Function to validate if object is a valid subclass
91 Returns:
92 Sorted list of discovered classes
94 """
95 discovered: list[type[T]] = []
96 for module_name in module_names:
97 module = import_module(module_name)
98 candidates: list[type[T]] = []
99 for _, obj in inspect.getmembers(module, inspect.isclass):
100 if not validator(obj):
101 continue
102 if obj is base_class or getattr(obj, "_is_template", False):
103 continue
104 if obj.__module__ != module.__name__:
105 continue
107 # Validate that the class has required methods
108 if not hasattr(obj, "get_class_uuid") or not callable(obj.get_class_uuid):
109 continue # Skip classes without proper UUID resolution
111 candidates.append(obj)
112 candidates.sort(key=lambda cls: cls.__name__)
113 discovered.extend(candidates)
114 return discovered