Skip to content

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

Merged
brettcannon merged 15 commits intopython:masterfrom
plusminushalf:fix-bpo-29851
May 24, 2017
Merged

bpo-29851: Have importlib.reload() raise ImportError if the module's spec is not found#972
brettcannon merged 15 commits intopython:masterfrom
plusminushalf:fix-bpo-29851

Conversation

@plusminushalf
Copy link
Copy Markdown
Contributor

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
Copy Markdown

@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
Copy Markdown
Contributor

DimitrisJim commented Apr 3, 2017

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

@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
Copy Markdown
Member

@brettcannon brettcannon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread Lib/importlib/__init__.py Outdated
target = module
spec = module.__spec__ = _bootstrap._find_spec(name, pkgpath, target)
if spec is None:
msg = "module {} not found in the path specified"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@brettcannon brettcannon self-assigned this Apr 5, 2017
Comment thread Lib/importlib/__init__.py Outdated
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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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

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

@plusminushalf
Copy link
Copy Markdown
Contributor Author

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
Copy Markdown
Contributor

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
Copy Markdown
Member

@berkerpeksag berkerpeksag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread Lib/importlib/__init__.py Outdated
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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'''
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

@brettcannon brettcannon Apr 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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

Copy link
Copy Markdown
Member

@brettcannon brettcannon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread .gitignore Outdated
Tools/ssl/win32
.vs/
.vscode/
.idea/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change doesn't look related.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

@brettcannon brettcannon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also going to require a versionchanged mention in the docs.

Comment thread Lib/test/test_importlib/test_api.py Outdated
name = 'spam'
subname = 'ham'
with test_util.temp_module(name, pkg=True) as pkg_dir:
fullname, _ = test_util.submodule(name, subname, pkg_dir)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason to test a submodule?

Comment thread Lib/test/test_importlib/test_api.py Outdated
module = self.init.import_module(fullname)
module.__spec__ = None
module.__name__ = 'destroyed_spam'
sys.modules[module.__name__] = module
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy Markdown

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
Copy Markdown
Member

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

@brettcannon done :)

@brettcannon
Copy link
Copy Markdown
Member

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
@brettcannon
Copy link
Copy Markdown
Member

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

@VKTB
Copy link
Copy Markdown

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
Copy Markdown
Member

@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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants