bpo-29851: Have importlib.reload() raise ImportError if the module's spec is not found#972
Conversation
if _bootstrap._find_spec() return None, an ImportError is raised stating: module could not be found in the path specified
|
@garvitdelhi, thanks for your PR! By analyzing the history of the files in this pull request, we identified @brettcannon, @ericsnowcurrently and @ncoghlan to be potential reviewers. |
|
Hi @garvitdelhi, I think this might also require tests to verify the change. |
brettcannon
left a comment
There was a problem hiding this comment.
Thanks for the patch, but tests are needed for this.
| target = module | ||
| spec = module.__spec__ = _bootstrap._find_spec(name, pkgpath, target) | ||
| if spec is None: | ||
| msg = "module {} not found in the path specified" |
There was a problem hiding this comment.
I would change this message to "spec not found for the module {!r}".
| spec = module.__spec__ = _bootstrap._find_spec(name, pkgpath, target) | ||
| if spec is None: | ||
| msg = "module {} not found in the path specified" | ||
| raise ImportError(msg.format(name), name=name) |
There was a problem hiding this comment.
As mentioned in the issue, this should probably be ModuleNotFoundError.
Raising ModuleNotFounderror instead of ImportError Changing error text
|
Hi, I am a little busy and hence will by writing tests by the weekend. |
|
Hi, I tried writing tests but I am facing a problem. I tried this: The problem here is I have testmodule.py within the same directory i.e Lib/test/test_importlib/extension/ but since tests are run in a sandbox environment I don't have access to above directory. Also I can't load the file itself since it has a relative import 'abc' and I get an error saying: "ImportError: attempted relative import with no known parent package" |
|
Hi @garvitdelhi, for cases like this, it's worth looking through the existing test cases to see if there are some existing examples you can copy, as we tend to have lots of odd helpers in the test suite that let us do things that we expressly tell end users not to do (since we're deliberately inducing error states so we can check for expected exceptions). For this PR, it turns out the most relevant examples are in As with the examples there, the simplest way to get a module that can't be reloaded is to take a normal module and corrupt its metadata, as in something like: |
berkerpeksag
left a comment
There was a problem hiding this comment.
Please add an entry to Misc/NEWS (also include "Patch by Your Name.") Thanks!
| spec = module.__spec__ = _bootstrap._find_spec(name, pkgpath, target) | ||
| if spec is None: | ||
| msg = "spec not found for the module {!r}" | ||
| raise ModuleNotFoundError(msg.format(name), name=name) |
There was a problem hiding this comment.
You can use an f-string here:
raise ModuleNotFoundError(f"spec not found for the module {name!r}", name=name)| self.assertIs(ex_class, module.Example) | ||
|
|
||
| def test_reload_faliure(self): | ||
| '''Test that reload throws ModuleNotFounderror if reloading''' |
There was a problem hiding this comment.
"""...""" supports multiline comments so you can just do:
'''Test that reload throws ModuleNotFounderror if reloading
a module whose spec cannot be found'''I'll let Brett or Nick comment on the actual wording of docstring.
There was a problem hiding this comment.
Actually the whole module is a mess when it comes to docstrings.
Use a # comment instead of a docstring, and it can take more than a single line. Also make sure to end it in a period.
| def test_reload_faliure(self): | ||
| '''Test that reload throws ModuleNotFounderror if reloading''' | ||
| '''a module whose spec cannot be found''' | ||
| datetime.__name__ = 'destroyed_datetime' |
There was a problem hiding this comment.
We probably shouldn't directly mess with a stdlib module and sys.modules and use a helper like Nick said. Untested, but I think we need to add something like the following snippet to Lib/test/test_importlib/test_api.py:
with support.CleanImport('types'):
module = self.init.import_module('types')
module.__name__ = 'broken_types'
module.__spec__ = None
sys.modules[module.__name__] = module # TODO: Do we need to cleanup sys.modules?
with self.assertRaises(ModuleNotFoundError):
self.init.reload(module)There was a problem hiding this comment.
All the helpers for importlib-related tests can be found in https://github.com/python/cpython/blob/master/Lib/test/test_importlib/util.py. In this instance you probably want both uncache() and temp_module().
using a temperory modulue and destroying it's meta data and trying to reload to raise ModuleNotFoundError exception
|
Hey sorry for the delay was busy in relocating, back to contributions now :) |
brettcannon
left a comment
There was a problem hiding this comment.
A versionchanged note should be added to the docs on top of the relocation of the tests. And don't forget to add yourself to Misc/ACKS.
| importlib.reload(module) | ||
| self.assertIs(ex_class, module.Example) | ||
|
|
||
| def test_reload_faliure(self): |
There was a problem hiding this comment.
Tests for reloading should be in test.test_importlib.test_api.
| Tools/ssl/win32 | ||
| .vs/ | ||
| .vscode/ | ||
| .idea/ |
There was a problem hiding this comment.
This change doesn't look related.
There was a problem hiding this comment.
will make a different issue for this removing this as of now
There was a problem hiding this comment.
Note that you can use a global .gitignore file to ignore these IDE specific files. We've rejected a few PRs because the global .gitignore solution is the preferred way to do it.
There was a problem hiding this comment.
Thank you for pointing this out almost forgot about global git ignores.
brettcannon
left a comment
There was a problem hiding this comment.
This is also going to require a versionchanged mention in the docs.
| name = 'spam' | ||
| subname = 'ham' | ||
| with test_util.temp_module(name, pkg=True) as pkg_dir: | ||
| fullname, _ = test_util.submodule(name, subname, pkg_dir) |
There was a problem hiding this comment.
Is there a specific reason to test a submodule?
| module = self.init.import_module(fullname) | ||
| module.__spec__ = None | ||
| module.__name__ = 'destroyed_spam' | ||
| sys.modules[module.__name__] = module |
There was a problem hiding this comment.
Would the test still work if you simply set module.__spec__ = None and left the name alone and didn't insert a new module? Currently you end up leaving a dead module in sys.modules under the name destroyed_spam.
There was a problem hiding this comment.
Not changing the name won't work. If I don't modify the name, the method _find_spec inside importlib/_bootstrap.py tries to find spec from three finder from sys.meta_path: _frozen_importlib.BuiltinImporter, _frozen_importlib.FrozenImporter and _frozen_importlib_external.PathFinder respectively. But since I have not changed the name and the module by that filename still exists PathFinder can find the spec (reload the spec) from the file. Hence I do need to change the name and put it explicitly in sys.modules
Submodule isin't needed to reproduce the error
Codecov Report
@@ Coverage Diff @@
## master #972 +/- ##
==========================================
- Coverage 83.7% 82.67% -1.03%
==========================================
Files 1371 1432 +61
Lines 346499 353357 +6858
==========================================
+ Hits 290026 292131 +2105
- Misses 56473 61226 +4753
Continue to review full report at Codecov.
|
|
@garvitdelhi when you think you have addressed all the requested changes, could you remove the "[WIP]" part of the title that I added and mention me in a comment so this PR shows up as a GitHub notification for me? |
Adding version change to the documentation. Uncaching the module so that we cleanup the mess made in sys.modules
|
@brettcannon done :) |
|
I made some of my own changes to the PR so I'm waiting to make sure I didn't break anything. Once Travis passes I will commit. |
|
Thanks for PR, @garvitdelhi and being patient with the whole process! |
|
@garvitdelhi I know that this contribution is from a while ago but I am dynamically loading my own modules and get a I am loading the module using this logic:: import importlib.util
module_name = 'a'
path = os.getcwd() + '/a.py'
spec = importlib.util.spec_from_file_location(module_name, path)
module_a = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module_a)
sys.modules[module_name ] = module_aWhen I inspect the module in debugging mode, I can see that it has its own |
|
@VKTB this isn't the best place to be asking this. If you're after help you can try asking on python-list, else if you think it's a bug then please open one at bugs.python.org. |

if _bootstrap._find_spec() return None, an ImportError is raised
stating: module could not be found in the path specified