Skip to content

Commit 8546be0

Browse files
authored
Re-adding load_submodules (Project-MONAI#8118)
### Description This restores load_modules for now to resolve importation issues. Doing a two-pass loading process seems to allow some references which normally Python wouldn't permit. The fact that these issues weren't caught in testing is rather strange, and only showed up when symbols in `monai.apps` were references in bundles. I'm not sure if this is all related to circular imports, if parts of MONAI aren't being tested properly, or the way the symbol resolution works within `monai.bundle` should be improved. ### Types of changes <!--- Put an `x` in all the boxes that apply, and remove the not applicable items --> - [x] Non-breaking change (fix or new feature that would not break existing functionality). - [ ] Breaking change (fix or new feature that would cause existing functionality to change). - [ ] New tests added to cover the changes. - [ ] Integration tests passed locally by running `./runtests.sh -f -u --net --coverage`. - [ ] Quick tests passed locally by running `./runtests.sh --quick --unittests --disttests`. - [ ] In-line docstrings updated. - [ ] Documentation updated, tested `make html` command in the `docs/` folder. --------- Signed-off-by: Eric Kerfoot <eric.kerfoot@kcl.ac.uk>
1 parent cac21f6 commit 8546be0

File tree

4 files changed

+63
-18
lines changed

4 files changed

+63
-18
lines changed

monai/__init__.py

+21-17
Original file line numberDiff line numberDiff line change
@@ -80,25 +80,29 @@ def filter(self, record):
8080
)
8181

8282

83-
from . import ( # noqa: E402
84-
apps,
85-
auto3dseg,
86-
bundle,
87-
config,
88-
data,
89-
engines,
90-
fl,
91-
handlers,
92-
inferers,
93-
losses,
94-
metrics,
95-
networks,
96-
optimizers,
97-
transforms,
98-
utils,
99-
visualize,
83+
from .utils.module import load_submodules # noqa: E402
84+
85+
# handlers_* have some external decorators the users may not have installed
86+
# *.so files and folder "_C" may not exist when the cpp extensions are not compiled
87+
excludes = "|".join(
88+
[
89+
"(^(monai.handlers))",
90+
"(^(monai.bundle))",
91+
"(^(monai.fl))",
92+
"((\\.so)$)",
93+
"(^(monai._C))",
94+
"(.*(__main__)$)",
95+
"(.*(video_dataset)$)",
96+
"(.*(nnunet).*$)",
97+
]
10098
)
10199

100+
# load directory modules only, skip loading individual files
101+
load_submodules(sys.modules[__name__], False, exclude_pattern=excludes)
102+
103+
# load all modules, this will trigger all export decorations
104+
load_submodules(sys.modules[__name__], True, exclude_pattern=excludes)
105+
102106
__all__ = [
103107
"apps",
104108
"auto3dseg",

monai/utils/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
get_package_version,
113113
get_torch_version_tuple,
114114
instantiate,
115+
load_submodules,
115116
look_up_option,
116117
min_version,
117118
optional_import,

monai/utils/module.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
import os
1717
import pdb
1818
import re
19+
import sys
1920
import warnings
2021
from collections.abc import Callable, Collection, Hashable, Mapping
2122
from functools import partial, wraps
2223
from importlib import import_module
24+
from pkgutil import walk_packages
2325
from pydoc import locate
2426
from re import match
25-
from types import FunctionType
27+
from types import FunctionType, ModuleType
2628
from typing import Any, Iterable, cast
2729

2830
import torch
@@ -168,6 +170,38 @@ def damerau_levenshtein_distance(s1: str, s2: str) -> int:
168170
return d[string_1_length - 1, string_2_length - 1]
169171

170172

173+
def load_submodules(
174+
basemod: ModuleType, load_all: bool = True, exclude_pattern: str = "(.*[tT]est.*)|(_.*)"
175+
) -> tuple[list[ModuleType], list[str]]:
176+
"""
177+
Traverse the source of the module structure starting with module `basemod`, loading all packages plus all files if
178+
`load_all` is True, excluding anything whose name matches `exclude_pattern`.
179+
"""
180+
submodules = []
181+
err_mod: list[str] = []
182+
for importer, name, is_pkg in walk_packages(
183+
basemod.__path__, prefix=basemod.__name__ + ".", onerror=err_mod.append
184+
):
185+
if (is_pkg or load_all) and name not in sys.modules and match(exclude_pattern, name) is None:
186+
try:
187+
mod = import_module(name)
188+
mod_spec = importer.find_spec(name) # type: ignore
189+
if mod_spec and mod_spec.loader:
190+
loader = mod_spec.loader
191+
loader.exec_module(mod)
192+
submodules.append(mod)
193+
except OptionalImportError:
194+
pass # could not import the optional deps., they are ignored
195+
except ImportError as e:
196+
msg = (
197+
"\nMultiple versions of MONAI may have been installed?\n"
198+
"Please see the installation guide: https://docs.monai.io/en/stable/installation.html\n"
199+
) # issue project-monai/monai#5193
200+
raise type(e)(f"{e}\n{msg}").with_traceback(e.__traceback__) from e # raise with modified message
201+
202+
return submodules, err_mod
203+
204+
171205
def instantiate(__path: str, __mode: str, **kwargs: Any) -> Any:
172206
"""
173207
Create an object instance or call a callable object from a class or function represented by ``_path``.

tests/min_tests.py

+6
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,12 @@ def run_testsuit():
232232

233233

234234
if __name__ == "__main__":
235+
# testing import submodules
236+
from monai.utils.module import load_submodules
237+
238+
_, err_mod = load_submodules(sys.modules["monai"], True)
239+
assert not err_mod, f"err_mod={err_mod} not empty"
240+
235241
# testing all modules
236242
test_runner = unittest.TextTestRunner(stream=sys.stdout, verbosity=2)
237243
result = test_runner.run(run_testsuit())

0 commit comments

Comments
 (0)