Skip to content

Commit 8eb6dd3

Browse files
feat: enhance command set package preloading to prevent module conflicts and ensure dependencies
1 parent 5db81a7 commit 8eb6dd3

File tree

1 file changed

+80
-20
lines changed

1 file changed

+80
-20
lines changed

lib/dt_shell/commands/importer.py

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,81 @@
2222
DTCommandAbs, CommandSet, DTCommandSetConfigurationAbs, default_commandset_configuration
2323

2424

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+
25100
def import_commandset_configuration(command_set: CommandSet) -> Type[DTCommandSetConfigurationAbs]:
26101
# constants
27102
_configuration_file = _join(command_set.path, "__command_set__", "configuration.py")
@@ -104,6 +179,11 @@ def import_configuration(command_set: CommandSet, selector: str) -> Type[DTComma
104179
if DTShellConstants.VERBOSE:
105180
logger.debug(f"Importing configuration for command '{_command_sel}' from "
106181
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+
107187
configuration = importlib.import_module(_configuration_sel)
108188
except ShellNeedsUpdate as e:
109189
logger.warning(
@@ -154,26 +234,6 @@ def import_command(command_set: CommandSet, fpath: str) -> Type[DTCommandAbs]:
154234
if DTShellConstants.VERBOSE:
155235
logger.debug(f"Importing command '{_command_sel}' from '{_dirname(fpath)}/'")
156236

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]
177237
command = importlib.import_module(_command_sel)
178238
except ShellNeedsUpdate as e:
179239
logger.warning(

0 commit comments

Comments
 (0)