update
This commit is contained in:
@@ -10,59 +10,41 @@ Create a wheel that, when installed, will make the source package 'editable'
|
||||
*auxiliary build directory* or ``auxiliary_dir``.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
from contextlib import suppress
|
||||
from enum import Enum
|
||||
from inspect import cleandoc
|
||||
from itertools import chain
|
||||
from itertools import chain, starmap
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Iterable, Iterator, Mapping, Protocol, TypeVar, cast
|
||||
|
||||
from .. import (
|
||||
Command,
|
||||
_normalization,
|
||||
_path,
|
||||
errors,
|
||||
namespaces,
|
||||
)
|
||||
from .. import Command, _normalization, _path, errors, namespaces
|
||||
from .._path import StrPath
|
||||
from ..compat import py312
|
||||
from ..discovery import find_package_path
|
||||
from ..dist import Distribution
|
||||
from ..warnings import (
|
||||
InformationOnly,
|
||||
SetuptoolsDeprecationWarning,
|
||||
SetuptoolsWarning,
|
||||
)
|
||||
from ..warnings import InformationOnly, SetuptoolsDeprecationWarning, SetuptoolsWarning
|
||||
from .build import build as build_cls
|
||||
from .build_py import build_py as build_py_cls
|
||||
from .dist_info import dist_info as dist_info_cls
|
||||
from .egg_info import egg_info as egg_info_cls
|
||||
from .install import install as install_cls
|
||||
from .install_scripts import install_scripts as install_scripts_cls
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from wheel.wheelfile import WheelFile # noqa
|
||||
from typing_extensions import Self
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Protocol
|
||||
elif TYPE_CHECKING:
|
||||
from typing_extensions import Protocol
|
||||
else:
|
||||
from abc import ABC as Protocol
|
||||
from .._vendor.wheel.wheelfile import WheelFile
|
||||
|
||||
_Path = Union[str, Path]
|
||||
_P = TypeVar("_P", bound=_Path)
|
||||
_P = TypeVar("_P", bound=StrPath)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -79,7 +61,7 @@ class _EditableMode(Enum):
|
||||
COMPAT = "compat" # TODO: Remove `compat` after Dec/2022.
|
||||
|
||||
@classmethod
|
||||
def convert(cls, mode: Optional[str]) -> "_EditableMode":
|
||||
def convert(cls, mode: str | None) -> _EditableMode:
|
||||
if not mode:
|
||||
return _EditableMode.LENIENT # default
|
||||
|
||||
@@ -156,13 +138,14 @@ class editable_wheel(Command):
|
||||
self._create_wheel_file(bdist_wheel)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
# TODO: Fix false-positive [attr-defined] in typeshed
|
||||
project = self.distribution.name or self.distribution.get_name()
|
||||
_DebuggingTips.emit(project=project)
|
||||
raise
|
||||
|
||||
def _ensure_dist_info(self):
|
||||
if self.dist_info_dir is None:
|
||||
dist_info = self.reinitialize_command("dist_info")
|
||||
dist_info = cast(dist_info_cls, self.reinitialize_command("dist_info"))
|
||||
dist_info.output_dir = self.dist_dir
|
||||
dist_info.ensure_finalized()
|
||||
dist_info.run()
|
||||
@@ -181,13 +164,13 @@ class editable_wheel(Command):
|
||||
installer = _NamespaceInstaller(dist, installation_dir, pth_prefix, src_root)
|
||||
installer.install_namespaces()
|
||||
|
||||
def _find_egg_info_dir(self) -> Optional[str]:
|
||||
def _find_egg_info_dir(self) -> str | None:
|
||||
parent_dir = Path(self.dist_info_dir).parent if self.dist_info_dir else Path()
|
||||
candidates = map(str, parent_dir.glob("*.egg-info"))
|
||||
return next(candidates, None)
|
||||
|
||||
def _configure_build(
|
||||
self, name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
|
||||
self, name: str, unpacked_wheel: StrPath, build_lib: StrPath, tmp_dir: StrPath
|
||||
):
|
||||
"""Configure commands to behave in the following ways:
|
||||
|
||||
@@ -209,12 +192,18 @@ class editable_wheel(Command):
|
||||
scripts = str(Path(unpacked_wheel, f"{name}.data", "scripts"))
|
||||
|
||||
# egg-info may be generated again to create a manifest (used for package data)
|
||||
egg_info = dist.reinitialize_command("egg_info", reinit_subcommands=True)
|
||||
egg_info = cast(
|
||||
egg_info_cls, dist.reinitialize_command("egg_info", reinit_subcommands=True)
|
||||
)
|
||||
egg_info.egg_base = str(tmp_dir)
|
||||
egg_info.ignore_egg_info_in_manifest = True
|
||||
|
||||
build = dist.reinitialize_command("build", reinit_subcommands=True)
|
||||
install = dist.reinitialize_command("install", reinit_subcommands=True)
|
||||
build = cast(
|
||||
build_cls, dist.reinitialize_command("build", reinit_subcommands=True)
|
||||
)
|
||||
install = cast(
|
||||
install_cls, dist.reinitialize_command("install", reinit_subcommands=True)
|
||||
)
|
||||
|
||||
build.build_platlib = build.build_purelib = build.build_lib = build_lib
|
||||
install.install_purelib = install.install_platlib = install.install_lib = wheel
|
||||
@@ -222,12 +211,14 @@ class editable_wheel(Command):
|
||||
install.install_headers = headers
|
||||
install.install_data = data
|
||||
|
||||
install_scripts = dist.get_command_obj("install_scripts")
|
||||
install_scripts = cast(
|
||||
install_scripts_cls, dist.get_command_obj("install_scripts")
|
||||
)
|
||||
install_scripts.no_ep = True
|
||||
|
||||
build.build_temp = str(tmp_dir)
|
||||
|
||||
build_py = dist.get_command_obj("build_py")
|
||||
build_py = cast(build_py_cls, dist.get_command_obj("build_py"))
|
||||
build_py.compile = False
|
||||
build_py.existing_egg_info_dir = self._find_egg_info_dir()
|
||||
|
||||
@@ -240,6 +231,7 @@ class editable_wheel(Command):
|
||||
"""Set the ``editable_mode`` flag in the build sub-commands"""
|
||||
dist = self.distribution
|
||||
build = dist.get_command_obj("build")
|
||||
# TODO: Update typeshed distutils stubs to overload non-None return type by default
|
||||
for cmd_name in build.get_sub_commands():
|
||||
cmd = dist.get_command_obj(cmd_name)
|
||||
if hasattr(cmd, "editable_mode"):
|
||||
@@ -247,9 +239,9 @@ class editable_wheel(Command):
|
||||
elif hasattr(cmd, "inplace"):
|
||||
cmd.inplace = True # backward compatibility with distutils
|
||||
|
||||
def _collect_build_outputs(self) -> Tuple[List[str], Dict[str, str]]:
|
||||
files: List[str] = []
|
||||
mapping: Dict[str, str] = {}
|
||||
def _collect_build_outputs(self) -> tuple[list[str], dict[str, str]]:
|
||||
files: list[str] = []
|
||||
mapping: dict[str, str] = {}
|
||||
build = self.get_finalized_command("build")
|
||||
|
||||
for cmd_name in build.get_sub_commands():
|
||||
@@ -262,8 +254,12 @@ class editable_wheel(Command):
|
||||
return files, mapping
|
||||
|
||||
def _run_build_commands(
|
||||
self, dist_name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
|
||||
) -> Tuple[List[str], Dict[str, str]]:
|
||||
self,
|
||||
dist_name: str,
|
||||
unpacked_wheel: StrPath,
|
||||
build_lib: StrPath,
|
||||
tmp_dir: StrPath,
|
||||
) -> tuple[list[str], dict[str, str]]:
|
||||
self._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir)
|
||||
self._run_build_subcommands()
|
||||
files, mapping = self._collect_build_outputs()
|
||||
@@ -272,7 +268,7 @@ class editable_wheel(Command):
|
||||
self._run_install("data")
|
||||
return files, mapping
|
||||
|
||||
def _run_build_subcommands(self):
|
||||
def _run_build_subcommands(self) -> None:
|
||||
"""
|
||||
Issue #3501 indicates that some plugins/customizations might rely on:
|
||||
|
||||
@@ -286,10 +282,10 @@ class editable_wheel(Command):
|
||||
# TODO: Once plugins/customisations had the chance to catch up, replace
|
||||
# `self._run_build_subcommands()` with `self.run_command("build")`.
|
||||
# Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023.
|
||||
build: Command = self.get_finalized_command("build")
|
||||
build = self.get_finalized_command("build")
|
||||
for name in build.get_sub_commands():
|
||||
cmd = self.get_finalized_command(name)
|
||||
if name == "build_py" and type(cmd) != build_py_cls:
|
||||
if name == "build_py" and type(cmd) is not build_py_cls:
|
||||
self._safely_run(name)
|
||||
else:
|
||||
self.run_command(name)
|
||||
@@ -360,8 +356,8 @@ class editable_wheel(Command):
|
||||
self,
|
||||
name: str,
|
||||
tag: str,
|
||||
build_lib: _Path,
|
||||
) -> "EditableStrategy":
|
||||
build_lib: StrPath,
|
||||
) -> EditableStrategy:
|
||||
"""Decides which strategy to use to implement an editable installation."""
|
||||
build_name = f"__editable__.{name}-{tag}"
|
||||
project_dir = Path(self.project_dir)
|
||||
@@ -384,28 +380,31 @@ class editable_wheel(Command):
|
||||
|
||||
|
||||
class EditableStrategy(Protocol):
|
||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
||||
...
|
||||
def __call__(self, wheel: WheelFile, files: list[str], mapping: dict[str, str]): ...
|
||||
|
||||
def __enter__(self):
|
||||
...
|
||||
def __enter__(self) -> Self: ...
|
||||
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
...
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
/,
|
||||
) -> object: ...
|
||||
|
||||
|
||||
class _StaticPth:
|
||||
def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):
|
||||
def __init__(self, dist: Distribution, name: str, path_entries: list[Path]):
|
||||
self.dist = dist
|
||||
self.name = name
|
||||
self.path_entries = path_entries
|
||||
|
||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
||||
entries = "\n".join((str(p.resolve()) for p in self.path_entries))
|
||||
def __call__(self, wheel: WheelFile, files: list[str], mapping: dict[str, str]):
|
||||
entries = "\n".join(str(p.resolve()) for p in self.path_entries)
|
||||
contents = _encode_pth(f"{entries}\n")
|
||||
wheel.writestr(f"__editable__.{self.name}.pth", contents)
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> Self:
|
||||
msg = f"""
|
||||
Editable install will be performed using .pth file to extend `sys.path` with:
|
||||
{list(map(os.fspath, self.path_entries))!r}
|
||||
@@ -413,8 +412,13 @@ class _StaticPth:
|
||||
_logger.warning(msg + _LENIENT_WARNING)
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
...
|
||||
def __exit__(
|
||||
self,
|
||||
_exc_type: object,
|
||||
_exc_value: object,
|
||||
_traceback: object,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class _LinkTree(_StaticPth):
|
||||
@@ -432,19 +436,19 @@ class _LinkTree(_StaticPth):
|
||||
self,
|
||||
dist: Distribution,
|
||||
name: str,
|
||||
auxiliary_dir: _Path,
|
||||
build_lib: _Path,
|
||||
auxiliary_dir: StrPath,
|
||||
build_lib: StrPath,
|
||||
):
|
||||
self.auxiliary_dir = Path(auxiliary_dir)
|
||||
self.build_lib = Path(build_lib).resolve()
|
||||
self._file = dist.get_command_obj("build_py").copy_file
|
||||
super().__init__(dist, name, [self.auxiliary_dir])
|
||||
|
||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
||||
def __call__(self, wheel: WheelFile, files: list[str], mapping: dict[str, str]):
|
||||
self._create_links(files, mapping)
|
||||
super().__call__(wheel, files, mapping)
|
||||
|
||||
def _normalize_output(self, file: str) -> Optional[str]:
|
||||
def _normalize_output(self, file: str) -> str | None:
|
||||
# Files relative to build_lib will be normalized to None
|
||||
with suppress(ValueError):
|
||||
path = Path(file).resolve().relative_to(self.build_lib)
|
||||
@@ -460,8 +464,9 @@ class _LinkTree(_StaticPth):
|
||||
def _create_links(self, outputs, output_mapping):
|
||||
self.auxiliary_dir.mkdir(parents=True, exist_ok=True)
|
||||
link_type = "sym" if _can_symlink_files(self.auxiliary_dir) else "hard"
|
||||
mappings = {self._normalize_output(k): v for k, v in output_mapping.items()}
|
||||
mappings.pop(None, None) # remove files that are not relative to build_lib
|
||||
normalised = ((self._normalize_output(k), v) for k, v in output_mapping.items())
|
||||
# remove files that are not relative to build_lib
|
||||
mappings = {k: v for k, v in normalised if k is not None}
|
||||
|
||||
for output in outputs:
|
||||
relative = self._normalize_output(output)
|
||||
@@ -471,12 +476,17 @@ class _LinkTree(_StaticPth):
|
||||
for relative, src in mappings.items():
|
||||
self._create_file(relative, src, link=link_type)
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> Self:
|
||||
msg = "Strict editable install will be performed using a link tree.\n"
|
||||
_logger.warning(msg + _STRICT_WARNING)
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
def __exit__(
|
||||
self,
|
||||
_exc_type: object,
|
||||
_exc_value: object,
|
||||
_traceback: object,
|
||||
) -> None:
|
||||
msg = f"""\n
|
||||
Strict editable installation performed using the auxiliary directory:
|
||||
{self.auxiliary_dir}
|
||||
@@ -492,13 +502,13 @@ class _TopLevelFinder:
|
||||
self.dist = dist
|
||||
self.name = name
|
||||
|
||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
||||
def template_vars(self) -> tuple[str, str, dict[str, str], dict[str, list[str]]]:
|
||||
src_root = self.dist.src_root or os.curdir
|
||||
top_level = chain(_find_packages(self.dist), _find_top_level_modules(self.dist))
|
||||
package_dir = self.dist.package_dir or {}
|
||||
roots = _find_package_roots(top_level, package_dir, src_root)
|
||||
|
||||
namespaces_: Dict[str, List[str]] = dict(
|
||||
namespaces_: dict[str, list[str]] = dict(
|
||||
chain(
|
||||
_find_namespaces(self.dist.packages or [], roots),
|
||||
((ns, []) for ns in _find_virtual_namespaces(roots)),
|
||||
@@ -517,18 +527,32 @@ class _TopLevelFinder:
|
||||
|
||||
name = f"__editable__.{self.name}.finder"
|
||||
finder = _normalization.safe_identifier(name)
|
||||
return finder, name, mapping, namespaces_
|
||||
|
||||
def get_implementation(self) -> Iterator[tuple[str, bytes]]:
|
||||
finder, name, mapping, namespaces_ = self.template_vars()
|
||||
|
||||
content = bytes(_finder_template(name, mapping, namespaces_), "utf-8")
|
||||
wheel.writestr(f"{finder}.py", content)
|
||||
yield (f"{finder}.py", content)
|
||||
|
||||
content = _encode_pth(f"import {finder}; {finder}.install()")
|
||||
wheel.writestr(f"__editable__.{self.name}.pth", content)
|
||||
yield (f"__editable__.{self.name}.pth", content)
|
||||
|
||||
def __enter__(self):
|
||||
def __call__(self, wheel: WheelFile, files: list[str], mapping: dict[str, str]):
|
||||
for file, content in self.get_implementation():
|
||||
wheel.writestr(file, content)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
msg = "Editable install will be performed using a meta path finder.\n"
|
||||
_logger.warning(msg + _LENIENT_WARNING)
|
||||
return self
|
||||
|
||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
def __exit__(
|
||||
self,
|
||||
_exc_type: object,
|
||||
_exc_value: object,
|
||||
_traceback: object,
|
||||
) -> None:
|
||||
msg = """\n
|
||||
Please be careful with folders in your working directory with the same
|
||||
name as your package as they may take precedence during imports.
|
||||
@@ -537,17 +561,20 @@ class _TopLevelFinder:
|
||||
|
||||
|
||||
def _encode_pth(content: str) -> bytes:
|
||||
""".pth files are always read with 'locale' encoding, the recommendation
|
||||
"""
|
||||
Prior to Python 3.13 (see https://github.com/python/cpython/issues/77102),
|
||||
.pth files are always read with 'locale' encoding, the recommendation
|
||||
from the cpython core developers is to write them as ``open(path, "w")``
|
||||
and ignore warnings (see python/cpython#77102, pypa/setuptools#3937).
|
||||
This function tries to simulate this behaviour without having to create an
|
||||
actual file, in a way that supports a range of active Python versions.
|
||||
(There seems to be some variety in the way different version of Python handle
|
||||
``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``).
|
||||
``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``
|
||||
or ``locale.getencoding()``).
|
||||
"""
|
||||
encoding = "locale" if sys.version_info >= (3, 10) else None
|
||||
with io.BytesIO() as buffer:
|
||||
wrapper = io.TextIOWrapper(buffer, encoding)
|
||||
wrapper = io.TextIOWrapper(buffer, encoding=py312.PTH_ENCODING)
|
||||
# TODO: Python 3.13 replace the whole function with `bytes(content, "utf-8")`
|
||||
wrapper.write(content)
|
||||
wrapper.flush()
|
||||
buffer.seek(0)
|
||||
@@ -575,7 +602,7 @@ def _can_symlink_files(base_dir: Path) -> bool:
|
||||
|
||||
|
||||
def _simple_layout(
|
||||
packages: Iterable[str], package_dir: Dict[str, str], project_dir: Path
|
||||
packages: Iterable[str], package_dir: dict[str, str], project_dir: StrPath
|
||||
) -> bool:
|
||||
"""Return ``True`` if:
|
||||
- all packages are contained by the same parent directory, **and**
|
||||
@@ -608,7 +635,7 @@ def _simple_layout(
|
||||
layout = {pkg: find_package_path(pkg, package_dir, project_dir) for pkg in packages}
|
||||
if not layout:
|
||||
return set(package_dir) in ({}, {""})
|
||||
parent = os.path.commonpath([_parent_path(k, v) for k, v in layout.items()])
|
||||
parent = os.path.commonpath(starmap(_parent_path, layout.items()))
|
||||
return all(
|
||||
_path.same_path(Path(parent, *key.split('.')), value)
|
||||
for key, value in layout.items()
|
||||
@@ -657,9 +684,9 @@ def _find_top_level_modules(dist: Distribution) -> Iterator[str]:
|
||||
def _find_package_roots(
|
||||
packages: Iterable[str],
|
||||
package_dir: Mapping[str, str],
|
||||
src_root: _Path,
|
||||
) -> Dict[str, str]:
|
||||
pkg_roots: Dict[str, str] = {
|
||||
src_root: StrPath,
|
||||
) -> dict[str, str]:
|
||||
pkg_roots: dict[str, str] = {
|
||||
pkg: _absolute_root(find_package_path(pkg, package_dir, src_root))
|
||||
for pkg in sorted(packages)
|
||||
}
|
||||
@@ -667,7 +694,7 @@ def _find_package_roots(
|
||||
return _remove_nested(pkg_roots)
|
||||
|
||||
|
||||
def _absolute_root(path: _Path) -> str:
|
||||
def _absolute_root(path: StrPath) -> str:
|
||||
"""Works for packages and top-level modules"""
|
||||
path_ = Path(path)
|
||||
parent = path_.parent
|
||||
@@ -678,7 +705,7 @@ def _absolute_root(path: _Path) -> str:
|
||||
return str(parent.resolve() / path_.name)
|
||||
|
||||
|
||||
def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:
|
||||
def _find_virtual_namespaces(pkg_roots: dict[str, str]) -> Iterator[str]:
|
||||
"""By carefully designing ``package_dir``, it is possible to implement the logical
|
||||
structure of PEP 420 in a package without the corresponding directories.
|
||||
|
||||
@@ -703,15 +730,15 @@ def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:
|
||||
|
||||
|
||||
def _find_namespaces(
|
||||
packages: List[str], pkg_roots: Dict[str, str]
|
||||
) -> Iterator[Tuple[str, List[str]]]:
|
||||
packages: list[str], pkg_roots: dict[str, str]
|
||||
) -> Iterator[tuple[str, list[str]]]:
|
||||
for pkg in packages:
|
||||
path = find_package_path(pkg, pkg_roots, "")
|
||||
if Path(path).exists() and not Path(path, "__init__.py").exists():
|
||||
yield (pkg, [path])
|
||||
|
||||
|
||||
def _remove_nested(pkg_roots: Dict[str, str]) -> Dict[str, str]:
|
||||
def _remove_nested(pkg_roots: dict[str, str]) -> dict[str, str]:
|
||||
output = dict(pkg_roots.copy())
|
||||
|
||||
for pkg, path in reversed(list(pkg_roots.items())):
|
||||
@@ -772,6 +799,7 @@ class _NamespaceInstaller(namespaces.Installer):
|
||||
|
||||
|
||||
_FINDER_TEMPLATE = """\
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from importlib.machinery import ModuleSpec, PathFinder
|
||||
from importlib.machinery import all_suffixes as module_suffixes
|
||||
@@ -779,16 +807,14 @@ from importlib.util import spec_from_file_location
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
|
||||
MAPPING = {mapping!r}
|
||||
NAMESPACES = {namespaces!r}
|
||||
MAPPING: dict[str, str] = {mapping!r}
|
||||
NAMESPACES: dict[str, list[str]] = {namespaces!r}
|
||||
PATH_PLACEHOLDER = {name!r} + ".__path_hook__"
|
||||
|
||||
|
||||
class _EditableFinder: # MetaPathFinder
|
||||
@classmethod
|
||||
def find_spec(cls, fullname, path=None, target=None):
|
||||
extra_path = []
|
||||
|
||||
def find_spec(cls, fullname: str, path=None, target=None) -> ModuleSpec | None: # type: ignore
|
||||
# Top-level packages and modules (we know these exist in the FS)
|
||||
if fullname in MAPPING:
|
||||
pkg_path = MAPPING[fullname]
|
||||
@@ -799,35 +825,42 @@ class _EditableFinder: # MetaPathFinder
|
||||
# to the importlib.machinery implementation.
|
||||
parent, _, child = fullname.rpartition(".")
|
||||
if parent and parent in MAPPING:
|
||||
return PathFinder.find_spec(fullname, path=[MAPPING[parent], *extra_path])
|
||||
return PathFinder.find_spec(fullname, path=[MAPPING[parent]])
|
||||
|
||||
# Other levels of nesting should be handled automatically by importlib
|
||||
# using the parent path.
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _find_spec(cls, fullname, candidate_path):
|
||||
def _find_spec(cls, fullname: str, candidate_path: Path) -> ModuleSpec | None:
|
||||
init = candidate_path / "__init__.py"
|
||||
candidates = (candidate_path.with_suffix(x) for x in module_suffixes())
|
||||
for candidate in chain([init], candidates):
|
||||
if candidate.exists():
|
||||
return spec_from_file_location(fullname, candidate)
|
||||
return None
|
||||
|
||||
|
||||
class _EditableNamespaceFinder: # PathEntryFinder
|
||||
@classmethod
|
||||
def _path_hook(cls, path):
|
||||
def _path_hook(cls, path) -> type[_EditableNamespaceFinder]:
|
||||
if path == PATH_PLACEHOLDER:
|
||||
return cls
|
||||
raise ImportError
|
||||
|
||||
@classmethod
|
||||
def _paths(cls, fullname):
|
||||
# Ensure __path__ is not empty for the spec to be considered a namespace.
|
||||
return NAMESPACES[fullname] or MAPPING.get(fullname) or [PATH_PLACEHOLDER]
|
||||
def _paths(cls, fullname: str) -> list[str]:
|
||||
paths = NAMESPACES[fullname]
|
||||
if not paths and fullname in MAPPING:
|
||||
paths = [MAPPING[fullname]]
|
||||
# Always add placeholder, for 2 reasons:
|
||||
# 1. __path__ cannot be empty for the spec to be considered namespace.
|
||||
# 2. In the case of nested namespaces, we need to force
|
||||
# import machinery to query _EditableNamespaceFinder again.
|
||||
return [*paths, PATH_PLACEHOLDER]
|
||||
|
||||
@classmethod
|
||||
def find_spec(cls, fullname, target=None):
|
||||
def find_spec(cls, fullname: str, target=None) -> ModuleSpec | None: # type: ignore
|
||||
if fullname in NAMESPACES:
|
||||
spec = ModuleSpec(fullname, None, is_package=True)
|
||||
spec.submodule_search_locations = cls._paths(fullname)
|
||||
@@ -835,7 +868,7 @@ class _EditableNamespaceFinder: # PathEntryFinder
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def find_module(cls, fullname):
|
||||
def find_module(cls, _fullname) -> None:
|
||||
return None
|
||||
|
||||
|
||||
@@ -855,7 +888,7 @@ def install():
|
||||
|
||||
|
||||
def _finder_template(
|
||||
name: str, mapping: Mapping[str, str], namespaces: Dict[str, List[str]]
|
||||
name: str, mapping: Mapping[str, str], namespaces: dict[str, list[str]]
|
||||
) -> str:
|
||||
"""Create a string containing the code for the``MetaPathFinder`` and
|
||||
``PathEntryFinder``.
|
||||
|
||||
Reference in New Issue
Block a user