Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show a nice error if editable mode is attempted with a pyproject.toml source tree #6331

Merged
merged 2 commits into from
Mar 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions news/6314.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Error out with an informative message if one tries to install a
``pyproject.toml``-style (PEP 517) source tree using ``--editable`` mode.
144 changes: 112 additions & 32 deletions src/pip/_internal/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pip._internal.utils.typing import MYPY_CHECK_RUNNING

if MYPY_CHECK_RUNNING:
from typing import Any, Tuple, Optional, List
from typing import Any, Dict, List, Optional, Tuple


def _is_list_of_str(obj):
Expand All @@ -32,42 +32,61 @@ def make_pyproject_path(setup_py_dir):
return path


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.
def read_pyproject_toml(path):
# type: (str) -> Optional[Dict[str, str]]
"""
Read a project's 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)
:param path: The path to the pyproject.toml file.

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
)
:return: The "build_system" value specified in the project's
pyproject.toml file.
"""
has_pyproject = os.path.isfile(pyproject_toml)
has_setup = os.path.isfile(setup_py)
with io.open(path, encoding="utf-8") as f:
pp_toml = pytoml.load(f)
build_system = pp_toml.get("build-system")

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 build_system


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 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: (...) -> Optional[Tuple[List[str], str, List[str]]]
"""
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" value.
: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).
"""
# 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
Expand All @@ -80,6 +99,10 @@ def load_pyproject_toml(
"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 use_pep517 is not None and not use_pep517:
Expand All @@ -90,7 +113,18 @@ def load_pyproject_toml(
build_system["build-backend"]
)
)
if editable:
reason = (
'it has a pyproject.toml file with a "build-backend" key '
'in the "build_system" value'
)
raise make_editable_error(req_name, reason)
use_pep517 = True
elif use_pep517:
if editable:
raise make_editable_error(
req_name, 'PEP 517 processing was explicitly requested'
)

# If we haven't worked out whether to use PEP 517 yet,
# and the user hasn't explicitly stated a preference,
Expand Down Expand Up @@ -169,3 +203,49 @@ def load_pyproject_toml(
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: (...) -> 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.
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:
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
)
"""
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,
)
1 change: 1 addition & 0 deletions src/pip/_internal/req/req_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ def load_pyproject_toml(self):
"""
pep517_data = load_pyproject_toml(
self.use_pep517,
self.editable,
self.pyproject_toml,
self.setup_py,
str(self)
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/test_pep517.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,69 @@
import pytest

from pip._internal.exceptions import InstallationError
from pip._internal.pyproject import resolve_pyproject_toml
from pip._internal.req import InstallRequirement


@pytest.mark.parametrize('editable', [False, True])
def test_resolve_pyproject_toml__pep_517_optional(editable):
"""
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=editable,
req_name='my-package',
)
expected = (
['setuptools>=40.8.0', 'wheel'],
'setuptools.build_meta:__legacy__',
[],
)
assert actual == expected


@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 "build-backend" present.
(True, True, None, {'build-backend': 'foo'},
'has a pyproject.toml file with a "build-backend" key'),
# 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.
"""
with pytest.raises(InstallationError) as excinfo:
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',
)
err_args = excinfo.value.args
assert len(err_args) == 1
msg = err_args[0]
assert msg.startswith(
"Error installing 'my-package': editable mode is not supported"
)
assert expected_err in msg, 'full message: {}'.format(msg)


@pytest.mark.parametrize(('source', 'expected'), [
("pep517_setup_and_pyproject", True),
("pep517_setup_only", False),
Expand Down