Repository navigation
Improve ImportString error when internal imports fail - #12740
Conversation
|
please review |
CodSpeed Performance ReportMerging this PR will improve performance by 6.66%Comparing Summary
Performance Changes
|
|
Hi @Viicos , just a quick follow-up in case this slipped through. No rush at all, but happy to make any adjustments or add tests if needed. |
| @@ -115,8 +121,7 @@ def _import_string_logic(dotted_path: str) -> Any: | |||
There was a problem hiding this comment.
Is there a possible bug here, where if len(components) == 2 we already had a :attribute which we're discarding and ignoring if line 120 returns?
There was a problem hiding this comment.
Good catch, thanks for pointing this out.
You’re right, in the case where the input already includes a :attribute, the fallback path here could incorrectly discard it. That wasn’t intentional.
I’ll adjust the logic to preserve the original attribute when : is already present and add a test to cover this case.
There was a problem hiding this comment.
No worries, I think this bug existed before your diff, however would be good to fix as part of the adjustments here.
There was a problem hiding this comment.
Hello again @davidhewitt !
I just made changes in the code snippet that should cover the case you pointed out and also added a test for that as well.
Let me know I need to adjust something further.
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, some suggestions to simplify / refine.
| raise ImportError(f"Import strings should have at most one ':'; received {dotted_path!r}") | ||
|
|
||
|
|
||
| has_explicit_attr = len(components) == 2 |
There was a problem hiding this comment.
I would prefer to write this as
| has_explicit_attr = len(components) == 2 | |
| attribute = None: | |
| if len(components) == 2: | |
| attribute = components[1] |
| if len(components) > 1: | ||
| attribute = components[1] |
There was a problem hiding this comment.
With the other change to set attribute above,
| if len(components) > 1: | |
| attribute = components[1] | |
| if attribute is not None: |
| if missing and not (missing == module_path or missing.startswith(module_path + ".")): | ||
| raise | ||
|
|
||
| if not has_explicit_attr and '.' in module_path: |
There was a problem hiding this comment.
With the other change to set attribute above,
| if not has_explicit_attr and '.' in module_path: | |
| if attribute is None and '.' in module_path: |
| return _import_string_logic(f'{maybe_module_path}:{maybe_attribute}') | ||
| except ImportError: | ||
| pass | ||
| raise ImportError(f'No module named {module_path!r}') from e |
There was a problem hiding this comment.
I think this error is the real cause of your reported bug, let's just remove it and let e say what's going on.
| raise ImportError(f'No module named {module_path!r}') from e |
| # If the missing module is NOT the module we tried to import (or its submodule), | ||
| # then it's an internal import failure (missing dependency) and we must not mask it. | ||
| missing = getattr(e, "name", None) | ||
| if missing and not (missing == module_path or missing.startswith(module_path + ".")): | ||
| raise |
There was a problem hiding this comment.
I think if the raise ImportError(...) on line 125 is removed as I suggest, we won't need this.
| # If the missing module is NOT the module we tried to import (or its submodule), | |
| # then it's an internal import failure (missing dependency) and we must not mask it. | |
| missing = getattr(e, "name", None) | |
| if missing and not (missing == module_path or missing.startswith(module_path + ".")): | |
| raise |
| with pytest.raises(ValidationError) as exc_info: | ||
| adapter.validate_python('my_module.MyClass') | ||
|
|
||
| msg = str(exc_info.value) | ||
| assert "definitely_missing_dep_xyz" in msg | ||
| assert "my_module" in msg | ||
| # ensure we don't incorrectly claim the object path is missing | ||
| assert "my_module.MyClass" not in msg or "No module named 'my_module.MyClass'" not in msg |
There was a problem hiding this comment.
I think asserting the underlying message should be good enough
| with pytest.raises(ValidationError) as exc_info: | |
| adapter.validate_python('my_module.MyClass') | |
| msg = str(exc_info.value) | |
| assert "definitely_missing_dep_xyz" in msg | |
| assert "my_module" in msg | |
| # ensure we don't incorrectly claim the object path is missing | |
| assert "my_module.MyClass" not in msg or "No module named 'my_module.MyClass'" not in msg | |
| with pytest.raises(ValidationError, match="No module named 'definitely_missing_dep_xyz'") as exc_info: | |
| adapter.validate_python('my_module.MyClass') |
| def test_import_string_explicit_colon_does_not_try_dot_fallback(): | ||
| # Regression test: if the input already contains ':attr', we should NOT try | ||
| # to reinterpret dots as module/attribute splits (which could accidentally | ||
| # create an invalid import string containing two colons). | ||
| with pytest.raises(ModuleNotFoundError): | ||
| _import_string_logic("does.not.exist:Thing") |
There was a problem hiding this comment.
Please use a TypeAdapter in this test too, rather than the internal function. I also think we should probably use an import string like 'collections.defaultdict:get' which a buggy implementation might actually resolve, rather than a truly nonexistent path.
The removal of custom error masking now allows Python's import system to provide more detailed error messages (e.g., "'os' is not a package" instead of just "No module named"). Updated tests to match the new, more informative error output.
|
Hello @davidhewitt. I've applied all your suggested changes:
All tests passing locally (949 passed). The error messages are now more informative. They include context from Python's import system like Ready for another review when you have time. Thanks again for your feedback! |
|
Thanks, looks good, please fix the issues with lint and CI and then this ready to merge. |
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||
|
@davidhewitt all lint and CI issues are now fixed and all checks are passing. |
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, I am happy with this!
|
Thank you very much! |
Change Summary
What this PR does
Fixes #12715.
Improves the error handling of
ImportStringso that internal import failures(e.g. missing dependencies inside a module) are no longer masked as
ImportError: No module named ....If the missing module is not the requested import path (or its submodules),
the original
ModuleNotFoundErroris now correctly re-raised.Why
Previously,
ImportStringcould incorrectly report that a module did not exist,even when the real issue was a missing internal dependency. This made debugging
harder and hid the true cause of the failure.
Changes
ModuleNotFoundError.namerefers to the requested moduleTests
tests/test_types.pyRelated issue number
Checklist
Selected Reviewer: @Viicos