Merge pull request #6449 from cjerdonek/restore-pyproject-toml-19.0.3

Restore pyproject.toml handling to its version 19.0.3 state
This commit is contained in:
Chris Jerdonek 2019-04-29 13:33:33 -07:00 committed by GitHub
commit f8732ac06d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 459 deletions

View File

@ -1,7 +1,2 @@
Allow ``--no-use-pep517`` to be used as a work-around when installing a
project in editable mode, even when `PEP 517
<https://www.python.org/dev/peps/pep-0517/>`_ mandates
``pyproject.toml``-style processing (i.e. when the project has a
``pyproject.toml`` file as well as a ``"build-backend"`` key for the
``[build-system]`` table). Since this option conflicts with the PEP 517 spec,
this mode of operation is officially unsupported.
Restore ``pyproject.toml`` handling to how it was with pip 19.0.3 to prevent
the need to add ``--no-use-pep517`` when installing in editable mode.

View File

@ -136,11 +136,7 @@ class IsSDist(DistAbstraction):
# 2. Set up the build environment
self.req.load_pyproject_toml()
should_isolate = (
(self.req.use_pep517 or self.req.pyproject_requires) and
build_isolation
)
should_isolate = self.req.use_pep517 and build_isolation
if should_isolate:
# Isolate in a BuildEnvironment and install the build-time
@ -167,12 +163,10 @@ class IsSDist(DistAbstraction):
" and ".join(map(repr, sorted(missing)))
)
if self.req.use_pep517:
# If we're using PEP 517, then install any extra build
# dependencies that the backend requested. This must be
# done in a second pass, as the pyproject.toml dependencies
# must be installed before we can call the backend.
self.install_backend_dependencies(finder=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.
self.install_backend_dependencies(finder=finder)
self.req.prepare_metadata()
self.req.assert_source_matches_version()

View File

@ -1,7 +1,6 @@
from __future__ import absolute_import
import io
import logging
import os
import sys
@ -11,12 +10,7 @@ from pip._internal.exceptions import InstallationError
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Any, Dict, List, Optional, Tuple
Pep517Data = Tuple[str, List[str]]
logger = logging.getLogger(__name__)
from typing import Any, Tuple, Optional, List
def _is_list_of_str(obj):
@ -38,42 +32,100 @@ def make_pyproject_path(setup_py_dir):
return path
def read_pyproject_toml(path):
# type: (str) -> Optional[Dict[str, str]]
def load_pyproject_toml(
use_pep517, # type: Optional[bool]
pyproject_toml, # type: str
setup_py, # type: str
req_name # type: str
):
# type: (...) -> Optional[Tuple[List[str], str, List[str]]]
"""Load the pyproject.toml file.
Parameters:
use_pep517 - Has the user requested PEP 517 processing? None
means the user hasn't explicitly specified.
pyproject_toml - Location of the project's pyproject.toml file
setup_py - Location of the project's setup.py file
req_name - The name of the requirement we're processing (for
error reporting)
Returns:
None if we should use the legacy code path, otherwise a tuple
(
requirements from pyproject.toml,
name of PEP 517 backend,
requirements we should check are installed after setting
up the build environment
)
"""
Read a project's pyproject.toml file.
has_pyproject = os.path.isfile(pyproject_toml)
has_setup = os.path.isfile(setup_py)
:param path: The path to the pyproject.toml file.
if has_pyproject:
with io.open(pyproject_toml, encoding="utf-8") as f:
pp_toml = pytoml.load(f)
build_system = pp_toml.get("build-system")
else:
build_system = None
:return: The "build_system" value specified in the project's
pyproject.toml file.
"""
with io.open(path, encoding="utf-8") as f:
pp_toml = pytoml.load(f)
build_system = pp_toml.get("build-system")
# The following cases must use PEP 517
# We check for use_pep517 being non-None and falsey because that means
# the user explicitly requested --no-use-pep517. The value 0 as
# opposed to False can occur when the value is provided via an
# environment variable or config file option (due to the quirk of
# strtobool() returning an integer in pip's configuration code).
if has_pyproject and not has_setup:
if use_pep517 is not None and not use_pep517:
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project does not have a setup.py"
)
use_pep517 = True
elif build_system and "build-backend" in build_system:
if use_pep517 is not None and not use_pep517:
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project specifies a build backend of {} "
"in pyproject.toml".format(
build_system["build-backend"]
)
)
use_pep517 = True
return build_system
# If we haven't worked out whether to use PEP 517 yet,
# and the user hasn't explicitly stated a preference,
# we do so if the project has a pyproject.toml file.
elif use_pep517 is None:
use_pep517 = has_pyproject
# At this point, we know whether we're going to use PEP 517.
assert use_pep517 is not None
def make_editable_error(req_name, reason):
"""
:param req_name: the name of the requirement.
:param reason: the reason the requirement is being processed as
pyproject.toml-style.
"""
message = (
'Error installing {!r}: editable mode is not supported for '
'pyproject.toml-style projects. This project is being processed '
'as pyproject.toml-style because {}. '
'See PEP 517 for the relevant specification.'
).format(req_name, reason)
return InstallationError(message)
def get_build_system_requires(build_system, req_name):
if build_system is None:
# If we're using the legacy code path, there is nothing further
# for us to do here.
if not use_pep517:
return None
if build_system is None:
# Either the user has a pyproject.toml with no build-system
# section, or the user has no pyproject.toml, but has opted in
# explicitly via --use-pep517.
# In the absence of any explicit backend specification, we
# assume the setuptools backend that most closely emulates the
# traditional direct setup.py execution, and require wheel and
# a version of setuptools that supports that backend.
build_system = {
"requires": ["setuptools>=40.8.0", "wheel"],
"build-backend": "setuptools.build_meta:__legacy__",
}
# If we're using PEP 517, we have build system information (either
# from pyproject.toml, or defaulted by the code above).
# Note that at this point, we do not know if the user has actually
# specified a backend, though.
assert build_system is not None
# Ensure that the build-system section in pyproject.toml conforms
# to PEP 518.
error_template = (
@ -98,150 +150,6 @@ def get_build_system_requires(build_system, req_name):
reason="'build-system.requires' is not a list of strings.",
))
return requires
def resolve_pyproject_toml(
build_system, # type: Optional[Dict[str, Any]]
has_pyproject, # type: bool
has_setup, # type: bool
use_pep517, # type: Optional[bool]
editable, # type: bool
req_name, # type: str
):
# type: (...) -> Tuple[Optional[List[str]], Optional[Pep517Data]]
"""
Return how a pyproject.toml file's contents should be interpreted.
:param build_system: the "build_system" value specified in a project's
pyproject.toml file, or None if the project either doesn't have the
file or does but the file doesn't have a [build-system] table.
:param has_pyproject: whether the project has a pyproject.toml file.
:param has_setup: whether the project has a setup.py file.
:param use_pep517: whether the user requested PEP 517 processing. None
means the user didn't explicitly specify.
:param editable: whether editable mode was requested for the requirement.
:param req_name: the name of the requirement we're processing (for
error reporting).
:return: a tuple (requires, pep517_data), where `requires` is the list
of build requirements from pyproject.toml (or else None). The value
`pep517_data` is None if `use_pep517` is False. Otherwise, it is the
tuple (backend, check), where `backend` is the name of the PEP 517
backend and `check` is the list of requirements we should check are
installed after setting up the build environment.
"""
# The following cases must use PEP 517
# We check for use_pep517 being non-None and falsey because that means
# the user explicitly requested --no-use-pep517. The value 0 as
# opposed to False can occur when the value is provided via an
# environment variable or config file option (due to the quirk of
# strtobool() returning an integer in pip's configuration code).
if editable and use_pep517:
raise make_editable_error(
req_name, 'PEP 517 processing was explicitly requested'
)
elif has_pyproject and not has_setup:
if use_pep517 is not None and not use_pep517:
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project does not have a setup.py"
)
if editable:
raise make_editable_error(
req_name, 'it has a pyproject.toml file and no setup.py'
)
use_pep517 = True
elif build_system and "build-backend" in build_system:
if editable:
if use_pep517 is None:
message = (
'Error installing {!r}: editable mode is not supported '
'for pyproject.toml-style projects. '
'This project is pyproject.toml-style because it has a '
'pyproject.toml file and a "build-backend" key for the '
'[build-system] table, but editable mode is undefined '
'for pyproject.toml-style projects. '
'Since the project has a setup.py, you may pass '
'--no-use-pep517 to opt out of pyproject.toml-style '
'processing. However, this is an unsupported combination. '
'See PEP 517 for details on pyproject.toml-style projects.'
).format(req_name)
raise InstallationError(message)
# The case of `editable and use_pep517` being true was already
# handled above.
assert not use_pep517
message = (
'Installing {!r} in editable mode, which is not supported '
'for pyproject.toml-style projects: '
'this project is pyproject.toml-style because it has a '
'pyproject.toml file and a "build-backend" key for the '
'[build-system] table, but editable mode is undefined '
'for pyproject.toml-style projects. '
'See PEP 517 for details on pyproject.toml-style projects.'
).format(req_name)
logger.warning(message)
elif use_pep517 is not None and not use_pep517:
raise InstallationError(
"Disabling PEP 517 processing is invalid: "
"project specifies a build backend of {} "
"in pyproject.toml".format(
build_system["build-backend"]
)
)
else:
use_pep517 = True
# If we haven't worked out whether to use PEP 517 yet, and the user
# hasn't explicitly stated a preference, we do so if the project has
# a pyproject.toml file (provided editable mode wasn't requested).
elif use_pep517 is None:
if has_pyproject and editable:
message = (
'Error installing {!r}: editable mode is not supported for '
'pyproject.toml-style projects. pip is processing this '
'project as pyproject.toml-style because it has a '
'pyproject.toml file. Since the project has a setup.py and '
'the pyproject.toml has no "build-backend" key for the '
'[build-system] table, you may pass --no-use-pep517 to opt '
'out of pyproject.toml-style processing. '
'See PEP 517 for details on pyproject.toml-style projects.'
).format(req_name)
raise InstallationError(message)
use_pep517 = has_pyproject
# At this point, we know whether we're going to use PEP 517.
assert use_pep517 is not None
requires = get_build_system_requires(build_system, req_name=req_name)
# If we're using the legacy code path, there is nothing further
# for us to do here.
if not use_pep517:
return (requires, None)
if build_system is None:
# Either the user has a pyproject.toml with no build-system
# section, or the user has no pyproject.toml, but has opted in
# explicitly via --use-pep517.
# In the absence of any explicit backend specification, we
# assume the setuptools backend that most closely emulates the
# traditional direct setup.py execution, and require wheel and
# a version of setuptools that supports that backend.
requires = ["setuptools>=40.8.0", "wheel"]
build_system = {
"build-backend": "setuptools.build_meta:__legacy__",
}
# If we're using PEP 517, we have build system information (either
# from pyproject.toml, or defaulted by the code above).
# Note that at this point, we do not know if the user has actually
# specified a backend, though.
assert build_system is not None
backend = build_system.get("build-backend")
check = [] # type: List[str]
if backend is None:
@ -260,50 +168,4 @@ def resolve_pyproject_toml(
backend = "setuptools.build_meta:__legacy__"
check = ["setuptools>=40.8.0", "wheel"]
return (requires, (backend, check))
def load_pyproject_toml(
use_pep517, # type: Optional[bool]
editable, # type: bool
pyproject_toml, # type: str
setup_py, # type: str
req_name # type: str
):
# type: (...) -> Tuple[Optional[List[str]], Optional[Pep517Data]]
"""Load the pyproject.toml file.
Parameters:
use_pep517 - Has the user requested PEP 517 processing? None
means the user hasn't explicitly specified.
editable - Whether editable mode was requested for the requirement.
pyproject_toml - Location of the project's pyproject.toml file
setup_py - Location of the project's setup.py file
req_name - The name of the requirement we're processing (for
error reporting)
Returns: (requires, pep_517_data)
requires: requirements from pyproject.toml (can be None).
pep_517_data: None if we should use the legacy code path, otherwise:
(
name of PEP 517 backend,
requirements we should check are installed after setting up
the build environment
)
"""
has_pyproject = os.path.isfile(pyproject_toml)
has_setup = os.path.isfile(setup_py)
if has_pyproject:
build_system = read_pyproject_toml(pyproject_toml)
else:
build_system = None
return resolve_pyproject_toml(
build_system=build_system,
has_pyproject=has_pyproject,
has_setup=has_setup,
use_pep517=use_pep517,
editable=editable,
req_name=req_name,
)
return (requires, backend, check)

View File

@ -485,22 +485,20 @@ class InstallRequirement(object):
use_pep517 attribute can be used to determine whether we should
follow the PEP 517 or legacy (setup.py) code path.
"""
requires, pep517_data = load_pyproject_toml(
pep517_data = load_pyproject_toml(
self.use_pep517,
self.editable,
self.pyproject_toml,
self.setup_py,
str(self)
)
use_pep517 = bool(pep517_data)
self.use_pep517 = use_pep517
self.pyproject_requires = requires
if use_pep517:
backend, check = pep517_data
if pep517_data is None:
self.use_pep517 = False
else:
self.use_pep517 = True
requires, backend, check = pep517_data
self.requirements_to_check = check
self.pyproject_requires = requires
self.pep517_backend = Pep517HookCaller(self.setup_py_dir, backend)
# Use a custom function to call subprocesses

View File

@ -302,12 +302,8 @@ def test_constraints_local_editable_install_pep518(script, data):
to_install = data.src.join("pep518-3.0")
script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
# --no-use-pep517 has to be passed since a pyproject.toml file is
# present but PEP 517 doesn't support editable mode.
script.pip(
'install', '--no-use-pep517', '--no-index', '-f', data.find_links,
'-e', to_install,
)
'install', '--no-index', '-f', data.find_links, '-e', to_install)
def test_constraints_local_install_causes_error(script, data):

