X Tutup
The Wayback Machine - https://web.archive.org/web/20210614144255/https://github.com/python/cpython/pull/972
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-29851: Have importlib.reload() raise ImportError if the module's spec is not found #972

Merged
merged 15 commits into from May 24, 2017

Conversation

@garvitdelhi
Copy link
Contributor

@garvitdelhi garvitdelhi commented Apr 3, 2017

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

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

@mention-bot mention-bot commented Apr 3, 2017

@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.

@DimitrisJim
Copy link
Contributor

@DimitrisJim DimitrisJim commented Apr 3, 2017

Hi @garvitdelhi, I think this might also require tests to verify the change.

@serhiy-storchaka serhiy-storchaka requested a review from brettcannon Apr 3, 2017
@brettcannon brettcannon changed the title bpo-29851: Raising an ImportError if module not found bpo-29851: Have importlib.reload() raise ImportError if the module's spec is not found Apr 3, 2017
Copy link
Member

@brettcannon brettcannon left a comment

Thanks for the patch, but tests are needed for this.

@@ -164,6 +164,9 @@ def reload(module):
pkgpath = None
target = module
spec = module.__spec__ = _bootstrap._find_spec(name, pkgpath, target)
if spec is None:
msg = "module {} not found in the path specified"

This comment has been minimized.

@brettcannon

brettcannon Apr 3, 2017
Member

I would change this message to "spec not found for the module {!r}".

@brettcannon brettcannon self-assigned this Apr 5, 2017
@@ -164,6 +164,9 @@ def reload(module):
pkgpath = None
target = module
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)

This comment has been minimized.

@brettcannon

brettcannon Apr 5, 2017
Member

As mentioned in the issue, this should probably be ModuleNotFoundError.

Raising ModuleNotFounderror instead of ImportError
Changing error text
@garvitdelhi
Copy link
Contributor Author

@garvitdelhi garvitdelhi commented Apr 6, 2017

Hi, I am a little busy and hence will by writing tests by the weekend.

@garvitdelhi
Copy link
Contributor Author

@garvitdelhi garvitdelhi commented Apr 16, 2017

Hi, I tried writing tests but I am facing a problem.

I tried this:

moduleName = 'testmodule'
path = os.getcwd() + '/testmodule.py'
spec = importlib.util.spec_from_file_location(moduleName, path)
module_test = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module_test)
sys.modules[moduleName] = module_test
with self.assertRaises(ModuleNotFoundError):
    importlib.reload(sys.modules[moduleName])

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"

@ncoghlan
Copy link
Contributor

@ncoghlan ncoghlan commented Apr 17, 2017

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 tests.test_importlib.test.api: https://github.com/python/cpython/blob/master/Lib/test/test_importlib/test_api.py#L198

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:

>>> import types
>>> import importlib
>>> importlib.reload(types)
<module 'types' from '/usr/lib64/python3.5/types.py'>
>>> import sys
>>> types.__name__ = "not_a_module"
>>> types.__spec__ = None
>>> sys.modules[types.__name__] = types
>>> importlib.reload(types)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib64/python3.5/importlib/__init__.py", line 166, in reload
    _bootstrap._exec(spec, module)
  File "<frozen importlib._bootstrap>", line 607, in _exec
AttributeError: 'NoneType' object has no attribute 'name'
Copy link
Member

@berkerpeksag berkerpeksag left a comment

Please add an entry to Misc/NEWS (also include "Patch by Your Name.") Thanks!

@@ -164,6 +164,9 @@ def reload(module):
pkgpath = None
target = module
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)

This comment has been minimized.

@berkerpeksag

berkerpeksag Apr 19, 2017
Member

You can use an f-string here:

raise ModuleNotFoundError(f"spec not found for the module {name!r}", name=name)
@@ -145,6 +146,15 @@ def test_reload(self):
importlib.reload(module)
self.assertIs(ex_class, module.Example)

def test_reload_faliure(self):
'''Test that reload throws ModuleNotFounderror if reloading'''

This comment has been minimized.

@berkerpeksag

berkerpeksag Apr 19, 2017
Member

"""...""" 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.

This comment has been minimized.

@brettcannon

brettcannon Apr 28, 2017
Member

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'

This comment has been minimized.

@berkerpeksag

berkerpeksag Apr 19, 2017
Member

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)

This comment has been minimized.

@brettcannon

brettcannon May 1, 2017
Member

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
@garvitdelhi
Copy link
Contributor Author

@garvitdelhi garvitdelhi commented May 7, 2017

Hey sorry for the delay was busy in relocating, back to contributions now :)

Copy link
Member

@brettcannon brettcannon left a 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.

