Merge pull request #4610 from eprigorodov/feature-4602-namespace-path

This commit is contained in:
David Lord 2022-06-06 09:26:53 -07:00 committed by GitHub
commit c7f2ab8e7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 28 deletions

View File

@ -8,6 +8,8 @@ Unreleased
- Inline some optional imports that are only used for certain CLI - Inline some optional imports that are only used for certain CLI
commands. :pr:`4606` commands. :pr:`4606`
- Relax type annotation for ``after_request`` functions. :issue:`4600` - Relax type annotation for ``after_request`` functions. :issue:`4600`
- ``instance_path`` for namespace packages uses the path closest to
the imported submodule. :issue:`4600`
Version 2.1.2 Version 2.1.2

View File

@ -1,5 +1,6 @@
import importlib.util import importlib.util
import os import os
import pathlib
import pkgutil import pkgutil
import sys import sys
import typing as t import typing as t
@ -780,30 +781,55 @@ def _matching_loader_thinks_module_is_package(loader, mod_name):
) )
def _find_package_path(root_mod_name): def _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool:
"""Find the path that contains the package or module.""" # Path.is_relative_to doesn't exist until Python 3.9
try: try:
spec = importlib.util.find_spec(root_mod_name) path.relative_to(base)
return True
except ValueError:
return False
if spec is None:
def _find_package_path(import_name):
"""Find the path that contains the package or module."""
root_mod_name, _, _ = import_name.partition(".")
try:
root_spec = importlib.util.find_spec(root_mod_name)
if root_spec is None:
raise ValueError("not found") raise ValueError("not found")
# ImportError: the machinery told us it does not exist # ImportError: the machinery told us it does not exist
# ValueError: # ValueError:
# - the module name was invalid # - the module name was invalid
# - the module name is __main__ # - the module name is __main__
# - *we* raised `ValueError` due to `spec` being `None` # - *we* raised `ValueError` due to `root_spec` being `None`
except (ImportError, ValueError): except (ImportError, ValueError):
pass # handled below pass # handled below
else: else:
# namespace package # namespace package
if spec.origin in {"namespace", None}: if root_spec.origin in {"namespace", None}:
return os.path.dirname(next(iter(spec.submodule_search_locations))) package_spec = importlib.util.find_spec(import_name)
if package_spec is not None and package_spec.submodule_search_locations:
# Pick the path in the namespace that contains the submodule.
package_path = pathlib.Path(
os.path.commonpath(package_spec.submodule_search_locations)
)
search_locations = (
location
for location in root_spec.submodule_search_locations
if _path_is_relative_to(package_path, location)
)
else:
# Pick the first path.
search_locations = iter(root_spec.submodule_search_locations)
return os.path.dirname(next(search_locations))
# a package (with __init__.py) # a package (with __init__.py)
elif spec.submodule_search_locations: elif root_spec.submodule_search_locations:
return os.path.dirname(os.path.dirname(spec.origin)) return os.path.dirname(os.path.dirname(root_spec.origin))
# just a normal module # just a normal module
else: else:
return os.path.dirname(spec.origin) return os.path.dirname(root_spec.origin)
# we were unable to find the `package_path` using PEP 451 loaders # we were unable to find the `package_path` using PEP 451 loaders
loader = pkgutil.get_loader(root_mod_name) loader = pkgutil.get_loader(root_mod_name)
@ -845,12 +871,11 @@ def find_package(import_name: str):
for import. If the package is not installed, it's assumed that the for import. If the package is not installed, it's assumed that the
package was imported from the current working directory. package was imported from the current working directory.
""" """
root_mod_name, _, _ = import_name.partition(".") package_path = _find_package_path(import_name)
package_path = _find_package_path(root_mod_name)
py_prefix = os.path.abspath(sys.prefix) py_prefix = os.path.abspath(sys.prefix)
# installed to the system # installed to the system
if package_path.startswith(py_prefix): if _path_is_relative_to(pathlib.PurePath(package_path), py_prefix):
return py_prefix, package_path return py_prefix, package_path
site_parent, site_folder = os.path.split(package_path) site_parent, site_folder = os.path.split(package_path)

View File

@ -1,4 +1,3 @@
import os
import sys import sys
import pytest import pytest
@ -15,19 +14,6 @@ def test_explicit_instance_paths(modules_tmpdir):
assert app.instance_path == str(modules_tmpdir) assert app.instance_path == str(modules_tmpdir)
@pytest.mark.xfail(reason="weird interaction with tox")
def test_main_module_paths(modules_tmpdir, purge_module):
app = modules_tmpdir.join("main_app.py")
app.write('import flask\n\napp = flask.Flask("__main__")')
purge_module("main_app")
from main_app import app
here = os.path.abspath(os.getcwd())
assert app.instance_path == os.path.join(here, "instance")
@pytest.mark.xfail(reason="weird interaction with tox")
def test_uninstalled_module_paths(modules_tmpdir, purge_module): def test_uninstalled_module_paths(modules_tmpdir, purge_module):
app = modules_tmpdir.join("config_module_app.py").write( app = modules_tmpdir.join("config_module_app.py").write(
"import os\n" "import os\n"
@ -42,7 +28,6 @@ def test_uninstalled_module_paths(modules_tmpdir, purge_module):
assert app.instance_path == str(modules_tmpdir.join("instance")) assert app.instance_path == str(modules_tmpdir.join("instance"))
@pytest.mark.xfail(reason="weird interaction with tox")
def test_uninstalled_package_paths(modules_tmpdir, purge_module): def test_uninstalled_package_paths(modules_tmpdir, purge_module):
app = modules_tmpdir.mkdir("config_package_app") app = modules_tmpdir.mkdir("config_package_app")
init = app.join("__init__.py") init = app.join("__init__.py")
@ -59,6 +44,25 @@ def test_uninstalled_package_paths(modules_tmpdir, purge_module):
assert app.instance_path == str(modules_tmpdir.join("instance")) assert app.instance_path == str(modules_tmpdir.join("instance"))
def test_uninstalled_namespace_paths(tmpdir, monkeypatch, purge_module):
def create_namespace(package):
project = tmpdir.join(f"project-{package}")
monkeypatch.syspath_prepend(str(project))
project.join("namespace").join(package).join("__init__.py").write(
"import flask\napp = flask.Flask(__name__)\n", ensure=True
)
return project
_ = create_namespace("package1")
project2 = create_namespace("package2")
purge_module("namespace.package2")
purge_module("namespace")
from namespace.package2 import app
assert app.instance_path == str(project2.join("instance"))
def test_installed_module_paths( def test_installed_module_paths(
modules_tmpdir, modules_tmpdir_prefix, purge_module, site_packages, limit_loader modules_tmpdir, modules_tmpdir_prefix, purge_module, site_packages, limit_loader
): ):

View File

@ -9,6 +9,7 @@ envlist =
skip_missing_interpreters = true skip_missing_interpreters = true
[testenv] [testenv]
envtmpdir = {toxworkdir}/tmp/{envname}
deps = deps =
-r requirements/tests.txt -r requirements/tests.txt
min: -r requirements/tests-pallets-min.txt min: -r requirements/tests-pallets-min.txt