Coverage for app/venv/lib/python3.14/site-packages/weblate/utils/classloader.py: 70%

103 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 07:15 +0000

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7from importlib import import_module 

8from typing import TYPE_CHECKING, Generic, Protocol, TypeVar 

9from weakref import WeakValueDictionary 

10 

11from django.conf import settings 

12from django.core.exceptions import ImproperlyConfigured 

13from django.utils.functional import cached_property 

14 

15if TYPE_CHECKING: 15 ↛ 16line 15 didn't jump to line 16 because the condition on line 15 was never true

16 from _collections_abc import dict_items, dict_keys, dict_values 

17 from collections.abc import Callable, Iterator 

18 

19 from django_stubs_ext import StrOrPromise 

20 

21 

22def load_class(name, setting): 

23 """Import module and creates class given by name in string.""" 

24 try: 

25 module, attr = name.rsplit(".", 1) 

26 except ValueError as error: 

27 msg = f"Error importing class {name!r} in {setting}: {error}" 

28 raise ImproperlyConfigured(msg) from error 

29 try: 

30 mod = import_module(module) 

31 except ImportError as error: 

32 msg = f"Error importing module {module!r} in {setting}: {error}" 

33 raise ImproperlyConfigured(msg) from error 

34 try: 

35 return getattr(mod, attr) 

36 except AttributeError as error: 

37 msg = f"Module {module!r} does not define a {attr!r} class in {setting}" 

38 raise ImproperlyConfigured(msg) from error 

39 

40 

41class ClassLoaderProtocol(Protocol): 

42 @property 

43 def name(self) -> StrOrPromise: ... 43 ↛ exitline 43 didn't return from function 'name' because

44 

45 

46T = TypeVar("T", bound=ClassLoaderProtocol) 

47 

48 

49class ClassLoader(Generic[T]): 

50 """Dict like object to lazy load list of classes.""" 

51 

52 instances: WeakValueDictionary[str, ClassLoader] = WeakValueDictionary() 

53 

54 def __init__( 

55 self, 

56 name: str, 

57 *, 

58 base_class: type[T], 

59 construct: bool = True, 

60 collect_errors: bool = False, 

61 ) -> None: 

62 self.name = name 

63 self.construct = construct 

64 self.collect_errors = collect_errors 

65 self.errors: dict[str, str | Exception] = {} 

66 self.base_class: type = base_class 

67 obj = self.instances.get(name) 

68 if obj is None: 68 ↛ exitline 68 didn't return from function '__init__' because the condition on line 68 was always true

69 self.instances[name] = self 

70 

71 def get_settings(self): 

72 result = getattr(settings, self.name) 

73 if result is None: 73 ↛ 75line 73 didn't jump to line 75 because the condition on line 73 was never true

74 # Special case to disable all checks/... 

75 result = [] 

76 elif not isinstance(result, (list, tuple)): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true

77 msg = f"Setting {self.name} must be list or tuple!" 

78 raise ImproperlyConfigured(msg) 

79 return result 

80 

81 def load_data(self) -> dict[str, T]: 

82 result = {} 

83 value = self.get_settings() 

84 for path in value: 

85 try: 

86 obj = load_class(path, self.name) 

87 except ImproperlyConfigured as error: 

88 self.errors[path] = error 

89 if self.collect_errors: 

90 continue 

91 raise 

92 try: 

93 if not issubclass(obj, self.base_class): 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true

94 msg = f"Setting {self.name} must be a {self.base_class.__name__} subclass, but {path} is {obj!r}" 

95 raise ImproperlyConfigured(msg) 

96 except TypeError as error: 

97 msg = f"Setting {self.name} must be a {self.base_class.__name__} subclass, but {path} is {obj!r}" 

98 raise ImproperlyConfigured(msg) from error 

99 if self.construct: 

100 obj = obj() 

101 result[obj.get_identifier()] = obj 

102 return result 

103 

104 @cached_property 

105 def data(self) -> dict[str, T]: 

106 return self.load_data() 

107 

108 def __getitem__(self, key: str) -> T: 

109 return self.data.__getitem__(key) 

110 

111 def __setitem__(self, key: str, value: T) -> None: 

112 self.data.__setitem__(key, value) 

113 

114 def get(self, key: str) -> T | None: 

115 return self.data.get(key) 

116 

117 def items(self) -> dict_items[str, T]: # pylint: disable=invalid-sequence-index 

118 return self.data.items() 

119 

120 def keys(self) -> dict_keys[str, T]: # pylint: disable=invalid-sequence-index 

121 return self.data.keys() 

122 

123 def values(self) -> dict_values[str, T]: # pylint: disable=invalid-sequence-index 

124 return self.data.values() 

125 

126 def __iter__(self) -> Iterator[str]: 

127 return self.data.__iter__() 

128 

129 def __len__(self) -> int: 

130 return self.data.__len__() 

131 

132 def __contains__(self, item: T) -> bool: 

133 return self.data.__contains__(item) 

134 

135 def exists(self) -> bool: 

136 return bool(self.data) 

137 

138 def get_choices( 

139 self, 

140 empty: bool = False, 

141 exclude: tuple[str, ...] | set[str] = (), 

142 cond: Callable[[T], bool] | None = None, 

143 ): 

144 if cond is None: 

145 

146 def cond(x: T) -> bool: 

147 return True 

148 

149 result = [ 

150 (x, self[x].name) 

151 for x in sorted(self.keys()) 

152 if x not in exclude and cond(self[x]) 

153 ] 

154 if empty: 

155 result.insert(0, ("", "")) 

156 return result