Coverage for gws-app/gws/lib/dynimport/__init__.py: 89%
54 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Dynamic imports of Python code.
3This package loads Python code at run time, either as a plain script or as a regular module:
5- ``load_file`` and ``load_string`` execute Python source and return its global namespace.
6 Nothing is added to ``sys.modules``. This is used, for example, for Python config files.
7- ``import_from_path`` imports a module from a file path and registers it in ``sys.modules``.
8 Paths inside the application directory are imported as part of the ``gws`` package.
9 Other paths (plugins) are imported relative to the deepest parent directory that is not a package
10 (has no ``__init__.py``), so that the plugin's own package structure is preserved.
12Example::
14 import gws.lib.dynimport
16 fn = gws.lib.dynimport.load_file('/data/config.py').get('main')
18 mod = gws.lib.dynimport.import_from_path('gws/plugin/ows_client/wms/caps.py')
19"""
21import sys
22import os
23import importlib
25import gws
28class Error(gws.Error):
29 """Dynamic import error."""
30 pass
33def load_file(path: str) -> dict:
34 """Execute a Python file and return its globals.
36 Args:
37 path: Path to the Python file.
39 Returns:
40 The global namespace of the executed code.
41 """
43 return load_string(gws.u.read_file(path), path)
46def load_string(text: str, path='') -> dict:
47 """Execute a string as Python code and return its globals.
49 Args:
50 text: Python source code.
51 path: File path, used for ``__file__`` and in error messages.
53 Returns:
54 The global namespace of the executed code.
55 """
57 globs = {'__file__': path}
58 code = compile(text, path, 'exec')
59 exec(code, globs)
60 return globs
63def import_from_path(path: str, base_dir: str = gws.c.APP_DIR):
64 """Import a module from a file path.
66 If the path is a directory, its ``__init__.py`` is imported. If a module with the same name
67 is already imported from the same file, it is returned as is.
69 Args:
70 path: Relative or absolute path to the module file or package directory.
71 base_dir: Base directory to resolve relative paths, the application directory by default.
73 Returns:
74 The imported module.
76 Raises:
77 ``Error``: If the module file is not found, a base directory cannot be located,
78 a module with the same name was imported from a different file, or the import fails.
79 """
80 abs_path = _abs_path(path, base_dir)
81 if not os.path.isfile(abs_path):
82 raise Error(f'{abs_path!r}: not found')
84 if abs_path.startswith(base_dir):
85 # Our own module, import relatively to base_dir
86 return _do_import(abs_path, base_dir)
88 # Plugin module, import relative to the bottom-most "namespace" dir (without __init__)
89 dirs = abs_path.strip('/').split('/')
90 dirs.pop()
92 for n in range(len(dirs), 0, -1):
93 ns_dir = '/' + '/'.join(dirs[:n])
94 if not os.path.isfile(ns_dir + '/__init__.py'):
95 return _do_import(abs_path, ns_dir)
97 raise Error(f'{abs_path!r}: cannot locate a base directory')
100def _abs_path(path: str, base_dir: str) -> str:
101 """Convert a path to an absolute normalized path of a Python file."""
102 if not os.path.isabs(path):
103 path = os.path.join(base_dir, path)
104 path = os.path.normpath(path)
105 if os.path.isdir(path):
106 path += '/__init__.py'
107 return path
110def _do_import(abs_path: str, base_dir: str):
111 """Import a module by its absolute path, with the module name relative to the base directory."""
112 mod_name = _module_name(abs_path[len(base_dir):])
114 if mod_name in sys.modules:
115 mpath = getattr(sys.modules[mod_name], '__file__', None)
116 if mpath != abs_path:
117 raise Error(f'{abs_path!r}: overwriting {mod_name!r} from {mpath!r}')
118 return sys.modules[mod_name]
120 gws.log.debug(f'import: {abs_path=} {mod_name=} {base_dir=}')
122 if base_dir not in sys.path:
123 sys.path.insert(0, base_dir)
125 try:
126 return importlib.import_module(mod_name)
127 except Exception as exc:
128 raise Error(f'{abs_path!r}: import failed') from exc
131def _module_name(path: str) -> str:
132 """Derive a dotted module name from a relative file path."""
133 parts = path.strip('/').split('/')
134 if parts[-1] == '__init__.py':
135 parts.pop()
136 elif parts[-1].endswith('.py'):
137 parts[-1] = parts[-1][:-3]
138 return '.'.join(parts)