View File

@ -1,210 +1,9 @@
from contextlib import contextmanager
import pytest
from pip._internal.exceptions import InstallationError
from pip._internal.pyproject import resolve_pyproject_toml
from pip._internal.req import InstallRequirement
@contextmanager
def assert_error_startswith(exc_type, expected_start, expected_substr):
with pytest.raises(InstallationError) as excinfo:
yield
err_args = excinfo.value.args
assert len(err_args) == 1
msg = err_args[0]
assert msg.startswith(expected_start), 'full message: {}'.format(msg)
assert expected_substr in msg, 'full message: {}'.format(msg)
def test_resolve_pyproject_toml__pep_517_optional():
"""
Test resolve_pyproject_toml() when has_pyproject=True but the source
tree isn't pyproject.toml-style per PEP 517.
"""
actual = resolve_pyproject_toml(
build_system=None,
has_pyproject=True,
has_setup=True,
use_pep517=None,
editable=False,
req_name='my-package',
)
expected = (
['setuptools>=40.8.0', 'wheel'],
('setuptools.build_meta:__legacy__', []),
)
assert actual == expected
def test_resolve_pyproject_toml__editable_with_build_system_requires():
"""
Test a case of editable=True when build-system.requires are returned.
"""
actual = resolve_pyproject_toml(
build_system={'requires': ['package-a', 'package=b']},
has_pyproject=True,
has_setup=True,
use_pep517=False,
editable=True,
req_name='my-package',
)
expected = (['package-a', 'package=b'], None)
assert actual == expected
def test_resolve_pyproject_toml__editable_without_use_pep517_false():
"""
Test passing editable=True when PEP 517 processing is optional, but
use_pep517=False hasn't been passed.
"""
expected_start = (
"Error installing 'my-package': editable mode is not supported"
)
expected_substr = (
'you may pass --no-use-pep517 to opt out of pyproject.toml-style '
'processing'
)
with assert_error_startswith(
InstallationError, expected_start, expected_substr=expected_substr,
):
resolve_pyproject_toml(
build_system=None,
has_pyproject=True,
has_setup=True,
use_pep517=None,
editable=True,
req_name='my-package',
)
@pytest.mark.parametrize(
'has_pyproject, has_setup, use_pep517, build_system, expected_err', [
# Test pyproject.toml with no setup.py.
(True, False, None, None, 'has a pyproject.toml file and no setup.py'),
# Test explicitly requesting PEP 517 processing.
(True, True, True, None,
'PEP 517 processing was explicitly requested'),
]
)
def test_resolve_pyproject_toml__editable_and_pep_517_required(
has_pyproject, has_setup, use_pep517, build_system, expected_err,
):
"""
Test that passing editable=True raises an error if PEP 517 processing
is required.
"""
expected_start = (
"Error installing 'my-package': editable mode is not supported"
)
with assert_error_startswith(
InstallationError, expected_start, expected_substr=expected_err,
):
resolve_pyproject_toml(
build_system=build_system,
has_pyproject=has_pyproject,
has_setup=has_setup,
use_pep517=use_pep517,
editable=True,
req_name='my-package',
)
def test_resolve_pyproject_toml__editable_build_backend_use_pep517_none():
"""
Test editable=True with "build-backend" and use_pep517=None.
"""
expected_start = (
"Error installing 'my-package': editable mode is not supported "
)
expected_substr = 'you may pass --no-use-pep517 to opt out'
with assert_error_startswith(
InstallationError, expected_start, expected_substr=expected_substr,
):
resolve_pyproject_toml(
build_system={'requires': ['my-package'], 'build-backend': 'foo'},
has_pyproject=True,
has_setup=True,
use_pep517=None,
editable=True,
req_name='my-package',
)
def test_resolve_pyproject_toml__editable_build_backend_use_pep517_false(
caplog
):
"""
Test editable=True with "build-backend" and use_pep517=False.
"""
resolve_pyproject_toml(
build_system={'requires': ['my-package'], 'build-backend': 'foo'},
has_pyproject=True,
has_setup=True,
use_pep517=False,
editable=True,
req_name='my-package',
)
records = caplog.records
assert len(records) == 1
record = records[0]
assert record.levelname == 'WARNING'
expected_start = (
"Installing 'my-package' in editable mode, which is not supported "
)
assert record.message.startswith(expected_start), (
'full message: {}'.format(record.message)
)
@pytest.mark.parametrize(
'has_pyproject, has_setup, use_pep517, editable, build_system, '
'expected_err', [
# Test editable=False with no build-system.requires.
(True, False, None, False, {},
"it has a 'build-system' table but not 'build-system.requires'"),
# Test editable=True with no build-system.requires (we also
# need to pass use_pep517=False).
(True, True, False, True, {},
"it has a 'build-system' table but not 'build-system.requires'"),
# Test editable=False with a bad build-system.requires value.
(True, False, None, False, {'requires': 'foo'},
"'build-system.requires' is not a list of strings"),
# Test editable=True with a bad build-system.requires value (we also
# need to pass use_pep517=False).
(True, True, False, True, {'requires': 'foo'},
"'build-system.requires' is not a list of strings"),
]
)
def test_resolve_pyproject_toml__bad_build_system_section(
has_pyproject, has_setup, use_pep517, editable, build_system,
expected_err,
):
"""
Test a pyproject.toml build-system section that doesn't conform to
PEP 518.
"""
expected_start = (
'my-package has a pyproject.toml file that does not comply with '
'PEP 518:'
)
with assert_error_startswith(
InstallationError, expected_start, expected_substr=expected_err,
):
resolve_pyproject_toml(
build_system=build_system,
has_pyproject=has_pyproject,
has_setup=has_setup,
use_pep517=use_pep517,
editable=editable,
req_name='my-package',
)
@pytest.mark.parametrize(('source', 'expected'), [
("pep517_setup_and_pyproject", True),
("pep517_setup_only", False),