@@ -145,6 +145,21 @@ def test_reload(self):
importlib.reload(module)
self.assertIs(ex_class, module.Example)

def test_reload_faliure(self):

This comment has been minimized.

@brettcannon

brettcannon May 8, 2017
Member

Tests for reloading should be in test.test_importlib.test_api.

.gitignore Outdated
@@ -99,3 +99,4 @@ Tools/ssl/amd64
Tools/ssl/win32
.vs/
.vscode/
.idea/

This comment has been minimized.

@serhiy-storchaka

serhiy-storchaka May 9, 2017
Member

This change doesn't look related.

This comment has been minimized.

@garvitdelhi

garvitdelhi May 9, 2017
Author Contributor

will make a different issue for this removing this as of now

This comment has been minimized.

@berkerpeksag

berkerpeksag May 10, 2017
Member

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.

This comment has been minimized.

@garvitdelhi

garvitdelhi May 10, 2017
Author Contributor

Thank you for pointing this out almost forgot about global git ignores.

Copy link
Member

@brettcannon brettcannon left a 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)

This comment has been minimized.

@brettcannon

brettcannon May 9, 2017
Member

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

This comment has been minimized.

@brettcannon

brettcannon May 9, 2017
Member

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.

This comment has been minimized.

@garvitdelhi

garvitdelhi May 14, 2017
Author Contributor

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

@codecov
Copy link

@codecov codecov bot commented May 14, 2017

Codecov Report

Merging #972 into master will decrease coverage by 1.02%.
The diff coverage is 88.88%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
Lib/test/test_importlib/test_api.py 98.73% <100%> (+0.02%) ⬆️
Lib/importlib/__init__.py 86.13% <50%> (+1.29%) ⬆️
Lib/idlelib/idle_test/test_help_about.py 8.21% <0%> (-88.84%) ⬇️
Lib/idlelib/idle_test/mock_idle.py 51.85% <0%> (-33.34%) ⬇️
Lib/idlelib/textview.py 17.85% <0%> (-8.07%) ⬇️
Lib/idlelib/help_about.py 17.7% <0%> (-6.21%) ⬇️
Lib/sre_compile.py 84.06% <0%> (-4.53%) ⬇️
Lib/idlelib/idle_test/test_textview.py 2.94% <0%> (-1.61%) ⬇️
Lib/test/libregrtest/cmdline.py 93.18% <0%> (-1.4%) ⬇️
Lib/test/test_dict.py 98.8% <0%> (-0.38%) ⬇️
... and 116 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 211a392...4f528ef. Read the comment docs.

@brettcannon brettcannon changed the title bpo-29851: Have importlib.reload() raise ImportError if the module's spec is not found [WIP] bpo-29851: Have importlib.reload() raise ImportError if the module's spec is not found May 16, 2017
@brettcannon
Copy link
Member

@brettcannon brettcannon commented May 16, 2017

@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
@garvitdelhi garvitdelhi changed the title [WIP] bpo-29851: Have importlib.reload() raise ImportError if the module's spec is not found bpo-29851: Have importlib.reload() raise ImportError if the module's spec is not found May 17, 2017
@garvitdelhi
Copy link
Contributor Author

@garvitdelhi garvitdelhi commented May 17, 2017

@brettcannon done :)

@brettcannon
Copy link
Member

@brettcannon brettcannon commented May 23, 2017

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.

@brettcannon brettcannon merged commit 9498782 into python:master May 24, 2017
2 of 3 checks passed
2 of 3 checks passed
continuous-integration/appveyor/pr AppVeyor build failed
Details
@bedevere-bot
bedevere/issue-number Issue number 29851 found.
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
@brettcannon
Copy link
Member

@brettcannon brettcannon commented May 24, 2017

Thanks for PR, @garvitdelhi and being patient with the whole process!

mlouielu added a commit to mlouielu/cpython that referenced this pull request Jun 15, 2017
@VKTB
Copy link

@VKTB VKTB commented Aug 11, 2020

@garvitdelhi I know that this contribution is from a while ago but I am dynamically loading my own modules and get a ModuleNotFoundError: spec not found for the module 'a' error message when I call importlib.reload(sys.modules['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_a

When I inspect the module in debugging mode, I can see that it has its own ModuleSpec (see screenshot below), so I do not quite understand why it is raising the ImportError that was introduced as part of this PR. I also tried module_a.__spec__ = spec before adding the module to sys.modules just to be safe that spec is added to the module, but no luck. Any ideas?

image

@brettcannon
Copy link
Member

@brettcannon brettcannon commented Aug 11, 2020

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
9 participants
X Tutup