summaryrefslogtreecommitdiff
path: root/setuptools/command
diff options
context:
space:
mode:
authorAnderson Bravalheri <andersonbravalheri@gmail.com>2022-06-25 13:51:52 +0100
committerAnderson Bravalheri <andersonbravalheri@gmail.com>2022-06-25 13:51:52 +0100
commit700237e252e45cb465c1013fe41ad43092fccf1a (patch)
tree88cb73c81cfcf58cf1e57cfb895b1426652aafb5 /setuptools/command
parent28f6f364442bd6cf5bfe02f462c0eb6b23a9e346 (diff)
parent965458d1d271553d25f431ffebdb68bd12938f9a (diff)
downloadpython-setuptools-git-700237e252e45cb465c1013fe41ad43092fccf1a.tar.gz
Update editable install to use get_output_mapping (#3409)
Diffstat (limited to 'setuptools/command')
-rw-r--r--setuptools/command/build.py108
-rw-r--r--setuptools/command/build_ext.py2
-rw-r--r--setuptools/command/build_py.py6
-rw-r--r--setuptools/command/editable_wheel.py374
4 files changed, 355 insertions, 135 deletions
diff --git a/setuptools/command/build.py b/setuptools/command/build.py
index 12a43622..bf4f71a7 100644
--- a/setuptools/command/build.py
+++ b/setuptools/command/build.py
@@ -1,8 +1,17 @@
-from distutils.command.build import build as _build
+import sys
import warnings
+from typing import TYPE_CHECKING, List, Dict
+from distutils.command.build import build as _build
from setuptools import SetuptoolsDeprecationWarning
+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
+
_ORIGINAL_SUBCOMMANDS = {"build_py", "build_clib", "build_ext", "build_scripts"}
@@ -22,3 +31,100 @@ class build(_build):
warnings.warn(msg, SetuptoolsDeprecationWarning)
self.sub_commands = _build.sub_commands
super().run()
+
+
+class SubCommand(Protocol):
+ """In order to support editable installations (see :pep:`660`) all
+ build subcommands **SHOULD** implement this protocol. They also **MUST** inherit
+ from ``setuptools.Command``.
+
+ When creating an :pep:`editable wheel <660>`, ``setuptools`` will try to evaluate
+ custom ``build`` subcommands using the following procedure:
+
+ 1. ``setuptools`` will set the ``editable_mode`` flag will be set to ``True``
+ 2. ``setuptools`` will execute the ``run()`` command.
+
+ .. important::
+ Subcommands **SHOULD** take advantage of ``editable_mode=True`` to adequate
+ its behaviour or perform optimisations.
+
+ For example, if a subcommand don't need to generate any extra file and
+ everything it does is to copy a source file into the build directory,
+ ``run()`` **SHOULD** simply "early return".
+
+ Similarly, if the subcommand creates files that would be placed alongside
+ Python files in the final distribution, during an editable install
+ the command **SHOULD** generate these files "in place" (i.e. write them to
+ the original source directory, instead of using the build directory).
+ Note that ``get_output_mapping()`` should reflect that and include mappings
+ for "in place" builds accordingly.
+
+ 3. ``setuptools`` use any knowledge it can derive from the return values of
+ ``get_outputs()`` and ``get_output_mapping()`` to create an editable wheel.
+ When relevant ``setuptools`` **MAY** attempt to use file links based on the value
+ of ``get_output_mapping()``. Alternatively, ``setuptools`` **MAY** attempt to use
+ :doc:`import hooks <python:reference/import>` to redirect any attempt to import
+ to the directory with the original source code and other files built in place.
+ """
+
+ editable_mode: bool = False
+ """Boolean flag that will be set to ``True`` when setuptools is used for an
+ editable installation (see :pep:`660`).
+ Implementations **SHOULD** explicitly set the default value of this attribute to
+ ``False``.
+ When subcommands run, they can use this flag to perform optimizations or change
+ their behaviour accordingly.
+ """
+
+ build_lib: str
+ """String representing the directory where the build artifacts should be stored,
+ e.g. ``build/lib``.
+ For example, if a distribution wants to provide a Python module named ``pkg.mod``,
+ then a corresponding file should be written to ``{build_lib}/package/module.py``.
+ A way of thinking about this is that the files saved under ``build_lib``
+ would be eventually copied to one of the directories in :obj:`site.PREFIXES`
+ upon installation.
+
+ A command that produces platform-independent files (e.g. compiling text templates
+ into Python functions), **CAN** initialize ``build_lib`` by copying its value from
+ the ``build_py`` command. On the other hand, a command that produces
+ platform-specific files **CAN** initialize ``build_lib`` by copying its value from
+ the ``build_ext`` command. In general this is done inside the ``finalize_options``
+ method with the help of the ``set_undefined_options`` command::
+
+ def finalize_options(self):
+ self.set_undefined_options("build_py", ("build_lib", "build_lib"))
+ ...
+ """
+
+ def initialize_options(self):
+ """(Required by the original :class:`setuptools.Command` interface)"""
+
+ def finalize_options(self):
+ """(Required by the original :class:`setuptools.Command` interface)"""
+
+ def run(self):
+ """(Required by the original :class:`setuptools.Command` interface)"""
+
+ def get_outputs(self) -> List[str]:
+ """
+ Return a list of files intended for distribution as they would have been
+ produced by the build.
+ These files should be strings in the form of
+ ``"{build_lib}/destination/file/path"``.
+
+ .. note::
+ The return value of ``get_output()`` should include all files used as keys
+ in ``get_output_mapping()`` plus files that are generated during the build
+ and don't correspond to any source file already present in the project.
+ """
+
+ def get_output_mapping(self) -> Dict[str, str]:
+ """
+ Return a mapping between destination files as they would be produced by the
+ build (dict keys) into the respective existing (source) files (dict values).
+ Existing (source) files should be represented as strings relative to the project
+ root directory.
+ Destination files should be strings in the form of
+ ``"{build_lib}/destination/file/path"``.
+ """
diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py
index 31ef47bf..7ad5a87a 100644
--- a/setuptools/command/build_ext.py
+++ b/setuptools/command/build_ext.py
@@ -104,7 +104,7 @@ class build_ext(_build_ext):
# Always copy, even if source is older than destination, to ensure
# that the right extensions for the current Python/platform are
# used.
- build_py.copy_file(regular_file, inplace_file)
+ self.copy_file(regular_file, inplace_file, level=self.verbose)
if ext._needs_stub:
inplace_stub = self._get_equivalent_stub(ext, inplace_file)
diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py
index a2a6fe2c..923a3232 100644
--- a/setuptools/command/build_py.py
+++ b/setuptools/command/build_py.py
@@ -40,17 +40,15 @@ class build_py(orig.build_py):
if 'data_files' in self.__dict__:
del self.__dict__['data_files']
self.__updated_files = []
- self.use_links = None
def copy_file(self, infile, outfile, preserve_mode=1, preserve_times=1,
link=None, level=1):
# Overwrite base class to allow using links
- link = getattr(self, "use_links", None) if link is None else link
if link:
infile = str(Path(infile).resolve())
outfile = str(Path(outfile).resolve())
- return super().copy_file(infile, outfile, preserve_mode,
- preserve_times, link, level)
+ return super().copy_file(infile, outfile, preserve_mode, preserve_times,
+ link, level)
def run(self):
"""Build modules, packages, and copy data files to build directory"""
diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py
index 2776577f..bd580acb 100644
--- a/setuptools/command/editable_wheel.py
+++ b/setuptools/command/editable_wheel.py
@@ -7,24 +7,46 @@ Create a wheel that, when installed, will make the source package 'editable'
One of the mechanisms briefly mentioned in PEP 660 to implement editable installs is
to create a separated directory inside ``build`` and use a .pth file to point to that
directory. In the context of this file such directory is referred as
- *auxiliary build directory* or ``auxiliary_build_dir``.
+ *auxiliary build directory* or ``auxiliary_dir``.
"""
+import logging
import os
import re
import shutil
import sys
-import logging
import warnings
+from contextlib import suppress
from itertools import chain
from pathlib import Path
from tempfile import TemporaryDirectory
-from typing import Dict, Iterable, Iterator, List, Mapping, Union, Tuple, TypeVar
-
-from setuptools import Command, namespaces
+from typing import (
+ TYPE_CHECKING,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Mapping,
+ Optional,
+ Tuple,
+ TypeVar,
+ Union
+)
+
+from setuptools import Command, errors, namespaces
from setuptools.discovery import find_package_path
from setuptools.dist import Distribution
+if TYPE_CHECKING:
+ from wheel.wheelfile import WheelFile # noqa
+
+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
+
_Path = Union[str, Path]
_P = TypeVar("_P", bound=_Path)
_logger = logging.getLogger(__name__)
@@ -64,9 +86,9 @@ class editable_wheel(Command):
self.project_dir = dist.src_root or os.curdir
self.package_dir = dist.package_dir or {}
self.dist_dir = Path(self.dist_dir or os.path.join(self.project_dir, "dist"))
- self.dist_dir.mkdir(exist_ok=True)
def run(self):
+ self.dist_dir.mkdir(exist_ok=True)
self._ensure_dist_info()
# Add missing dist_info files
@@ -96,6 +118,95 @@ class editable_wheel(Command):
installer = _NamespaceInstaller(dist, installation_dir, pth_prefix, src_root)
installer.install_namespaces()
+ def _find_egg_info_dir(self) -> Optional[str]:
+ 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
+ ):
+ """Configure commands to behave in the following ways:
+
+ - Build commands can write to ``build_lib`` if they really want to...
+ (but this folder is expected to be ignored and modules are expected to live
+ in the project directory...)
+ - Binary extensions should be built in-place (editable_mode = True)
+ - Data/header/script files are not part of the "editable" specification
+ so they are written directly to the unpacked_wheel directory.
+ """
+ # Non-editable files (data, headers, scripts) are written directly to the
+ # unpacked_wheel
+
+ dist = self.distribution
+ wheel = str(unpacked_wheel)
+ build_lib = str(build_lib)
+ data = str(Path(unpacked_wheel, f"{name}.data", "data"))
+ headers = str(Path(unpacked_wheel, f"{name}.data", "include"))
+ 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.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.build_platlib = build.build_purelib = build.build_lib = build_lib
+ install.install_purelib = install.install_platlib = install.install_lib = wheel
+ install.install_scripts = build.build_scripts = scripts
+ install.install_headers = headers
+ install.install_data = data
+
+ install_scripts = 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.compile = False
+ build_py.existing_egg_info_dir = self._find_egg_info_dir()
+
+ self._set_editable_mode()
+
+ build.ensure_finalized()
+ install.ensure_finalized()
+
+ def _set_editable_mode(self):
+ """Set the ``editable_mode`` flag in the build sub-commands"""
+ dist = self.distribution
+ build = dist.get_command_obj("build")
+ for cmd_name in build.get_sub_commands():
+ cmd = dist.get_command_obj(cmd_name)
+ if hasattr(cmd, "editable_mode"):
+ cmd.editable_mode = True
+
+ 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():
+ cmd = self.get_finalized_command(cmd_name)
+ if hasattr(cmd, "get_outputs"):
+ files.extend(cmd.get_outputs() or [])
+ if hasattr(cmd, "get_output_mapping"):
+ mapping.update(cmd.get_output_mapping() or {})
+
+ 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._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir)
+ self.run_command("build")
+ files, mapping = self._collect_build_outputs()
+ self._run_install("headers")
+ self._run_install("scripts")
+ self._run_install("data")
+ return files, mapping
+
def _create_wheel_file(self, bdist_wheel):
from wheel.wheelfile import WheelFile
@@ -108,25 +219,19 @@ class editable_wheel(Command):
if wheel_path.exists():
wheel_path.unlink()
- # Currently the wheel API receives a directory and dump all its contents
- # inside of a wheel. So let's use a temporary directory.
- unpacked_tmp = TemporaryDirectory(suffix=archive_name)
+ unpacked_wheel = TemporaryDirectory(suffix=archive_name)
+ build_lib = TemporaryDirectory(suffix=".build-lib")
build_tmp = TemporaryDirectory(suffix=".build-temp")
- with unpacked_tmp as unpacked, build_tmp as tmp:
+ with unpacked_wheel as unpacked, build_lib as lib, build_tmp as tmp:
unpacked_dist_info = Path(unpacked, Path(self.dist_info_dir).name)
shutil.copytree(self.dist_info_dir, unpacked_dist_info)
self._install_namespaces(unpacked, dist_info.name)
-
- # Add non-editable files to the wheel
- _configure_build(dist_name, self.distribution, unpacked, tmp)
- self._run_install("headers")
- self._run_install("scripts")
- self._run_install("data")
-
- self._populate_wheel(dist_info.name, tag, unpacked, tmp)
- with WheelFile(wheel_path, "w") as wf:
- wf.write_files(unpacked)
+ files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)
+ strategy = self._select_strategy(dist_name, tag, lib)
+ with strategy, WheelFile(wheel_path, "w") as wheel_obj:
+ strategy(wheel_obj, files, mapping)
+ wheel_obj.write_files(unpacked)
return wheel_path
@@ -136,60 +241,40 @@ class editable_wheel(Command):
_logger.info(f"Installing {category} as non editable")
self.run_command(f"install_{category}")
- def _populate_wheel(self, name: str, tag: str, unpacked_dir: Path, tmp: _Path):
+ def _select_strategy(
+ self,
+ name: str,
+ tag: str,
+ build_lib: _Path,
+ ) -> "EditableStrategy":
"""Decides which strategy to use to implement an editable installation."""
build_name = f"__editable__.{name}-{tag}"
project_dir = Path(self.project_dir)
if self.strict or os.getenv("SETUPTOOLS_EDITABLE", None) == "strict":
- return self._populate_link_tree(name, build_name, unpacked_dir, tmp)
-
- # Build extensions in-place
- self.reinitialize_command("build_ext", inplace=1)
- self.run_command("build_ext")
+ auxiliary_dir = _empty_dir(Path(self.project_dir, "build", build_name))
+ return _LinkTree(self.distribution, name, auxiliary_dir, build_lib)
packages = _find_packages(self.distribution)
has_simple_layout = _simple_layout(packages, self.package_dir, project_dir)
if set(self.package_dir) == {""} and has_simple_layout:
# src-layout(ish) is relatively safe for a simple pth file
- return self._populate_static_pth(name, project_dir, unpacked_dir)
+ src_dir = self.package_dir[""]
+ return _StaticPth(self.distribution, name, [Path(project_dir, src_dir)])
# Use a MetaPathFinder to avoid adding accidental top-level packages/modules
- self._populate_finder(name, unpacked_dir)
+ return _TopLevelFinder(self.distribution, name)
- def _populate_link_tree(
- self, name: str, build_name: str, unpacked_dir: Path, tmp: _Path
- ):
- """Populate wheel using the "strict" ``link tree`` strategy."""
- msg = "Strict editable install will be performed using a link tree.\n"
- _logger.warning(msg + _STRICT_WARNING)
- auxiliary_build_dir = _empty_dir(Path(self.project_dir, "build", build_name))
- populate = _LinkTree(self.distribution, name, auxiliary_build_dir, tmp)
- populate(unpacked_dir)
- msg = f"""\n
- Strict editable installation performed using the auxiliary directory:
- {auxiliary_build_dir}
+class EditableStrategy(Protocol):
+ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
+ ...
- Please be careful to not remove this directory, otherwise you might not be able
- to import/use your package.
- """
- warnings.warn(msg, InformationOnly)
-
- def _populate_static_pth(self, name: str, project_dir: Path, unpacked_dir: Path):
- """Populate wheel using the "lax" ``.pth`` file strategy, for ``src-layout``."""
- src_dir = self.package_dir[""]
- msg = f"Editable install will be performed using .pth file to {src_dir}.\n"
- _logger.warning(msg + _LAX_WARNING)
- populate = _StaticPth(self.distribution, name, [Path(project_dir, src_dir)])
- populate(unpacked_dir)
+ def __enter__(self):
+ ...
- def _populate_finder(self, name: str, unpacked_dir: Path):
- """Populate wheel using the "lax" MetaPathFinder strategy."""
- msg = "Editable install will be performed using a meta path finder.\n"
- _logger.warning(msg + _LAX_WARNING)
- populate = _TopLevelFinder(self.distribution, name)
- populate(unpacked_dir)
+ def __exit__(self, _exc_type, _exc_value, _traceback):
+ ...
class _StaticPth:
@@ -198,53 +283,91 @@ class _StaticPth:
self.name = name
self.path_entries = path_entries
- def __call__(self, unpacked_wheel_dir: Path):
- pth = Path(unpacked_wheel_dir, f"__editable__.{self.name}.pth")
+ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
entries = "\n".join((str(p.resolve()) for p in self.path_entries))
- pth.write_text(f"{entries}\n", encoding="utf-8")
+ contents = bytes(f"{entries}\n", "utf-8")
+ wheel.writestr(f"__editable__.{self.name}.pth", contents)
+
+ def __enter__(self):
+ msg = f"""
+ Editable install will be performed using .pth file to extend `sys.path` with:
+ {self.path_entries!r}
+ """
+ _logger.warning(msg + _LAX_WARNING)
+ return self
+
+ def __exit__(self, _exc_type, _exc_value, _traceback):
+ ...
class _LinkTree(_StaticPth):
"""
- Creates a ``.pth`` file that points to a link tree in the ``auxiliary_build_dir``.
+ Creates a ``.pth`` file that points to a link tree in the ``auxiliary_dir``.
This strategy will only link files (not dirs), so it can be implemented in
any OS, even if that means using hardlinks instead of symlinks.
- By collocating ``auxiliary_build_dir`` and the original source code, limitations
+ By collocating ``auxiliary_dir`` and the original source code, limitations
with hardlinks should be avoided.
"""
def __init__(
- self, dist: Distribution, name: str, auxiliary_build_dir: Path, tmp: _Path
+ self, dist: Distribution,
+ name: str,
+ auxiliary_dir: _Path,
+ build_lib: _Path,
):
- super().__init__(dist, name, [auxiliary_build_dir])
- self.auxiliary_build_dir = auxiliary_build_dir
- self.tmp = tmp
-
- def _build_py(self):
- if not self.dist.has_pure_modules():
- return
-
- build_py = self.dist.get_command_obj("build_py")
- build_py.ensure_finalized()
- # Force build_py to use links instead of copying files
- build_py.use_links = "sym" if _can_symlink_files() else "hard"
- build_py.run()
+ 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]):
+ self._create_links(files, mapping)
+ super().__call__(wheel, files, mapping)
+
+ def _normalize_output(self, file: str) -> Optional[str]:
+ # Files relative to build_lib will be normalized to None
+ with suppress(ValueError):
+ path = Path(file).resolve().relative_to(self.build_lib)
+ return str(path).replace(os.sep, '/')
+ return None
- def _build_ext(self):
- if not self.dist.has_ext_modules():
- return
+ def _create_file(self, relative_output: str, src_file: str, link=None):
+ dest = self.auxiliary_dir / relative_output
+ if not dest.parent.is_dir():
+ dest.parent.mkdir(parents=True)
+ self._file(src_file, dest, link=link)
+
+ def _create_links(self, outputs, output_mapping):
+ link_type = "sym" if _can_symlink_files() 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
+
+ for output in outputs:
+ relative = self._normalize_output(output)
+ if relative and relative not in mappings:
+ self._create_file(relative, output)
+
+ for relative, src in mappings.items():
+ self._create_file(relative, src, link=link_type)
+
+ def __enter__(self):
+ msg = "Strict editable install will be performed using a link tree.\n"
+ _logger.warning(msg + _STRICT_WARNING)
+ return self
- build_ext = self.dist.get_command_obj("build_ext")
- build_ext.ensure_finalized()
- # Extensions are not editable, so we just have to build them in the right dir
- build_ext.run()
+ def __exit__(self, _exc_type, _exc_value, _traceback):
+ msg = f"""\n
+ Strict editable installation performed using the auxiliary directory:
+ {self.auxiliary_dir}
- def __call__(self, unpacked_wheel_dir: Path):
- _configure_build(self.name, self.dist, self.auxiliary_build_dir, self.tmp)
- self._build_py()
- self._build_ext()
- super().__call__(unpacked_wheel_dir)
+ Please be careful to not remove this directory, otherwise you might not be able
+ to import/use your package.
+ """
+ warnings.warn(msg, InformationOnly)
class _TopLevelFinder:
@@ -252,7 +375,7 @@ class _TopLevelFinder:
self.dist = dist
self.name = name
- def __call__(self, unpacked_wheel_dir: Path):
+ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, 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 {}
@@ -265,51 +388,38 @@ class _TopLevelFinder:
name = f"__editable__.{self.name}.finder"
finder = _make_identifier(name)
- content = _finder_template(name, roots, namespaces_)
- Path(unpacked_wheel_dir, f"{finder}.py").write_text(content, encoding="utf-8")
-
- pth = f"__editable__.{self.name}.pth"
- content = f"import {finder}; {finder}.install()"
- Path(unpacked_wheel_dir, pth).write_text(content, encoding="utf-8")
-
-
-def _configure_build(name: str, dist: Distribution, target_dir: _Path, tmp_dir: _Path):
- target = str(target_dir)
- data = str(Path(target_dir, f"{name}.data", "data"))
- headers = str(Path(target_dir, f"{name}.data", "include"))
- scripts = str(Path(target_dir, f"{name}.data", "scripts"))
+ content = bytes(_finder_template(name, roots, namespaces_), "utf-8")
+ wheel.writestr(f"{finder}.py", content)
- # egg-info will be generated again to create a manifest (used for package data)
- egg_info = dist.reinitialize_command("egg_info", reinit_subcommands=True)
- egg_info.egg_base = str(tmp_dir)
- egg_info.ignore_egg_info_in_manifest = True
+ content = bytes(f"import {finder}; {finder}.install()", "utf-8")
+ wheel.writestr(f"__editable__.{self.name}.pth", content)
- build = dist.reinitialize_command("build", reinit_subcommands=True)
- install = dist.reinitialize_command("install", reinit_subcommands=True)
-
- build.build_platlib = build.build_purelib = build.build_lib = target
- install.install_purelib = install.install_platlib = install.install_lib = target
- install.install_scripts = build.build_scripts = scripts
- install.install_headers = headers
- install.install_data = data
-
- build.build_temp = str(tmp_dir)
-
- build_py = dist.get_command_obj("build_py")
- build_py.compile = False
+ def __enter__(self):
+ msg = "Editable install will be performed using a meta path finder.\n"
+ _logger.warning(msg + _LAX_WARNING)
+ return self
- build.ensure_finalized()
- install.ensure_finalized()
+ def __exit__(self, _exc_type, _exc_value, _traceback):
+ ...
-def _can_symlink_files():
- try:
- with TemporaryDirectory() as tmp:
- path1, path2 = Path(tmp, "file1.txt"), Path(tmp, "file2.txt")
- path1.write_text("file1", encoding="utf-8")
+def _can_symlink_files() -> bool:
+ with TemporaryDirectory() as tmp:
+ path1, path2 = Path(tmp, "file1.txt"), Path(tmp, "file2.txt")
+ path1.write_text("file1", encoding="utf-8")
+ with suppress(AttributeError, NotImplementedError, OSError):
os.symlink(path1, path2)
- return path2.is_symlink() and path2.read_text(encoding="utf-8") == "file1"
- except (AttributeError, NotImplementedError, OSError):
+ if path2.is_symlink() and path2.read_text(encoding="utf-8") == "file1":
+ return True
+
+ try:
+ os.link(path1, path2) # Ensure hard links can be created
+ except Exception as ex:
+ msg = (
+ "File system does not seem to support either symlinks or hard links. "
+ "Strict editable installs require one of them to be supported."
+ )
+ raise LinksNotSupported(msg) from ex
return False
@@ -336,6 +446,8 @@ def _simple_layout(
False
>>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a.a1.a2": "_a2"}, ".")
False
+ >>> _simple_layout(['a', 'a.b'], {"": "src", "a.b": "_ab"}, "/tmp/myproj")
+ False
"""
layout = {
pkg: find_package_path(pkg, package_dir, project_dir)
@@ -601,3 +713,7 @@ class InformationOnly(UserWarning):
The only thing that might work is a warning, although it is not the
most appropriate tool for the job...
"""
+
+
+class LinksNotSupported(errors.FileError):
+ """File system does not seem to support either symlinks or hard links."""