instance_path for namespace packages uses path closest to submodule

This commit is contained in:
Evgeny Prigorodov 2022-05-26 21:12:36 +02:00 committed by David Lord
parent fb89745408
commit 88bcf78439
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
3 changed files with 48 additions and 12 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

@ -780,30 +780,46 @@ def _matching_loader_thinks_module_is_package(loader, mod_name):
) )
def _find_package_path(root_mod_name): def _find_package_path(import_name):
"""Find the path that contains the package or module.""" """Find the path that contains the package or module."""
try: root_mod_name, _, _ = import_name.partition(".")
spec = importlib.util.find_spec(root_mod_name)
if spec is None: 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 = os.path.commonpath(
package_spec.submodule_search_locations
)
search_locations = (
location
for location in root_spec.submodule_search_locations
if package_path.startswith(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,8 +861,7 @@ 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

View file

@ -59,6 +59,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
): ):