Coverage for src / bluetooth_sig / registry / core / formattypes.py: 80%

64 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 20:14 +0000

1"""Format Types registry for Bluetooth SIG characteristic format type definitions.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6 

7import msgspec 

8 

9from bluetooth_sig.registry.base import BaseGenericRegistry 

10from bluetooth_sig.registry.utils import find_bluetooth_sig_path 

11from bluetooth_sig.types.registry.formattypes import FormatTypeInfo 

12 

13logger = logging.getLogger(__name__) 

14 

15 

16class FormatTypesRegistry(BaseGenericRegistry[FormatTypeInfo]): 

17 """Registry for Bluetooth characteristic format types with lazy loading. 

18 

19 This registry loads format type definitions from the official Bluetooth SIG 

20 assigned_numbers YAML file, providing authoritative format type information 

21 from the specification. 

22 """ 

23 

24 def __init__(self) -> None: 

25 """Initialize the format types registry.""" 

26 super().__init__() 

27 self._format_types: dict[int, FormatTypeInfo] = {} 

28 self._format_types_by_name: dict[str, FormatTypeInfo] = {} 

29 

30 def _load(self) -> None: 

31 """Perform the actual loading of format types data.""" 

32 base_path = find_bluetooth_sig_path() 

33 if not base_path: 

34 logger.warning("Bluetooth SIG path not found. Format types registry will be empty.") 

35 self._loaded = True 

36 return 

37 

38 yaml_path = base_path.parent / "core" / "formattypes.yaml" 

39 if not yaml_path.exists(): 

40 logger.warning( 

41 "Format types YAML file not found at %s. Registry will be empty.", 

42 yaml_path, 

43 ) 

44 self._loaded = True 

45 return 

46 

47 try: 

48 with yaml_path.open("r", encoding="utf-8") as f: 

49 data = msgspec.yaml.decode(f.read()) 

50 

51 if not data or "formattypes" not in data: 

52 logger.warning("Invalid format types YAML format. Registry will be empty.") 

53 self._loaded = True 

54 return 

55 

56 for item in data["formattypes"]: 

57 value = item.get("value") 

58 short_name = item.get("short_name") 

59 description = item.get("description") 

60 exponent = item.get("exponent") 

61 size = item.get("size") 

62 

63 if value is None or not short_name: 

64 continue 

65 

66 # Handle hex values in YAML (e.g., 0x01) 

67 if isinstance(value, str): 

68 value = int(value, 16) 

69 

70 format_type_info = FormatTypeInfo( 

71 value=value, 

72 short_name=short_name, 

73 description=description, 

74 exponent=exponent, 

75 size=size, 

76 ) 

77 

78 self._format_types[value] = format_type_info 

79 self._format_types_by_name[short_name.lower()] = format_type_info 

80 

81 logger.info("Loaded %d format types from specification", len(self._format_types)) 

82 except (FileNotFoundError, OSError, msgspec.DecodeError, KeyError) as e: 

83 logger.warning( 

84 "Failed to load format types from YAML: %s. Registry will be empty.", 

85 e, 

86 ) 

87 

88 self._loaded = True 

89 

90 def get_format_type_info(self, value: int) -> FormatTypeInfo | None: 

91 """Get format type info by value (lazy loads on first call). 

92 

93 Args: 

94 value: The format type value (e.g., 0x01 for boolean) 

95 

96 Returns: 

97 FormatTypeInfo object, or None if not found 

98 """ 

99 self._ensure_loaded() 

100 with self._lock: 

101 return self._format_types.get(value) 

102 

103 def get_format_type_by_name(self, name: str) -> FormatTypeInfo | None: 

104 """Get format type info by short name (lazy loads on first call). 

105 

106 Args: 

107 name: Format type short name (case-insensitive, e.g., "boolean", "utf8s") 

108 

109 Returns: 

110 FormatTypeInfo object, or None if not found 

111 """ 

112 self._ensure_loaded() 

113 with self._lock: 

114 return self._format_types_by_name.get(name.lower()) 

115 

116 def is_known_format_type(self, value: int) -> bool: 

117 """Check if format type is known (lazy loads on first call). 

118 

119 Args: 

120 value: The format type value to check 

121 

122 Returns: 

123 True if the format type is registered, False otherwise 

124 """ 

125 self._ensure_loaded() 

126 with self._lock: 

127 return value in self._format_types 

128 

129 def get_all_format_types(self) -> dict[int, FormatTypeInfo]: 

130 """Get all registered format types (lazy loads on first call). 

131 

132 Returns: 

133 Dictionary mapping format type values to FormatTypeInfo objects 

134 """ 

135 self._ensure_loaded() 

136 with self._lock: 

137 return self._format_types.copy() 

138 

139 

140# Global singleton instance 

141format_types_registry = FormatTypesRegistry()