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
« 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
5from __future__ import annotations
7from importlib import import_module
8from typing import TYPE_CHECKING, Generic, Protocol, TypeVar
9from weakref import WeakValueDictionary
11from django.conf import settings
12from django.core.exceptions import ImproperlyConfigured
13from django.utils.functional import cached_property
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
19 from django_stubs_ext import StrOrPromise
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
41class ClassLoaderProtocol(Protocol):
42 @property
43 def name(self) -> StrOrPromise: ... 43 ↛ exitline 43 didn't return from function 'name' because
46T = TypeVar("T", bound=ClassLoaderProtocol)
49class ClassLoader(Generic[T]):
50 """Dict like object to lazy load list of classes."""
52 instances: WeakValueDictionary[str, ClassLoader] = WeakValueDictionary()
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
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
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
104 @cached_property
105 def data(self) -> dict[str, T]:
106 return self.load_data()
108 def __getitem__(self, key: str) -> T:
109 return self.data.__getitem__(key)
111 def __setitem__(self, key: str, value: T) -> None:
112 self.data.__setitem__(key, value)
114 def get(self, key: str) -> T | None:
115 return self.data.get(key)
117 def items(self) -> dict_items[str, T]: # pylint: disable=invalid-sequence-index
118 return self.data.items()
120 def keys(self) -> dict_keys[str, T]: # pylint: disable=invalid-sequence-index
121 return self.data.keys()
123 def values(self) -> dict_values[str, T]: # pylint: disable=invalid-sequence-index
124 return self.data.values()
126 def __iter__(self) -> Iterator[str]:
127 return self.data.__iter__()
129 def __len__(self) -> int:
130 return self.data.__len__()
132 def __contains__(self, item: T) -> bool:
133 return self.data.__contains__(item)
135 def exists(self) -> bool:
136 return bool(self.data)
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:
146 def cond(x: T) -> bool:
147 return True
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