diff --git a/.mailmap b/.mailmap index c8f94a9d8c9..4206a022a9d 100644 --- a/.mailmap +++ b/.mailmap @@ -19,6 +19,7 @@ Dongweiming Endoh Takanao Erik M. Bray +Ee Durbin Gabriel de Perthuis Hsiaoming Yang Hugo van Kemenade Hugo diff --git a/AUTHORS.txt b/AUTHORS.txt index a65fc021997..289eca44572 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -204,6 +204,7 @@ DrFeathers Dustin Ingram Dwayne Bailey Ed Morley +Ee Durbin Eitan Adler ekristina elainechan @@ -222,8 +223,6 @@ Eric Hanchrow Eric Hopper Erik M. Bray Erik Rose -Ernest W Durbin III -Ernest W. Durbin III Erwin Janssen Eugene Vereshchagin everdimension diff --git a/NEWS.rst b/NEWS.rst index 4f48ad2efa5..3563cfcd036 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,31 @@ .. towncrier release notes start +21.3.1 (2021-10-22) +=================== + + +Bug Fixes +--------- + + +- Always refuse installing or building projects that have no ``pyproject.toml`` nor + ``setup.py``. (`#10531 `_) +- Tweak running-as-root detection, to check ``os.getuid`` if it exists, on Unix-y and non-Linux/non-MacOS machines. (`#10565 `_) +- When installing projects with a ``pyproject.toml`` in editable mode, and the build + backend does not support :pep:`660`, prepare metadata using + ``prepare_metadata_for_build_wheel`` instead of ``setup.py egg_info``. Also, refuse + installing projects that only have a ``setup.cfg`` and no ``setup.py`` nor + ``pyproject.toml``. These restore the pre-21.3 behaviour. (`#10573 `_) +- Restore compatibility of where configuration files are loaded from on MacOS (back to ``Library/Application Support/pip``, instead of ``Preferences/pip``). (`#10585 `_) + +Vendored Libraries +------------------ + + +- Upgrade pep517 to 0.12.0 + + 21.3 (2021-10-11) ================= diff --git a/docs/html/news.rst b/docs/html/news.rst index 829e6b74fd8..af1d10479a5 100644 --- a/docs/html/news.rst +++ b/docs/html/news.rst @@ -7,6 +7,6 @@ Changelog Major and minor releases of pip also include changes listed within prior beta releases. -.. towncrier-draft-entries:: |release|, unreleased as on +.. towncrier-draft-entries:: Not yet released .. pip-news-include:: ../../NEWS.rst diff --git a/docs/html/reference/build-system/setup-py.md b/docs/html/reference/build-system/setup-py.md index 722a05fa9ae..53917b8a4c8 100644 --- a/docs/html/reference/build-system/setup-py.md +++ b/docs/html/reference/build-system/setup-py.md @@ -54,7 +54,9 @@ depending on the package), the generated wheel is added to pip's wheel cache and will be used for this installation. The built wheel is cached locally by pip to avoid repeated identical builds. -If this wheel generation fails, pip will attempt a direct installation instead. +If this wheel generation fails, pip runs `setup.py clean` to clean up any build +artifacts that may have been generated. After that, pip will attempt a direct +installation. ### Direct Installation @@ -70,11 +72,6 @@ For installing packages in "editable" mode `setup.py develop`, which will use setuptools' mechanisms to perform an editable/development installation. -### Cleanup - -After attempting installation, pip may run `setup.py clean` to clean up build -artifacts from that setuptools has generated. - ## Setuptools Injection To support projects that directly use `distutils`, pip injects `setuptools` into diff --git a/docs/html/topics/configuration.md b/docs/html/topics/configuration.md index eb392ecd493..2d63ac1413f 100644 --- a/docs/html/topics/configuration.md +++ b/docs/html/topics/configuration.md @@ -29,11 +29,10 @@ complexity for backwards compatibility reasons. ```{tab} Unix Global -: {file}`/etc/pip.conf` +: In a "pip" subdirectory of any of the paths set in the environment variable + `XDG_CONFIG_DIRS` (if it exists), for example {file}`/etc/xdg/pip/pip.conf`. - Alternatively, it may be in a "pip" subdirectory of any of the paths set - in the environment variable `XDG_CONFIG_DIRS` (if it exists), for - example {file}`/etc/xdg/pip/pip.conf`. + This will be followed by loading {file}`/etc/pip.conf`. User : {file}`$HOME/.config/pip/pip.conf`, which respects the `XDG_CONFIG_HOME` environment variable. diff --git a/docs/requirements.txt b/docs/requirements.txt index e311562e669..8c71de35d74 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx ~= 4.1.0 +sphinx ~= 4.2 towncrier furo myst_parser diff --git a/noxfile.py b/noxfile.py index df42af8b8f5..5b5a66d5307 100644 --- a/noxfile.py +++ b/noxfile.py @@ -171,7 +171,7 @@ def lint(session: nox.Session) -> None: @nox.session def vendoring(session: nox.Session) -> None: - session.install("vendoring~=1.0.0") + session.install("vendoring~=1.2.0") if "--upgrade" not in session.posargs: session.run("vendoring", "sync", "-v") diff --git a/pyproject.toml b/pyproject.toml index fac27944798..53343bfced0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,13 +3,19 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [tool.towncrier] +# For finding the __version__ package = "pip" package_dir = "src" +# For writing into the correct file filename = "NEWS.rst" +# For finding the news fragments directory = "news/" -title_format = "{version} ({project_date})" + +# For rendering properly for this project issue_format = "`#{issue} `_" template = "tools/news/template.rst" + +# Grouping of entries, within our changelog type = [ { name = "Process", directory = "process", showcontent = true }, { name = "Deprecations and Removals", directory = "removal", showcontent = true }, @@ -54,4 +60,6 @@ distro = [] setuptools = "pkg_resources" [tool.vendoring.license.fallback-urls] +CacheControl = "https://raw.githubusercontent.com/ionrock/cachecontrol/v0.12.6/LICENSE.txt" +distlib = "https://bitbucket.org/pypa/distlib/raw/master/LICENSE.txt" webencodings = "https://github.com/SimonSapin/python-webencodings/raw/master/LICENSE" diff --git a/src/pip/__init__.py b/src/pip/__init__.py index e6de28e434c..09e946fbe1f 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1,6 +1,6 @@ from typing import List, Optional -__version__ = "21.3" +__version__ = "22.0.dev0" def main(args: Optional[List[str]] = None) -> int: diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index c224bd276e2..dbd15cbce63 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -173,9 +173,10 @@ def warn_if_run_as_root() -> None: # checks: https://mypy.readthedocs.io/en/stable/common_issues.html if sys.platform == "win32" or sys.platform == "cygwin": return - if sys.platform == "darwin" or sys.platform == "linux": - if os.getuid() != 0: - return + + if os.getuid() != 0: + return + logger.warning( "Running pip as the 'root' user can result in broken permissions and " "conflicting behaviour with the system package manager. " diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py index b4e2892931b..cd85ac5c439 100644 --- a/src/pip/_internal/distributions/sdist.py +++ b/src/pip/_internal/distributions/sdist.py @@ -32,21 +32,22 @@ def prepare_distribution_metadata( # Set up the build isolation, if this requirement should be isolated should_isolate = self.req.use_pep517 and build_isolation if should_isolate: - self._setup_isolation(finder) + # Setup an isolated environment and install the build backend static + # requirements in it. + self._prepare_build_backend(finder) + # Check that if the requirement is editable, it either supports PEP 660 or + # has a setup.py or a setup.cfg. This cannot be done earlier because we need + # to setup the build backend to verify it supports build_editable, nor can + # it be done later, because we want to avoid installing build requirements + # needlessly. Doing it here also works around setuptools generating + # UNKNOWN.egg-info when running get_requires_for_build_wheel on a directory + # without setup.py nor setup.cfg. + self.req.isolated_editable_sanity_check() + # Install the dynamic build requirements. + self._install_build_reqs(finder) self.req.prepare_metadata() - def _setup_isolation(self, finder: PackageFinder) -> None: - self._prepare_build_backend(finder) - # Install any extra build dependencies that the backend requests. - # This must be done in a second pass, as the pyproject.toml - # dependencies must be installed before we can call the backend. - if self.req.editable and self.req.permit_editable_wheels: - build_reqs = self._get_build_requires_editable() - else: - build_reqs = self._get_build_requires_wheel() - self._install_build_reqs(finder, build_reqs) - def _prepare_build_backend(self, finder: PackageFinder) -> None: # Isolate in a BuildEnvironment and install the build-time # requirements. @@ -91,8 +92,19 @@ def _get_build_requires_editable(self) -> Iterable[str]: with backend.subprocess_runner(runner): return backend.get_requires_for_build_editable() - def _install_build_reqs(self, finder: PackageFinder, reqs: Iterable[str]) -> None: - conflicting, missing = self.req.build_env.check_requirements(reqs) + def _install_build_reqs(self, finder: PackageFinder) -> None: + # Install any extra build dependencies that the backend requests. + # This must be done in a second pass, as the pyproject.toml + # dependencies must be installed before we can call the backend. + if ( + self.req.editable + and self.req.permit_editable_wheels + and self.req.supports_pyproject_editable() + ): + build_reqs = self._get_build_requires_editable() + else: + build_reqs = self._get_build_requires_wheel() + conflicting, missing = self.req.build_env.check_requirements(build_reqs) if conflicting: self._raise_conflicts("the backend dependencies", conflicting) self.req.build_env.install_requirements( diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index e99af4697c9..7d12438d6ed 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -23,9 +23,7 @@ def generate_metadata(build_env: BuildEnvironment, backend: Pep517HookCaller) -> # Note that Pep517HookCaller implements a fallback for # prepare_metadata_for_build_wheel, so we don't have to # consider the possibility that this hook doesn't exist. - runner = runner_with_spinner_message( - "Preparing wheel metadata (pyproject.toml)" - ) + runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)") with backend.subprocess_runner(runner): distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index ccf034eb5a0..34cf9a51b6e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -348,6 +348,7 @@ def _ensure_link_req_src_dir( # installation. # FIXME: this won't upgrade when there's an existing # package unpacked in `req.source_dir` + # TODO: this check is now probably dead code if is_installable_dir(req.source_dir): raise PreviousBuildDirError( "pip can't proceed with requirements '{}' due to a" diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 0b3a6cde64f..31534a3a9d3 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -48,6 +48,12 @@ def load_pyproject_toml( has_pyproject = os.path.isfile(pyproject_toml) has_setup = os.path.isfile(setup_py) + if not has_pyproject and not has_setup: + raise InstallationError( + f"{req_name} does not appear to be a Python project: " + f"neither 'setup.py' nor 'pyproject.toml' found." + ) + if has_pyproject: with open(pyproject_toml, encoding="utf-8") as f: pp_toml = tomli.load(f) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 5cf923515d7..4a594037fd1 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -235,6 +235,8 @@ def _get_url_from_path(path: str, name: str) -> Optional[str]: if _looks_like_path(name) and os.path.isdir(path): if is_installable_dir(path): return path_to_url(path) + # TODO: The is_installable_dir test here might not be necessary + # now that it is done in load_pyproject_toml too. raise InstallationError( f"Directory {name!r} is not installable. Neither 'setup.py' " "nor 'pyproject.toml' found." diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ff0dd2f2d58..95dacab53ef 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -1,6 +1,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +import functools import logging import os import shutil @@ -16,7 +17,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version from pip._vendor.packaging.version import parse as parse_version -from pip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller +from pip._vendor.pep517.wrappers import Pep517HookCaller from pip._vendor.pkg_resources import Distribution from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment @@ -53,6 +54,7 @@ redact_auth_from_url, ) from pip._internal.utils.packaging import get_metadata +from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds from pip._internal.utils.virtualenv import running_under_virtualenv from pip._internal.vcs import vcs @@ -196,11 +198,6 @@ def __init__( # but after loading this flag should be treated as read only. self.use_pep517 = use_pep517 - # supports_pyproject_editable will be set to True or False when we try - # to prepare editable metadata or build an editable wheel. None means - # "we don't know yet". - self.supports_pyproject_editable: Optional[bool] = None - # This requirement needs more preparation before it can be built self.needs_more_preparation = False @@ -247,6 +244,18 @@ def name(self) -> Optional[str]: return None return pkg_resources.safe_name(self.req.name) + @functools.lru_cache() # use cached_property in python 3.8+ + def supports_pyproject_editable(self) -> bool: + if not self.use_pep517: + return False + assert self.pep517_backend + with self.build_env: + runner = runner_with_spinner_message( + "Checking if build backend supports build_editable" + ) + with self.pep517_backend.subprocess_runner(runner): + return "build_editable" in self.pep517_backend._supported_features() + @property def specifier(self) -> SpecifierSet: return self.req.specifier @@ -503,93 +512,59 @@ def load_pyproject_toml(self) -> None: backend_path=backend_path, ) - def _generate_editable_metadata(self) -> str: - """Invokes metadata generator functions, with the required arguments.""" - if self.use_pep517: - assert self.pep517_backend is not None - try: - metadata_directory = generate_editable_metadata( - build_env=self.build_env, - backend=self.pep517_backend, - ) - except HookMissing as e: - self.supports_pyproject_editable = False - if not os.path.exists(self.setup_py_path) and not os.path.exists( - self.setup_cfg_path - ): - raise InstallationError( - f"Project {self} has a 'pyproject.toml' and its build " - f"backend is missing the {e} hook. Since it does not " - f"have a 'setup.py' nor a 'setup.cfg', " - f"it cannot be installed in editable mode. " - f"Consider using a build backend that supports PEP 660." - ) - # At this point we have determined that the build_editable hook - # is missing, and there is a setup.py or setup.cfg - # so we fallback to the legacy metadata generation - logger.info( - "Build backend does not support editables, " - "falling back to setup.py egg_info." - ) - else: - self.supports_pyproject_editable = True - return metadata_directory - elif not os.path.exists(self.setup_py_path) and not os.path.exists( - self.setup_cfg_path - ): - raise InstallationError( - f"File 'setup.py' or 'setup.cfg' not found " - f"for legacy project {self}. " - f"It cannot be installed in editable mode." - ) - - return generate_metadata_legacy( - build_env=self.build_env, - setup_py_path=self.setup_py_path, - source_dir=self.unpacked_source_directory, - isolated=self.isolated, - details=self.name or f"from {self.link}", - ) + def isolated_editable_sanity_check(self) -> None: + """Check that an editable requirement if valid for use with PEP 517/518. - def _generate_metadata(self) -> str: - """Invokes metadata generator functions, with the required arguments.""" - if self.use_pep517: - assert self.pep517_backend is not None - try: - return generate_metadata( - build_env=self.build_env, - backend=self.pep517_backend, - ) - except HookMissing as e: - raise InstallationError( - f"Project {self} has a pyproject.toml but its build " - f"backend is missing the required {e} hook." - ) - elif not os.path.exists(self.setup_py_path): + This verifies that an editable that has a pyproject.toml either supports PEP 660 + or as a setup.py or a setup.cfg + """ + if ( + self.editable + and self.use_pep517 + and not self.supports_pyproject_editable() + and not os.path.isfile(self.setup_py_path) + and not os.path.isfile(self.setup_cfg_path) + ): raise InstallationError( - f"File 'setup.py' not found for legacy project {self}." + f"Project {self} has a 'pyproject.toml' and its build " + f"backend is missing the 'build_editable' hook. Since it does not " + f"have a 'setup.py' nor a 'setup.cfg', " + f"it cannot be installed in editable mode. " + f"Consider using a build backend that supports PEP 660." ) - return generate_metadata_legacy( - build_env=self.build_env, - setup_py_path=self.setup_py_path, - source_dir=self.unpacked_source_directory, - isolated=self.isolated, - details=self.name or f"from {self.link}", - ) - def prepare_metadata(self) -> None: """Ensure that project metadata is available. - Under PEP 517, call the backend hook to prepare the metadata. + Under PEP 517 and PEP 660, call the backend hook to prepare the metadata. Under legacy processing, call setup.py egg-info. """ assert self.source_dir - if self.editable and self.permit_editable_wheels: - self.metadata_directory = self._generate_editable_metadata() + if self.use_pep517: + assert self.pep517_backend is not None + if ( + self.editable + and self.permit_editable_wheels + and self.supports_pyproject_editable() + ): + self.metadata_directory = generate_editable_metadata( + build_env=self.build_env, + backend=self.pep517_backend, + ) + else: + self.metadata_directory = generate_metadata( + build_env=self.build_env, + backend=self.pep517_backend, + ) else: - self.metadata_directory = self._generate_metadata() + self.metadata_directory = generate_metadata_legacy( + build_env=self.build_env, + setup_py_path=self.setup_py_path, + source_dir=self.unpacked_source_directory, + isolated=self.isolated, + details=self.name or f"from {self.link}", + ) # Act on the newly generated metadata, based on the name and version. if not self.name: diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index 5e334126b61..16933bf8afe 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -17,24 +17,36 @@ def user_cache_dir(appname: str) -> str: return _appdirs.user_cache_dir(appname, appauthor=False) +def _macos_user_config_dir(appname: str, roaming: bool = True) -> str: + # Use ~/Application Support/pip, if the directory exists. + path = _appdirs.user_data_dir(appname, appauthor=False, roaming=roaming) + if os.path.isdir(path): + return path + + # Use a Linux-like ~/.config/pip, by default. + linux_like_path = "~/.config/" + if appname: + linux_like_path = os.path.join(linux_like_path, appname) + + return os.path.expanduser(linux_like_path) + + def user_config_dir(appname: str, roaming: bool = True) -> str: - path = _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming) - if sys.platform == "darwin" and not os.path.isdir(path): - path = os.path.expanduser("~/.config/") - if appname: - path = os.path.join(path, appname) - return path + if sys.platform == "darwin": + return _macos_user_config_dir(appname, roaming) + + return _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming) # for the discussion regarding site_config_dir locations # see def site_config_dirs(appname: str) -> List[str]: - dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True) if sys.platform == "darwin": - # always look in /Library/Application Support/pip as well - return dirval.split(os.pathsep) + ["/Library/Application Support/pip"] - elif sys.platform == "win32": + return [_appdirs.site_data_dir(appname, appauthor=False, multipath=True)] + + dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True) + if sys.platform == "win32": return [dirval] - else: - # always look in /etc directly as well - return dirval.split(os.pathsep) + ["/etc"] + + # Unix-y system. Look in /etc as well. + return dirval.split(os.pathsep) + ["/etc"] diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index b4855a9a7fb..a9123a0f1f6 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -71,10 +71,8 @@ def _should_build( return False if req.editable: - if req.use_pep517 and req.supports_pyproject_editable is not False: - return True - # we don't build legacy editable requirements - return False + # we only build PEP 660 editable requirements + return req.supports_pyproject_editable() if req.use_pep517: return True diff --git a/src/pip/_vendor/pep517/__init__.py b/src/pip/_vendor/pep517/__init__.py index f064d60c8b9..2b6b8856790 100644 --- a/src/pip/_vendor/pep517/__init__.py +++ b/src/pip/_vendor/pep517/__init__.py @@ -1,6 +1,6 @@ """Wrappers to build Python packages using PEP 517 hooks """ -__version__ = '0.11.0' +__version__ = '0.12.0' from .wrappers import * # noqa: F401, F403 diff --git a/src/pip/_vendor/pep517/build.py b/src/pip/_vendor/pep517/build.py index 3b752145322..bc463b2ba6d 100644 --- a/src/pip/_vendor/pep517/build.py +++ b/src/pip/_vendor/pep517/build.py @@ -31,7 +31,7 @@ def load_system(source_dir): Load the build system from a source dir (pyproject.toml). """ pyproject = os.path.join(source_dir, 'pyproject.toml') - with io.open(pyproject, encoding="utf-8") as f: + with io.open(pyproject, 'rb') as f: pyproject_data = toml_load(f) return pyproject_data['build-system'] diff --git a/src/pip/_vendor/pep517/check.py b/src/pip/_vendor/pep517/check.py index 719be04033f..bf3c722641e 100644 --- a/src/pip/_vendor/pep517/check.py +++ b/src/pip/_vendor/pep517/check.py @@ -142,7 +142,7 @@ def check(source_dir): return False try: - with io.open(pyproject, encoding="utf-8") as f: + with io.open(pyproject, 'rb') as f: pyproject_data = toml_load(f) # Ensure the mandatory data can be loaded buildsys = pyproject_data['build-system'] diff --git a/src/pip/_vendor/pep517/compat.py b/src/pip/_vendor/pep517/compat.py index d5636645bd3..730ef5ffaa1 100644 --- a/src/pip/_vendor/pep517/compat.py +++ b/src/pip/_vendor/pep517/compat.py @@ -1,4 +1,5 @@ """Python 2/3 compatibility""" +import io import json import sys @@ -35,7 +36,15 @@ def read_json(path): if sys.version_info < (3, 6): - from toml import load as toml_load # noqa: F401 + from toml import load as _toml_load # noqa: F401 + + def toml_load(f): + w = io.TextIOWrapper(f, encoding="utf8", newline="") + try: + return _toml_load(w) + finally: + w.detach() + from toml import TomlDecodeError as TOMLDecodeError # noqa: F401 else: from pip._vendor.tomli import load as toml_load # noqa: F401 diff --git a/src/pip/_vendor/pep517/envbuild.py b/src/pip/_vendor/pep517/envbuild.py index 7c2344bf3bf..fe8873c64a9 100644 --- a/src/pip/_vendor/pep517/envbuild.py +++ b/src/pip/_vendor/pep517/envbuild.py @@ -19,7 +19,7 @@ def _load_pyproject(source_dir): with io.open( os.path.join(source_dir, 'pyproject.toml'), - encoding="utf-8", + 'rb', ) as f: pyproject_data = toml_load(f) buildsys = pyproject_data['build-system'] diff --git a/src/pip/_vendor/pep517/in_process/_in_process.py b/src/pip/_vendor/pep517/in_process/_in_process.py index c7f5f0577f8..954a4ab05e9 100644 --- a/src/pip/_vendor/pep517/in_process/_in_process.py +++ b/src/pip/_vendor/pep517/in_process/_in_process.py @@ -103,6 +103,19 @@ def _build_backend(): return obj +def _supported_features(): + """Return the list of options features supported by the backend. + + Returns a list of strings. + The only possible value is 'build_editable'. + """ + backend = _build_backend() + features = [] + if hasattr(backend, "build_editable"): + features.append("build_editable") + return features + + def get_requires_for_build_wheel(config_settings): """Invoke the optional get_requires_for_build_wheel hook @@ -312,6 +325,7 @@ def build_sdist(sdist_directory, config_settings): 'build_editable', 'get_requires_for_build_sdist', 'build_sdist', + '_supported_features', } diff --git a/src/pip/_vendor/pep517/wrappers.py b/src/pip/_vendor/pep517/wrappers.py index 52da22e8257..e031ed70875 100644 --- a/src/pip/_vendor/pep517/wrappers.py +++ b/src/pip/_vendor/pep517/wrappers.py @@ -154,6 +154,10 @@ def subprocess_runner(self, runner): finally: self._subprocess_runner = prev + def _supported_features(self): + """Return the list of optional features supported by the backend.""" + return self._call_hook('_supported_features', {}) + def get_requires_for_build_wheel(self, config_settings=None): """Identify packages required for building a wheel diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 0b74c2bacc2..ab2d6152890 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -1,11 +1,11 @@ -CacheControl==0.12.6 +CacheControl==0.12.6 # Make sure to update the license in pyproject.toml for this. colorama==0.4.4 distlib==0.3.3 distro==1.6.0 html5lib==1.1 msgpack==1.0.2 packaging==21.0 -pep517==0.11.0 +pep517==0.12.0 platformdirs==2.4.0 progress==1.6 pyparsing==2.4.7 diff --git a/tests/data/src/pep517_setup_cfg_only/setup.cfg b/tests/data/src/pep517_setup_cfg_only/setup.cfg new file mode 100644 index 00000000000..4d62ef58d5a --- /dev/null +++ b/tests/data/src/pep517_setup_cfg_only/setup.cfg @@ -0,0 +1,3 @@ +[metadata] +name = "dummy" +version = "0.1" diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 355113e37d3..ac96b1f940e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -653,7 +653,6 @@ def test_install_from_local_directory_with_no_setup_py(script, data): """ result = script.pip("install", data.root, expect_error=True) assert not result.files_created - assert "is not installable." in result.stderr assert "Neither 'setup.py' nor 'pyproject.toml' found." in result.stderr @@ -663,11 +662,10 @@ def test_editable_install__local_dir_no_setup_py(script, data): """ result = script.pip("install", "-e", data.root, expect_error=True) assert not result.files_created - - msg = result.stderr - assert msg.startswith("ERROR: File 'setup.py' or 'setup.cfg' not found ") - assert "cannot be installed in editable mode" in msg - assert "pyproject.toml" not in msg + assert ( + "does not appear to be a Python project: " + "neither 'setup.py' nor 'pyproject.toml' found" in result.stderr + ) def test_editable_install__local_dir_no_setup_py_with_pyproject(script): @@ -689,6 +687,26 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject(script): assert "cannot be installed in editable mode" in msg +def test_editable_install__local_dir_setup_requires_with_pyproject(script, shared_data): + """ + Test installing in editable mode from a local directory with a setup.py + that has setup_requires and a pyproject.toml. + + https://github.com/pypa/pip/issues/10573 + """ + local_dir = script.scratch_path.joinpath("temp") + local_dir.mkdir() + pyproject_path = local_dir.joinpath("pyproject.toml") + pyproject_path.write_text("") + setup_py_path = local_dir.joinpath("setup.py") + setup_py_path.write_text( + "from setuptools import setup\n" + "setup(name='dummy', setup_requires=['simplewheel'])\n" + ) + + script.pip("install", "--find-links", shared_data.find_links, "-e", local_dir) + + @pytest.mark.network def test_upgrade_argparse_shadowed(script): # If argparse is installed - even if shadowed for imported - we support diff --git a/tests/functional/test_pep660.py b/tests/functional/test_pep660.py index 0c1ac746a91..52549f1d3f1 100644 --- a/tests/functional/test_pep660.py +++ b/tests/functional/test_pep660.py @@ -59,19 +59,22 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non # fmt: on -def _make_project(tmpdir, backend_code, with_setup_py): +def _make_project(tmpdir, backend_code, with_setup_py, with_pyproject=True): project_dir = tmpdir / "project" project_dir.mkdir() project_dir.joinpath("setup.cfg").write_text(SETUP_CFG) if with_setup_py: project_dir.joinpath("setup.py").write_text(SETUP_PY) if backend_code: + assert with_pyproject buildsys = {"requires": ["setuptools", "wheel"]} buildsys["build-backend"] = "test_backend" buildsys["backend-path"] = ["."] data = tomli_w.dumps({"build-system": buildsys}) project_dir.joinpath("pyproject.toml").write_text(data) project_dir.joinpath("test_backend.py").write_text(backend_code) + elif with_pyproject: + project_dir.joinpath("pyproject.toml").touch() project_dir.joinpath("log.txt").touch() return project_dir @@ -124,7 +127,8 @@ def test_install_pep660_basic(tmpdir, script, with_wheel): def test_install_no_pep660_setup_py_fallback(tmpdir, script, with_wheel): """ Test that we fall back to setuptools develop when using a backend that - does not support build_editable . + does not support build_editable. Since there is a pyproject.toml, + the prepare_metadata_for_build_wheel hook is called. """ project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=True) result = script.pip( @@ -135,6 +139,7 @@ def test_install_no_pep660_setup_py_fallback(tmpdir, script, with_wheel): project_dir, allow_stderr_warning=False, ) + _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel") assert ( result.test_env.site_packages.joinpath("project.egg-link") in result.files_created @@ -144,7 +149,8 @@ def test_install_no_pep660_setup_py_fallback(tmpdir, script, with_wheel): def test_install_no_pep660_setup_cfg_fallback(tmpdir, script, with_wheel): """ Test that we fall back to setuptools develop when using a backend that - does not support build_editable . + does not support build_editable. Since there is a pyproject.toml, + the prepare_metadata_for_build_wheel hook is called. """ project_dir = _make_project(tmpdir, BACKEND_WITHOUT_PEP660, with_setup_py=False) result = script.pip( @@ -156,6 +162,7 @@ def test_install_no_pep660_setup_cfg_fallback(tmpdir, script, with_wheel): allow_stderr_warning=False, ) print(result.stdout, result.stderr) + _assert_hook_called(project_dir, "prepare_metadata_for_build_wheel") assert ( result.test_env.site_packages.joinpath("project.egg-link") in result.files_created diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py index e59931b6652..cdb15e7a1a3 100644 --- a/tests/unit/test_appdirs.py +++ b/tests/unit/test_appdirs.py @@ -99,7 +99,6 @@ def test_site_config_dirs_osx(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("HOME", "/home/test") assert appdirs.site_config_dirs("pip") == [ - "/Library/Preferences/pip", "/Library/Application Support/pip", ] @@ -107,7 +106,10 @@ def test_site_config_dirs_osx(self, monkeypatch: pytest.MonkeyPatch) -> None: def test_site_config_dirs_linux(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("XDG_CONFIG_DIRS", raising=False) - assert appdirs.site_config_dirs("pip") == ["/etc/xdg/pip", "/etc"] + assert appdirs.site_config_dirs("pip") == [ + "/etc/xdg/pip", + "/etc", + ] @pytest.mark.skipif(sys.platform != "linux", reason="Linux-only test") def test_site_config_dirs_linux_override( @@ -129,7 +131,10 @@ def test_site_config_dirs_linux_empty( ) -> None: monkeypatch.setattr(os, "pathsep", ":") monkeypatch.setenv("XDG_CONFIG_DIRS", "") - assert appdirs.site_config_dirs("pip") == ["/etc/xdg/pip", "/etc"] + assert appdirs.site_config_dirs("pip") == [ + "/etc/xdg/pip", + "/etc", + ] class TestUserConfigDir: diff --git a/tests/unit/test_pep517.py b/tests/unit/test_pep517.py index b18299d7039..6c11ab625ec 100644 --- a/tests/unit/test_pep517.py +++ b/tests/unit/test_pep517.py @@ -27,6 +27,22 @@ def test_use_pep517(shared_data: TestData, source: str, expected: bool) -> None: assert req.use_pep517 is expected +def test_use_pep517_rejects_setup_cfg_only(shared_data: TestData) -> None: + """ + Test that projects with setup.cfg but no pyproject.toml are rejected. + """ + src = shared_data.src.joinpath("pep517_setup_cfg_only") + req = InstallRequirement(None, None) + req.source_dir = src # make req believe it has been unpacked + with pytest.raises(InstallationError) as e: + req.load_pyproject_toml() + err_msg = e.value.args[0] + assert ( + "does not appear to be a Python project: " + "neither 'setup.py' nor 'pyproject.toml' found" in err_msg + ) + + @pytest.mark.parametrize( ("source", "msg"), [ diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 2b8ec2fa94d..9562541ff55 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -39,7 +39,7 @@ def __init__( constraint: bool = False, source_dir: Optional[str] = "/tmp/pip-install-123/pendulum", use_pep517: bool = True, - supports_pyproject_editable: Optional[bool] = None, + supports_pyproject_editable: bool = False, ) -> None: self.name = name self.is_wheel = is_wheel @@ -48,7 +48,10 @@ def __init__( self.constraint = constraint self.source_dir = source_dir self.use_pep517 = use_pep517 - self.supports_pyproject_editable = supports_pyproject_editable + self._supports_pyproject_editable = supports_pyproject_editable + + def supports_pyproject_editable(self) -> bool: + return self._supports_pyproject_editable @pytest.mark.parametrize( @@ -66,7 +69,6 @@ def __init__( # We don't build reqs that are already wheels. (ReqMock(is_wheel=True), False, False), (ReqMock(editable=True, use_pep517=False), False, False), - (ReqMock(editable=True, use_pep517=True), False, True), ( ReqMock(editable=True, use_pep517=True, supports_pyproject_editable=True), False, diff --git a/tools/news/template.rst b/tools/news/template.rst index 91003f20226..fcedf6677c9 100644 --- a/tools/news/template.rst +++ b/tools/news/template.rst @@ -1,41 +1,47 @@ -{% set underline = "=" %} +{# This is a heavily customised version of towncrier's default template. #} -{{ underline * ((top_line)|length) }} -{% for section in sections %} -{% set underline = "-" %} -{% if section %} -{{ section }} -{{ underline * section|length }}{% set underline = "~" %} +{#- + Only render if there's any changes to show. -{% endif %} -{% if sections[section] %} -{% for category, val in definitions.items() if category in sections[section] and category != 'trivial' %} + This serves as a compatibility "hack" since we render unreleased news entries + in our changelog with ``sphinxcontrib.towncrier``; which triggers a render even + when there's no entries to be rendered. +#} +{% if sections[''] %} -{{ definitions[category]['name'] }} -{{ underline * definitions[category]['name']|length }} +{#- Heading for individual version #} +{{ versiondata.version }} ({{ versiondata.date }}) +{{ top_underline * ((versiondata.version + versiondata.date)|length + 3) }} -{% if definitions[category]['showcontent'] %} -{% for text, values in sections[section][category]|dictsort(by='value') %} -- {{ text }}{% if category != 'vendor' and category != 'process' %} ({{ values|sort|join(', ') }}){% endif %} +{# -{% endfor %} -{% else %} -- {{ sections[section][category]['']|sort|join(', ') }} + The following loop will run exactly once, with ``section_name == ""``. + This is due to the undocumented "sections" feature in towncrier. + See https://github.com/twisted/towncrier/issues/61. -{% endif %} -{% if sections[section][category]|length == 0 %} + We don't use this feature, and this template doesn't render the section + heading for that reason. +#} +{% for section_name, entries_by_type in sections.items() -%} +{# Only show types with entries and ``showcontent = true``, using the order from pyproject.toml #} +{% for type_ in definitions if (sections[section_name][type_] and definitions[type_]['showcontent']) %} -No significant changes. +{# Heading for individual types #} +{{ definitions[type_]['name'] }} +{{ underlines[0] * definitions[type_]['name']|length }} +{# This is the loop that generates individual entries #} +{% for message, issue_reference in sections[section_name][type_]|dictsort(by='value') %} -{% else %} -{% endif %} +- {{ message }} + {%- if type_ not in ["vendor", "process"] %} ({{ issue_reference|sort|join(', ') }}){% endif %} {% endfor %} -{% else %} +{% else %} +{# We only have entries where the type has ``showcontent = true``. #} No significant changes. - -{% endif %} -{% endfor %} +{% endfor -%} +{% endfor -%} +{% endif -%} diff --git a/tox.ini b/tox.ini index 23738ad1ae5..9063c3ac340 100644 --- a/tox.ini +++ b/tox.ini @@ -70,10 +70,7 @@ basepython = python3 skip_install = True commands_pre = deps = - vendoring~=1.0.0 - # Required, otherwise we interpret --no-binary :all: as - # "do not build wheels", which fails for PEP 517 requirements - pip>=19.3.1 + vendoring~=1.2.0 whitelist_externals = git commands = # Check that the vendoring is up-to-date