Coverage for gws-app/gws/lib/importer/__init__.py: 89%
54 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
1"""Handle dynamic imports"""
3import sys
4import os
5import importlib
7import gws
10class Error(gws.Error):
11 """Custom error class for import-related exceptions."""
12 pass
15def load_file(path: str) -> dict:
16 """Load a python file and return its globals."""
18 return load_string(gws.u.read_file(path), path)
21def load_string(text: str, path='') -> dict:
22 """Load a string as python code and return its globals."""
24 globs = {'__file__': path}
25 code = compile(text, path, 'exec')
26 exec(code, globs)
27 return globs
30def import_from_path(path: str, base_dir: str = gws.c.APP_DIR):
31 """Imports a module from a given file path.
33 Args:
34 path: The relative or absolute path to the module file.
35 base_dir: The base directory to resolve relative paths. Defaults to `gws.c.APP_DIR`.
37 Returns:
38 The imported module.
40 Raises:
41 Error: If the module file is not found or a base directory cannot be located.
42 """
43 abs_path = _abs_path(path, base_dir)
44 if not os.path.isfile(abs_path):
45 raise Error(f'{abs_path!r}: not found')
47 if abs_path.startswith(base_dir):
48 # Our own module, import relatively to base_dir
49 return _do_import(abs_path, base_dir)
51 # Plugin module, import relative to the bottom-most "namespace" dir (without __init__)
52 dirs = abs_path.strip('/').split('/')
53 dirs.pop()
55 for n in range(len(dirs), 0, -1):
56 ns_dir = '/' + '/'.join(dirs[:n])
57 if not os.path.isfile(ns_dir + '/__init__.py'):
58 return _do_import(abs_path, ns_dir)
60 raise Error(f'{abs_path!r}: cannot locate a base directory')
63def _abs_path(path: str, base_dir: str) -> str:
64 """Converts a relative path to an absolute normalized path.
66 Args:
67 path: The input file path.
68 base_dir: The base directory for resolving relative paths.
70 Returns:
71 The absolute, normalized file path.
72 """
73 if not os.path.isabs(path):
74 path = os.path.join(base_dir, path)
75 path = os.path.normpath(path)
76 if os.path.isdir(path):
77 path += '/__init__.py'
78 return path
81def _do_import(abs_path: str, base_dir: str):
82 """Imports a module given its absolute path and base directory.
84 Args:
85 abs_path: The absolute path to the module file.
86 base_dir: The base directory for resolving module names.
88 Returns:
89 The imported module.
91 Raises:
92 Error: If the module import fails or an existing module is being overwritten.
93 """
94 mod_name = _module_name(abs_path[len(base_dir):])
96 if mod_name in sys.modules:
97 mpath = getattr(sys.modules[mod_name], '__file__', None)
98 if mpath != abs_path:
99 raise Error(f'{abs_path!r}: overwriting {mod_name!r} from {mpath!r}')
100 return sys.modules[mod_name]
102 gws.log.debug(f'import: {abs_path=} {mod_name=} {base_dir=}')
104 if base_dir not in sys.path:
105 sys.path.insert(0, base_dir)
107 try:
108 return importlib.import_module(mod_name)
109 except Exception as exc:
110 raise Error(f'{abs_path!r}: import failed') from exc
113def _module_name(path: str) -> str:
114 """Derives the module name from a given file path.
116 Args:
117 path: The file path of the module.
119 Returns:
120 The module name in dotted notation.
121 """
122 parts = path.strip('/').split('/')
123 if parts[-1] == '__init__.py':
124 parts.pop()
125 elif parts[-1].endswith('.py'):
126 parts[-1] = parts[-1][:-3]
127 return '.'.join(parts)