|
22 | 22 | DTCommandAbs, CommandSet, DTCommandSetConfigurationAbs, default_commandset_configuration |
23 | 23 |
|
24 | 24 |
|
| 25 | +def _preload_command_set_packages(command_set: CommandSet, module_parts: List[str]) -> None: |
| 26 | + """ |
| 27 | + Pre-load packages from the command set to prevent Python from finding built-in modules |
| 28 | + with the same names (e.g., 'code', 'devel') or missing dependencies (e.g., 'utils'). |
| 29 | +
|
| 30 | + Args: |
| 31 | + command_set: The command set containing the packages |
| 32 | + module_parts: List of module name parts (e.g., ['code', 'build'] for 'code.build') |
| 33 | + """ |
| 34 | + # Pre-load utility package from command set (e.g., 'utils') that may be imported |
| 35 | + _util_pkg = "utils" |
| 36 | + _util_path = _join(command_set.path, _util_pkg) |
| 37 | + _util_init = _join(_util_path, "__init__.py") |
| 38 | + if _exists(_util_init) and _util_pkg not in sys.modules: |
| 39 | + spec = importlib.util.spec_from_file_location( |
| 40 | + _util_pkg, |
| 41 | + _util_init, |
| 42 | + submodule_search_locations=[_util_path] |
| 43 | + ) |
| 44 | + if spec and spec.loader: |
| 45 | + module = importlib.util.module_from_spec(spec) |
| 46 | + sys.modules[_util_pkg] = module |
| 47 | + try: |
| 48 | + spec.loader.exec_module(module) |
| 49 | + except Exception: |
| 50 | + # Log the exception to aid debugging when utility package loading fails |
| 51 | + logger.exception( |
| 52 | + f"Failed to preload utility package '{_util_pkg}' from '{_util_path}'. Removing it from " |
| 53 | + "sys.modules." |
| 54 | + ) |
| 55 | + # If loading fails, remove from sys.modules |
| 56 | + if _util_pkg in sys.modules: |
| 57 | + del sys.modules[_util_pkg] |
| 58 | + |
| 59 | + # Pre-load parent packages from command set to prevent Python from finding built-in modules |
| 60 | + for idx in range(1, len(module_parts) + 1): |
| 61 | + _parent_name = ".".join(module_parts[:idx]) |
| 62 | + _parent_path = _join(command_set.path, *module_parts[:idx]) |
| 63 | + _parent_init = _join(_parent_path, "__init__.py") |
| 64 | + |
| 65 | + # Check if module is already loaded |
| 66 | + if _parent_name in sys.modules: |
| 67 | + mod = sys.modules.get(_parent_name) |
| 68 | + # Check if it's from our command set |
| 69 | + if mod is not None and hasattr(mod, "__path__") and any(command_set.path in str(p) for p in mod.__path__): |
| 70 | + # It's ours, keep it |
| 71 | + continue |
| 72 | + else: |
| 73 | + # It's a conflicting module, remove it |
| 74 | + if DTShellConstants.VERBOSE: |
| 75 | + module_file = getattr(mod, "__file__") |
| 76 | + logger.debug(f"Removing conflicting module '{_parent_name}' ({module_file})") |
| 77 | + del sys.modules[_parent_name] |
| 78 | + |
| 79 | + # Explicitly load our package if it exists |
| 80 | + if not _exists(_parent_init): |
| 81 | + continue |
| 82 | + spec = importlib.util.spec_from_file_location( |
| 83 | + _parent_name, |
| 84 | + _parent_init, |
| 85 | + submodule_search_locations=[_parent_path] |
| 86 | + ) |
| 87 | + if not spec or not spec.loader: |
| 88 | + continue |
| 89 | + module = importlib.util.module_from_spec(spec) |
| 90 | + sys.modules[_parent_name] = module |
| 91 | + try: |
| 92 | + spec.loader.exec_module(module) |
| 93 | + except Exception: |
| 94 | + # If loading fails, remove from sys.modules |
| 95 | + if _parent_name in sys.modules: |
| 96 | + del sys.modules[_parent_name] |
| 97 | + raise |
| 98 | + |
| 99 | + |
25 | 100 | def import_commandset_configuration(command_set: CommandSet) -> Type[DTCommandSetConfigurationAbs]: |
26 | 101 | # constants |
27 | 102 | _configuration_file = _join(command_set.path, "__command_set__", "configuration.py") |
@@ -104,6 +179,11 @@ def import_configuration(command_set: CommandSet, selector: str) -> Type[DTComma |
104 | 179 | if DTShellConstants.VERBOSE: |
105 | 180 | logger.debug(f"Importing configuration for command '{_command_sel}' from " |
106 | 181 | f"'{_configuration_file}'") |
| 182 | + |
| 183 | + # Pre-load packages to prevent conflicts with built-in modules and ensure dependencies |
| 184 | + _parts = _command_sel.split('.') |
| 185 | + _preload_command_set_packages(command_set, _parts) |
| 186 | + |
107 | 187 | configuration = importlib.import_module(_configuration_sel) |
108 | 188 | except ShellNeedsUpdate as e: |
109 | 189 | logger.warning( |
@@ -154,26 +234,6 @@ def import_command(command_set: CommandSet, fpath: str) -> Type[DTCommandAbs]: |
154 | 234 | if DTShellConstants.VERBOSE: |
155 | 235 | logger.debug(f"Importing command '{_command_sel}' from '{_dirname(fpath)}/'") |
156 | 236 |
|
157 | | - # when running in debug mode (via vscode debugpy), it seems that Python's `code.py` is loaded |
158 | | - # and it clashes with the `code` commands... The code below checks for such clashes, and |
159 | | - # unloads the module if not part of the commandset. |
160 | | - |
161 | | - # see implementation of `_find_and_load_unlocked` in importlib._bootstrap |
162 | | - _command_sel_parent = _command_sel.split('.')[0] |
163 | | - if _command_sel_parent and _command_sel_parent in sys.modules: |
164 | | - mod = sys.modules.get(_command_sel_parent, None) |
165 | | - # if (_command_sel_parent == "code"): |
166 | | - # print(dir(mod)) |
167 | | - # print(mod.__path__) |
168 | | - if "__path__" not in dir(mod) or mod.__path__[0] not in _command_dir: |
169 | | - # print(f"{_command_dir} == {mod.__path__[0]}") |
170 | | - logger.warning( |
171 | | - f"Command '{_command_sel}' is already loaded or there's a name " |
172 | | - f"clash with '{_command_sel_parent}:{mod.__file__}'. Unloading '{_command_sel_parent}' " |
173 | | - f"to load '{_command_sel}'" |
174 | | - ) |
175 | | - # see https://stackoverflow.com/questions/43181440/what-does-del-sys-modulesmodule-actually-do |
176 | | - del sys.modules[_command_sel_parent] |
177 | 237 | command = importlib.import_module(_command_sel) |
178 | 238 | except ShellNeedsUpdate as e: |
179 | 239 | logger.warning( |
|
0 commit comments