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

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 collections.abc import Callable 

12from importlib import import_module 

13from typing import Any, TypeGuard, TypeVar 

14 

15 

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

17 """Utility class for type validation operations. 

18 

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

20 """ 

21 

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. 

25 

26 Args: 

27 candidate: Object to check 

28 base_class: Base class to check against 

29 

30 Returns: 

31 True if candidate is a subclass of base_class 

32 

33 """ 

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

35 

36 

37T = TypeVar("T") 

38 

39 

40class ModuleDiscovery: 

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

42 

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

44 across different GATT packages (characteristics and services). 

45 """ 

46 

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. 

53 

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

57 

58 Returns: 

59 Sorted list of module names found in the package 

60 

61 References: 

62 Python standard library documentation, pkgutil.walk_packages, 

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

64 

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 

76 

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. 

84 

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 

89 

90 Returns: 

91 Sorted list of discovered classes 

92 

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 

107 

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 

111 

112 candidates.append(obj) 

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

114 discovered.extend(candidates) 

115 return discovered