diff --git a/CHANGES.rst b/CHANGES.rst index e1a3a9e0..c777f7d0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased requires upgrading to Werkzeug 0.15.5. :issue:`3249` - ``send_file`` url quotes the ":" and "/" characters for more compatible UTF-8 filename support in some browsers. :issue:`3074` +- Fixes for PEP451 import loaders and pytest 5.x. :issue:`3275` Version 1.0.3 diff --git a/docs/tutorial/tests.rst b/docs/tutorial/tests.rst index 565450f9..218817ad 100644 --- a/docs/tutorial/tests.rst +++ b/docs/tutorial/tests.rst @@ -188,7 +188,7 @@ should be closed. with pytest.raises(sqlite3.ProgrammingError) as e: db.execute('SELECT 1') - assert 'closed' in str(e) + assert 'closed' in str(e.value) The ``init-db`` command should call the ``init_db`` function and output a message. diff --git a/examples/tutorial/tests/test_db.py b/examples/tutorial/tests/test_db.py index 99c46d04..4c112b19 100644 --- a/examples/tutorial/tests/test_db.py +++ b/examples/tutorial/tests/test_db.py @@ -12,7 +12,7 @@ def test_get_close_db(app): with pytest.raises(sqlite3.ProgrammingError) as e: db.execute('SELECT 1') - assert 'closed' in str(e) + assert 'closed' in str(e.value) def test_init_db_command(runner, monkeypatch): diff --git a/flask/helpers.py b/flask/helpers.py index f1eaa8e4..45c7909a 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -789,19 +789,38 @@ def _matching_loader_thinks_module_is_package(loader, mod_name): loader.__class__.__name__) -def find_package(import_name): - """Finds a package and returns the prefix (or None if the package is - not installed) as well as the folder that contains the package or - module as a tuple. The package path returned is the module that would - have to be added to the pythonpath in order to make it possible to - import the module. The prefix is the path below which a UNIX like - folder structure exists (lib, share etc.). - """ - root_mod_name = import_name.split('.')[0] +def _find_package_path(root_mod_name): + """Find the path where the module's root exists in""" + if sys.version_info >= (3, 4): + import importlib.util + + try: + spec = importlib.util.find_spec(root_mod_name) + if spec is None: + raise ValueError("not found") + # ImportError: the machinery told us it does not exist + # ValueError: + # - the module name was invalid + # - the module name is __main__ + # - *we* raised `ValueError` due to `spec` being `None` + except (ImportError, ValueError): + pass # handled below + else: + # namespace package + if spec.origin in {"namespace", None}: + return os.path.dirname(next(iter(spec.submodule_search_locations))) + # a package (with __init__.py) + elif spec.submodule_search_locations: + return os.path.dirname(os.path.dirname(spec.origin)) + # just a normal module + else: + return os.path.dirname(spec.origin) + + # we were unable to find the `package_path` using PEP 451 loaders loader = pkgutil.get_loader(root_mod_name) - if loader is None or import_name == '__main__': + if loader is None or root_mod_name == '__main__': # import name is not found, or interactive/main module - package_path = os.getcwd() + return os.getcwd() else: # For .egg, zipimporter does not have get_filename until Python 2.7. if hasattr(loader, 'get_filename'): @@ -815,17 +834,29 @@ def find_package(import_name): # Google App Engine's HardenedModulesHook # # Fall back to imports. - __import__(import_name) - filename = sys.modules[import_name].__file__ + __import__(root_mod_name) + filename = sys.modules[root_mod_name].__file__ package_path = os.path.abspath(os.path.dirname(filename)) # In case the root module is a package we need to chop of the # rightmost part. This needs to go through a helper function # because of python 3.3 namespace packages. - if _matching_loader_thinks_module_is_package( - loader, root_mod_name): + if _matching_loader_thinks_module_is_package(loader, root_mod_name): package_path = os.path.dirname(package_path) + return package_path + + +def find_package(import_name): + """Finds a package and returns the prefix (or None if the package is + not installed) as well as the folder that contains the package or + module as a tuple. The package path returned is the module that would + have to be added to the pythonpath in order to make it possible to + import the module. The prefix is the path below which a UNIX like + folder structure exists (lib, share etc.). + """ + root_mod_name, _, _ = import_name.partition('.') + package_path = _find_package_path(root_mod_name) site_parent, site_folder = os.path.split(package_path) py_prefix = os.path.abspath(sys.prefix) if package_path.startswith(py_prefix): diff --git a/tests/test_basic.py b/tests/test_basic.py index 587781b3..6f813627 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1219,17 +1219,17 @@ def test_response_type_errors(): with pytest.raises(TypeError) as e: c.get('/none') - assert 'returned None' in str(e) + assert 'returned None' in str(e.value) with pytest.raises(TypeError) as e: c.get('/small_tuple') - assert 'tuple must have the form' in str(e) + assert 'tuple must have the form' in str(e.value) pytest.raises(TypeError, c.get, '/large_tuple') with pytest.raises(TypeError) as e: c.get('/bad_type') - assert 'it was a bool' in str(e) + assert 'it was a bool' in str(e.value) pytest.raises(TypeError, c.get, '/bad_wsgi') @@ -1622,7 +1622,7 @@ def test_debug_mode_complains_after_first_request(app, client): @app.route('/foo') def broken(): return 'Meh' - assert 'A setup function was called' in str(e) + assert 'A setup function was called' in str(e.value) app.debug = False @@ -1677,9 +1677,9 @@ def test_routing_redirect_debugging(app, client): with client: with pytest.raises(AssertionError) as e: client.post('/foo', data={}) - assert 'http://localhost/foo/' in str(e) + assert 'http://localhost/foo/' in str(e.value) assert ('Make sure to directly send ' - 'your POST-request to this URL') in str(e) + 'your POST-request to this URL') in str(e.value) rv = client.get('/foo', data={}, follow_redirects=True) assert rv.data == b'success' diff --git a/tests/test_helpers.py b/tests/test_helpers.py index d6a023e7..75c81059 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -468,8 +468,8 @@ class TestSendfile(object): def test_send_file_object_without_mimetype(self, app, req_ctx): with pytest.raises(ValueError) as excinfo: flask.send_file(StringIO("LOL")) - assert 'Unable to infer MIME-type' in str(excinfo) - assert 'no filename is available' in str(excinfo) + assert 'Unable to infer MIME-type' in str(excinfo.value) + assert 'no filename is available' in str(excinfo.value) flask.send_file(StringIO("LOL"), attachment_filename='filename')