diff --git a/src/pythonjsonlogger/utils.py b/src/pythonjsonlogger/utils.py index d810a13..c2ea8fd 100644 --- a/src/pythonjsonlogger/utils.py +++ b/src/pythonjsonlogger/utils.py @@ -32,7 +32,14 @@ def package_is_available( Returns: If the package is available for import. """ - available = importlib.util.find_spec(name) is not None + try: + available = importlib.util.find_spec(name) is not None + except ModuleNotFoundError as exc: + # A dotted import also fails when one of its parent packages is missing. + # Preserve errors from unrelated dependencies imported by an existing parent. + if exc.name is None or not (name == exc.name or name.startswith(exc.name + ".")): + raise + available = False if not available and throw_error: raise MissingPackageError(name, extras_name) diff --git a/tests/test_missing.py b/tests/test_missing.py index 0878014..ba43d65 100644 --- a/tests/test_missing.py +++ b/tests/test_missing.py @@ -65,3 +65,23 @@ def test_msgspec_import_error(): with pytest.raises(MissingPackageError, match="msgspec"): import pythonjsonlogger.msgspec return + + +@pytest.mark.parametrize("name", [MISSING_PACKAGE_NAME + ".child", "json.missing.child"]) +def test_dotted_package_not_available(name): + assert not package_is_available(name) + with pytest.raises(MissingPackageError): + package_is_available(name, throw_error=True) + + +def test_existing_dotted_package_is_available(): + assert package_is_available("json.decoder") + + +def test_missing_dependency_in_parent_is_not_hidden(tmp_path, monkeypatch): + package = tmp_path / "broken_parent_for_json_logger_test" + package.mkdir() + (package / "__init__.py").write_text("import missing_internal_dependency_for_test\n") + monkeypatch.syspath_prepend(str(tmp_path)) + with pytest.raises(ModuleNotFoundError, match="missing_internal_dependency_for_test"): + package_is_available("broken_parent_for_json_logger_test.child")