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

1"""Common utilities for GATT registries. 

2 

3This module contains shared utility classes used by both characteristic and 

4service registries to avoid code duplication. 

5""" 

6 

7from __future__ import annotations 

8 

9import inspect 

10import pkgutil 

11from importlib import import_module 

12from typing import Any, Callable, TypeVar 

13 

14from typing_extensions import TypeGuard 

15 

16 

17class TypeValidator: # pylint: disable=too-few-public-methods 

18 """Utility class for type validation operations. 

19 

20 Note: Utility class pattern with static methods - pylint disable justified. 

21 """ 

22 

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. 

26 

27 Args: 

28 candidate: Object to check 

29 base_class: Base class to check against 

30 

31 Returns: 

32 True if candidate is a subclass of base_class 

33 

34 """ 

35 return isinstance(candidate, type) and issubclass(candidate, base_class) 

36 

37 

38T = TypeVar("T") 

39 

40 

41class ModuleDiscovery: 

42 """Base class for discovering classes in a package using pkgutil.walk_packages. 

43 

44 This utility provides a common pattern for discovering and validating classes 

45 across different GATT packages (characteristics and services). 

46 """ 

47 

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. 

54 

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"}) 

58 

59 Returns: 

60 Sorted list of module names found in the package 

61 

62 References: 

63 Python standard library documentation, pkgutil.walk_packages, 

64 https://docs.python.org/3/library/pkgutil.html#pkgutil.walk_packages 

65 

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 

77 

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. 

85 

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 

90 

91 Returns: 

92 Sorted list of discovered classes 

93 

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 

106 

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 

110 

111 candidates.append(obj) 

112 candidates.sort(key=lambda cls: cls.__name__) 

113 discovered.extend(candidates) 

114 return discovered