diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 0b3fbe2ffc2..cc5b647e173 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -314,10 +314,104 @@ def install_unpacked_wheel( # TODO: Look into moving this into a dedicated class for representing an # installation. - source = wheeldir.rstrip(os.path.sep) + os.path.sep - info_dir, metadata = parse_wheel(wheel_zip, name) + return install_unpacked_parsed_wheel( + name, + wheeldir, + info_dir, + metadata, + scheme, + req_description, + pycompile=pycompile, + warn_script_location=warn_script_location, + direct_url=direct_url + ) + + +def install_editable( + name, # type: str + wheeldir, # type: str + info_dir, # type: str + scheme, # type: Scheme + req_description, # type: str + pycompile=True, # type: bool + warn_script_location=True, # type: bool + direct_url=None, # type: Optional[DirectUrl] +): # type: (...) -> None + """Install a directory with a .dist-info folder but no RECORD for + editable installs. + + :param name: Name of the project to install + :param wheeldir: Base directory of the unpacked wheel + :param info_dir: Wheel's .dist-info directory + :param scheme: Distutils scheme dictating the install directories + :param req_description: String used in place of the requirement, for + logging + :param pycompile: Whether to byte-compile installed Python files + :param warn_script_location: Whether to check that scripts are installed + into a directory on PATH + :raises UnsupportedWheel: + * when the directory holds an unpacked wheel with incompatible + Wheel-Version + * when the .dist-info dir does not match the wheel + """ + + # Create RECORD. install_unpacked_parsed_wheel will copy but won't remember + # files not in RECORD. + record_path = os.path.join(info_dir, 'RECORD') + with open(record_path, **csv_io_kwargs('w+')) as record_file: + writer = csv.writer(record_file) + for dir, _, files in os.walk(wheeldir): + writer.writerows(((normpath(os.path.join(dir, f), wheeldir), '', '')) for f in files) + + return install_unpacked_parsed_wheel( + name, + wheeldir, + info_dir, + { + "Root-Is-Purelib" : "false", # shouldn't matter + }, + scheme, + req_description, + pycompile=pycompile, + warn_script_location=warn_script_location, + direct_url=direct_url + ) + + +def install_unpacked_parsed_wheel( + name, # type: str + wheeldir, # type: str + info_dir, # type: str + metadata, # type: Message + scheme, # type: Scheme + req_description, # type: str + pycompile=True, # type: bool + warn_script_location=True, # type: bool + direct_url=None, # type: Optional[DirectUrl] +): + # type: (...) -> None + """Install a wheel. + + :param name: Name of the project to install + :param wheeldir: Base directory of the unpacked wheel + :param info_dir: Wheel's .dist-info directory + :param metadata: Wheel's parsed WHEEL-file + :param scheme: Distutils scheme dictating the install directories + :param req_description: String used in place of the requirement, for + logging + :param pycompile: Whether to byte-compile installed Python files + :param warn_script_location: Whether to check that scripts are installed + into a directory on PATH + :raises UnsupportedWheel: + * when the directory holds an unpacked wheel with incompatible + Wheel-Version + * when the .dist-info dir does not match the wheel + """ + + source = wheeldir.rstrip(os.path.sep) + os.path.sep + if wheel_root_is_purelib(metadata): lib_dir = scheme.purelib else: diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 3b28209b1bd..174acfead12 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -27,7 +27,7 @@ install_editable as install_editable_legacy from pip._internal.operations.install.legacy import LegacyInstallFailure from pip._internal.operations.install.legacy import install as install_legacy -from pip._internal.operations.install.wheel import install_wheel +from pip._internal.operations.install.wheel import install_wheel, install_editable from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.deprecation import deprecated @@ -777,18 +777,42 @@ def install( global_options = global_options if global_options is not None else [] if self.editable: - install_editable_legacy( - install_options, - global_options, - prefix=prefix, - home=home, - use_user_site=use_user_site, - name=self.name, - setup_py_path=self.setup_py_path, - isolated=self.isolated, - build_env=self.build_env, - unpacked_source_directory=self.unpacked_source_directory, - ) + if self.metadata_directory and self.metadata_directory.endswith('.dist-info'): + + # XXX the metadata_directory passed to + # 'prepare_metadata_for_build_wheel' is the parent + metadata_directory = os.path.dirname(self.metadata_directory) + + self.pep517_backend.build_editable( + metadata_directory, + scheme.platlib, + {} + ) + + install_editable( + self.name, + metadata_directory, + self.metadata_directory, + scheme=scheme, + req_description=str(self.req), + pycompile=pycompile, + warn_script_location=warn_script_location, + direct_url=None, + ) + + else: + install_editable_legacy( + install_options, + global_options, + prefix=prefix, + home=home, + use_user_site=use_user_site, + name=self.name, + setup_py_path=self.setup_py_path, + isolated=self.isolated, + build_env=self.build_env, + unpacked_source_directory=self.unpacked_source_directory, + ) self.install_succeeded = True return diff --git a/src/pip/_vendor/pep517/_in_process.py b/src/pip/_vendor/pep517/_in_process.py index a536b03e6bb..b6c92249a09 100644 --- a/src/pip/_vendor/pep517/_in_process.py +++ b/src/pip/_vendor/pep517/_in_process.py @@ -238,12 +238,22 @@ def build_sdist(sdist_directory, config_settings): raise GotUnsupportedOperation(traceback.format_exc()) +def build_editable(metadata_directory, target_directory, config_settings): + """Invoke the optional build_editable hook.""" + backend = _build_backend() + try: + return backend.build_editable(metadata_directory, target_directory, config_settings) + except getattr(backend, 'UnsupportedOperation', _DummyException): + raise GotUnsupportedOperation(traceback.format_exc()) + + HOOK_NAMES = { 'get_requires_for_build_wheel', 'prepare_metadata_for_build_wheel', 'build_wheel', 'get_requires_for_build_sdist', 'build_sdist', + 'build_editable', } diff --git a/src/pip/_vendor/pep517/wrappers.py b/src/pip/_vendor/pep517/wrappers.py index 00a3d1a789f..33e3112fa3c 100644 --- a/src/pip/_vendor/pep517/wrappers.py +++ b/src/pip/_vendor/pep517/wrappers.py @@ -1,7 +1,7 @@ import threading from contextlib import contextmanager import os -from os.path import dirname, abspath, join as pjoin +from os.path import dirname, abspath, normpath, join as pjoin import shutil from subprocess import check_call, check_output, STDOUT import sys @@ -225,6 +225,24 @@ def build_sdist(self, sdist_directory, config_settings=None): 'config_settings': config_settings, }) + + def build_editable(self, metadata_directory, target_directory, config_settings=None): + """Build this project for editable. Usually in-place. + + Copy files into metadata_directory to effect an editable distribution. + The metadata_directory has the layout of the root directory of a wheel + and has been populated with prepare_metadata_for_build_wheel(). + It will be copied into target_directory on PYTHONPATH, usually site-packages. + + This calls the 'build_editable' backend hook in a subprocess. + """ + return self._call_hook('build_editable', { + 'metadata_directory': metadata_directory, + 'target_directory': target_directory, + 'config_settings': config_settings, + }) + + def _call_hook(self, hook_name, kwargs): # On Python 2, pytoml returns Unicode values (which is correct) but the # environment passed to check_call needs to contain string values. We