From 811c137b3fb950ec002f90d75321a39a64eb1fce Mon Sep 17 00:00:00 2001 From: yoarajota Date: Thu, 17 Sep 2026 11:21:37 -0300 Subject: [PATCH] Fix is_installed/get_installed_path crash on dotted package names --- mmengine/utils/package_utils.py | 18 +++++++++++++++--- tests/test_utils/test_package_utils.py | 7 +++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/mmengine/utils/package_utils.py b/mmengine/utils/package_utils.py index 8169a75b9d..d99a5fd109 100644 --- a/mmengine/utils/package_utils.py +++ b/mmengine/utils/package_utils.py @@ -12,8 +12,14 @@ def is_installed(package: str) -> bool: """ import importlib.util - # First check if it's an importable module - spec = importlib.util.find_spec(package) + # First check if it's an importable module. For a dotted name (e.g. + # `zope.interface`), `find_spec` imports the parent package to resolve + # it and raises `ModuleNotFoundError` instead of returning None if the + # parent isn't importable. + try: + spec = importlib.util.find_spec(package) + except ModuleNotFoundError: + spec = None if spec is not None and spec.origin is not None: return True @@ -75,7 +81,13 @@ def package2module(package: str) -> str: # The importable module name usually matches the package name. Probing the # import machinery first also covers editable installs, whose # `top_level.txt` may be absent even though the module is importable. - if importlib.util.find_spec(package) is not None: + # As above, `find_spec` raises rather than returning None when a dotted + # name's parent isn't importable. + try: + found = importlib.util.find_spec(package) is not None + except ModuleNotFoundError: + found = False + if found: return package # Distribution name differs from the module name (e.g. `mmcv-full` -> diff --git a/tests/test_utils/test_package_utils.py b/tests/test_utils/test_package_utils.py index 276d514dfc..c99db0336a 100644 --- a/tests/test_utils/test_package_utils.py +++ b/tests/test_utils/test_package_utils.py @@ -26,6 +26,10 @@ def test_is_installed(): # Should work on both distribution and module name assert is_installed('pillow') is True and is_installed('PIL') is True + # `find_spec` imports the leading component of a dotted name to resolve + # it, so a missing leading component must not raise. + assert is_installed('nonexistentnamespace12345.submodule') is False + def test_get_install_path(): # TODO: Windows CI may failed in unknown reason. Skip check the value @@ -41,3 +45,6 @@ def test_get_install_path(): with pytest.raises(PackageNotFoundError): get_installed_path('unknown') + + with pytest.raises(PackageNotFoundError): + get_installed_path('nonexistentnamespace12345.submodule')