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

1# Reference: https://docs.djangoproject.com/en/5.0/_modules/django/utils/module_loading/#import_string 

2 

3import sys 

4from fnmatch import fnmatchcase 

5from importlib import import_module 

6 

7 

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) 

17 

18 

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 

23 

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` 

26 

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