Coverage for /Users/ajo/work/jumpstarter/jumpstarter/packages/jumpstarter/jumpstarter/common/importlib.py: 68%
19 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:21 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:21 +0200
1# Reference: https://docs.djangoproject.com/en/5.0/_modules/django/utils/module_loading/#import_string
3import sys
4from fnmatch import fnmatchcase
5from importlib import import_module
8def cached_import(module_path, class_name):
9 # Check whether module is loaded and fully initialized.
10 if not (
11 (module := sys.modules.get(module_path))
12 and (spec := getattr(module, "__spec__", None))
13 and getattr(spec, "_initializing", False) is False
14 ):
15 module = import_module(module_path)
16 return getattr(module, class_name)
19def import_class(class_path: str, allow: list[str], unsafe: bool):
20 """
21 Import a class by its full class path while checking
22 the path matches the given allow list with unix style glob
24 e.g. `import_class("example_package.some_module.fooclass", allow=["example_package.*"], unsafe=false)`
25 is equivalent to `from example_package.some_module import FooClass; return FooClass`
27 while `import_class("example_package.some_module.fooclass", allow=["notexample_package.*"], unsafe=false)`
28 throws ImportError due to not matching the allow list
29 """
30 if not unsafe:
31 if not any(fnmatchcase(class_path, pattern) for pattern in allow):
32 raise ImportError(f"{class_path} doesn't match any of the allowed patterns")
33 try:
34 module_path, class_name = class_path.rsplit(".", 1)
35 except ValueError as e:
36 raise ImportError(f"{class_path} doesn't look like a class path") from e
37 try:
38 return cached_import(module_path, class_name)
39 except AttributeError as e:
40 raise ImportError(f"{module_path} doesn't have specified class {class_name}") from e