From 56c0b10ef337de9eb229fc7f20c4588e20481b4b Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Tue, 28 Apr 2020 00:23:54 -0400 Subject: [PATCH 1/3] prototype pep517-style editable --- src/pip/_internal/operations/install/wheel.py | 47 +++++++++++++++- src/pip/_internal/req/req_install.py | 55 ++++++++++++++----- src/pip/_vendor/pep517/_in_process.py | 10 ++++ src/pip/_vendor/pep517/wrappers.py | 15 ++++- 4 files changed, 111 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 0b3fbe2ffc2..14dfb4ff5c4 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -314,10 +314,53 @@ 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_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..7f6485459b0 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_unpacked_parsed_wheel 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,47 @@ 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'): + + editable_path = self.pep517_backend.build_editable() + + # create empty RECORD + with open(os.path.join(self.metadata_directory, 'RECORD'), 'w+'): + pass + + wheeldir = os.path.dirname(self.metadata_directory) + + # put .pth file in wheeldir + with open(os.path.join(wheeldir, self.name + '.pth'), 'w+') as pthfile: + pthfile.write(editable_path['src_root'] + '\n') + + install_unpacked_parsed_wheel( + self.name, + wheeldir, + self.metadata_directory, + { + "Root-Is-Purelib" : "false", # shouldn't matter + }, + 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..09153f26252 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(): + """Invoke the optional build_editable hook.""" + backend = _build_backend() + try: + return backend.build_editable() + 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..62424b6c7f2 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,19 @@ def build_sdist(self, sdist_directory, config_settings=None): 'config_settings': config_settings, }) + + def build_editable(self): + """Build this project for editable. Usually in-place. + + Returns a dict with the source directory (point .pth here) + + This calls the 'build_editable' backend hook in a subprocess. + """ + editable = self._call_hook('build_editable', {}) + editable['src_root'] = normpath(pjoin(self.source_dir, editable['src_root'])) + return editable + + 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 From fc5341993c184447805d412bb7a0cb92c28425c5 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 7 May 2020 10:23:13 -0400 Subject: [PATCH 2/3] update build_editable hook --- src/pip/_internal/req/req_install.py | 20 +++++++++++--------- src/pip/_vendor/pep517/_in_process.py | 4 ++-- src/pip/_vendor/pep517/wrappers.py | 15 ++++++++++----- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 7f6485459b0..b25d6e51170 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -779,21 +779,23 @@ def install( if self.editable: if self.metadata_directory and self.metadata_directory.endswith('.dist-info'): - editable_path = self.pep517_backend.build_editable() + # 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, + {} + ) - # create empty RECORD + # create empty RECORD. To make install_unpacked_parsed_wheel work. with open(os.path.join(self.metadata_directory, 'RECORD'), 'w+'): pass - wheeldir = os.path.dirname(self.metadata_directory) - - # put .pth file in wheeldir - with open(os.path.join(wheeldir, self.name + '.pth'), 'w+') as pthfile: - pthfile.write(editable_path['src_root'] + '\n') - install_unpacked_parsed_wheel( self.name, - wheeldir, + metadata_directory, self.metadata_directory, { "Root-Is-Purelib" : "false", # shouldn't matter diff --git a/src/pip/_vendor/pep517/_in_process.py b/src/pip/_vendor/pep517/_in_process.py index 09153f26252..b6c92249a09 100644 --- a/src/pip/_vendor/pep517/_in_process.py +++ b/src/pip/_vendor/pep517/_in_process.py @@ -238,11 +238,11 @@ def build_sdist(sdist_directory, config_settings): raise GotUnsupportedOperation(traceback.format_exc()) -def build_editable(): +def build_editable(metadata_directory, target_directory, config_settings): """Invoke the optional build_editable hook.""" backend = _build_backend() try: - return backend.build_editable() + return backend.build_editable(metadata_directory, target_directory, config_settings) except getattr(backend, 'UnsupportedOperation', _DummyException): raise GotUnsupportedOperation(traceback.format_exc()) diff --git a/src/pip/_vendor/pep517/wrappers.py b/src/pip/_vendor/pep517/wrappers.py index 62424b6c7f2..33e3112fa3c 100644 --- a/src/pip/_vendor/pep517/wrappers.py +++ b/src/pip/_vendor/pep517/wrappers.py @@ -226,16 +226,21 @@ def build_sdist(self, sdist_directory, config_settings=None): }) - def build_editable(self): + def build_editable(self, metadata_directory, target_directory, config_settings=None): """Build this project for editable. Usually in-place. - Returns a dict with the source directory (point .pth here) + 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. """ - editable = self._call_hook('build_editable', {}) - editable['src_root'] = normpath(pjoin(self.source_dir, editable['src_root'])) - return editable + 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): From 26ac4c4cbcb29edb9ccd1de7daab551e097d5185 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 7 May 2020 15:13:57 -0400 Subject: [PATCH 3/3] support uninstall for editable --- src/pip/_internal/operations/install/wheel.py | 51 +++++++++++++++++++ src/pip/_internal/req/req_install.py | 11 +--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 14dfb4ff5c4..cc5b647e173 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -329,6 +329,57 @@ def install_unpacked_wheel( ) +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 diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index b25d6e51170..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, install_unpacked_parsed_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 @@ -789,17 +789,10 @@ def install( {} ) - # create empty RECORD. To make install_unpacked_parsed_wheel work. - with open(os.path.join(self.metadata_directory, 'RECORD'), 'w+'): - pass - - install_unpacked_parsed_wheel( + install_editable( self.name, metadata_directory, self.metadata_directory, - { - "Root-Is-Purelib" : "false", # shouldn't matter - }, scheme=scheme, req_description=str(self.req), pycompile=pycompile,