Repository: pypa/pip

Issue:
"conflict with the backend dependencies" error running pip install in editable mode with pyproject.toml / PEP-517 enabled project
**Environment**

* pip version: 19.0.2
* Python version: 2.7.15
* OS: travis xenial

**Description**

Around a day ago, my cron-triggered builds on Travis for one projects started failing with `Some build dependencies for file:///home/travis/build/cjw296/carly conflict   
with the backend dependencies: attrs==19.1.0 is incompatible with attrs<19.0  
,>=18.2.` when being installed by poetry. Looks like under the hood, poetry is calling `['/home/travis/virtualenv/python2.7.15/bin/pip', 'install', '-e', '/        
home/travis/build/cjw296/carly', '--no-deps']` which would suggest that dependencies should be ignored.

The full build log for this first failure is here: https://travis-ci.org/cjw296/carly/jobs/501041034

There is a poetry issue open for this here: https://github.com/sdispater/poetry/issues/826

It's unclear whether this is a poetry bug or a pip bug, but it appears that either downgrading to pip 18.x or removing the `build-system` section from `pyproject.toml` stop this happening.

What's super confusing is that my particular builds are run nightly and they only started failing on 3rd March, even though neither pip nor poetry had any releases around that date, and I haven't made any code changes to carly since 20th Feb.

**Expected behavior**

This build log shows expected behaviour:
https://travis-ci.org/cjw296/carly/jobs/500733549

**How to Reproduce**
<!-- Describe the steps to reproduce this bug. -->

These are the steps:
https://github.com/cjw296/carly/blob/6ec7eedea9c8405ea6a61f733cb7ee9dfaa9b801/.travis.yml#L21-L23

These should be run against https://github.com/cjw296/carly/tree/6ec7eedea9c8405ea6a61f733cb7ee9dfaa9b801

**Output**

```
$ curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
Retrieving Poetry metadata
# Welcome to Poetry!
This will download and install the latest version of Poetry,
a dependency and package manager for Python.
It will add the `poetry` command to Poetry's bin directory, located at:
$HOME/.poetry/bin
This path will then be added to your `PATH` environment variable by
modifying the profile files located at:
$HOME/.profile
$HOME/.bash_profile
You can uninstall at any time with `poetry self:uninstall`,
or by executing this script with the --uninstall option,
and these changes will be reverted.
Installing version: 0.12.11
  - Downloading poetry-0.12.11-linux.tar.gz (8.00MB)
Poetry (0.12.11) is installed now. Great!
To get started you need Poetry's bin directory ($HOME/.poetry/bin) in your `PATH`
environment variable. Next time you log in this will be done
automatically.
To configure your current shell run `source $HOME/.poetry/env`
install.2
0.00s$ source $HOME/.poetry/env
23.50s$ poetry install
Updating dependencies
Resolving dependencies... (5.7s)
Package operations: 12 installs, 1 update, 0 removals
Writing lock file
  - Installing idna (2.8)
  - Updating six (1.11.0 -> 1.12.0)
  - Installing automat (0.7.0)
  - Installing constantly (15.1.0)
  - Installing hyperlink (18.0.0)
  - Installing incremental (17.5.0)
  - Installing pyhamcrest (1.9.0)
  - Installing txaio (18.8.1)
  - Installing zope.interface (4.6.0)
  - Installing autobahn (18.12.1)
  - Installing coverage (4.4.2)
  - Installing testfixtures (6.6.0)
  - Installing twisted (18.9.0)
  - Installing carly (0.9.1)
                                                                                
[EnvCommandError]                                                
Command ['/home/travis/virtualenv/python2.7.15/bin/pip', 'install', '-e', '/        
home/travis/build/cjw296/carly', '--no-deps'] errored with the following out  
put:                                                                          
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020.  
 Please upgrade your Python as Python 2.7 won't be maintained after that dat  
e. A future version of pip will drop support for Python 2.7.                  
Obtaining file:///home/travis/build/cjw296/carly                              
  Installing build dependencies: started                                      
  Installing build dependencies: finished with status 'done'                  
  Getting requirements to build wheel: started                                
  Getting requirements to build wheel: finished with status 'done'            
Some build dependencies for file:///home/travis/build/cjw296/carly conflict   
with the backend dependencies: attrs==19.1.0 is incompatible with attrs<19.0  
,>=18.2.                                                                      
                                                                                
install [--no-dev] [--dry-run] [-E|--extras EXTRAS] [--develop DEVELOP]
The command "poetry install" failed and exited with 1 during .
Your build has been stopped.
```



Discussion:
Note that it looks like attrs just released 19.1, which could explain why it just started happening: 
https://github.com/python-attrs/attrs/releases . Can you simplify to a reproducer that doesn’t involve poetry?
Does poetry auto-generate a setup.py? Because the logs show pip being used to install carly in editable mode, but pip doesn't support editable mode without a setup.py: https://github.com/pypa/pip/pull/6206 , and carly doesn't seem like it has a setup.py.
Good spot on the attrs release. What's interesting here is that while carly specifies "attrs 18.x", poetry [doesn't depend on attrs at all](https://github.com/sdispater/poetry/blob/0.12.11/pyproject.toml#L24-L49). (although, of course, one of its dependencies might?)

Answers in order:

- Reproducer without poetry: I suspect poetry is intrinsic in this, so no ;-) pip 19 introduced support for `build-system` in a pyproject.toml, if I remember rightly, and carly [that section](https://github.com/cjw296/carly/blob/8188db8d902fceda01cfa50356a141f7e86099e1/pyproject.toml#L28-L30) pointing at poetry.

- Does poetry auto-generate a setup.py? Not in the checkout, as far I'm aware. There's mention of it in the [changelog](https://github.com/sdispater/poetry/blob/master/CHANGELOG.md#0120---2018-10-17), but I'd been under the impression it was just in the sdist.
If you look at poetry's source code, here is where it auto-generates a setup.py prior to calling pip install in editable mode: https://github.com/sdispater/poetry/blob/0222bf6bcbe0a55bfe2c5d593beb667a47b8a752/poetry/console/commands/install.py#L85-L89

```python
if has_setup:
    self.line("<warning>A setup.py file already exists. Using it.</warning>")
else:
    with setup.open("w", encoding="utf-8") as f:
        f.write(decode(builder.build_setup()))

try:
    self.env.run("pip", "install", "-e", str(setup.parent), "--no-deps")
```

Some pdb'ing later, yes, it looks like poetry does generate a setup.py (and then delete it):
```
(Pdb) print e
Command '['/Users/chris/Library/Caches/pypoetry/virtualenvs/carly-py2.7/bin/pip', 'install', '-e', '/Users/chris/vcs/git/carly', '--no-deps']' returned non-zero exit status 1

$ cat /Users/chris/vcs/git/carly/setup.py 
# -*- coding: utf-8 -*-
from distutils.core import setup

packages = \
['carly']

package_data = \
{'': ['*']}

install_requires = \
['Twisted>=18.9,<19.0', 'attrs>=18.2,<19.0']

setup_kwargs = {
    'name': 'carly',
    'version': '0.9.1',
    'description': 'A tool for putting messages into and collecting responses from Twisted servers using real networking',
    'long_description': None,
    'author': 'Chris Withers',
    'author_email': 'chris@withers.org',
    'url': None,
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
}


setup(**setup_kwargs)
(carly-py2.7)(master) Mac Pro:carly chris:
```
@cjerdonek - hah! crossing the streams... okay, so what is requiring attrs >=19?
I don't know.. I would suggest trying to reproduce the issue (if there is one) independent of poetry and Travis CI, as there are currently a lot of confounding factors. Hopefully this provides more of a start. You might also want to try examining what's installed in the environment at the time poetry invokes `pip install`.
Right, here's the answer courtesy of @wimglenn's excellent [johnnydep](https://github.com/wimglenn/johnnydep) for poetry: 
```
name                                         summary
-------------------------------------------  -------------------------------------------------------------------------
poetry                                       Python dependency management and packaging made easy.
├── cachecontrol[filecache]<0.13.0,>=0.12.4  httplib2 caching for requests
│   ├── lockfile>=0.9                        Platform-independent file locking module
│   ├── msgpack                              MessagePack (de)serializer.
│   └── requests                             Python HTTP for Humans.
│       ├── certifi>=2017.4.17               Python package for providing Mozilla's CA Bundle.
│       ├── chardet<3.1.0,>=3.0.2            Universal encoding detector for Python 2 and 3
│       ├── idna<2.9,>=2.5                   Internationalized Domain Names in Applications (IDNA)
│       └── urllib3<1.25,>=1.21.1            HTTP library with thread-safe connection pooling, file post, and more.
├── cachy<0.3,>=0.2                          Cachy provides a simple yet effective caching library.
├── cleo<0.7.0,>=0.6.7                       Cleo allows you to create beautiful and testable command-line interfaces.
│   ├── pastel<0.2.0,>=0.1.0                 Bring colors to your terminal.
│   └── pylev<2.0,>=1.3                      A pure Python Levenshtein implementation that's not freaking GPL'd.
├── html5lib<2.0,>=1.0                       HTML parser based on the WHATWG HTML specification
│   ├── six>=1.9                             Python 2 and 3 compatibility utilities
│   └── webencodings                         Character encoding aliases for legacy web content
├── jsonschema<4.0,>=3.0a3                   An implementation of JSON Schema validation for Python
│   ├── attrs>=17.4.0                        Classes Without Boilerplate
│   ├── pyrsistent>=0.14.0                   Persistent/Functional/Immutable data structures
│   │   └── six                              Python 2 and 3 compatibility utilities
│   ├── setuptools                           Easily download, build, install, upgrade, and uninstall Python packages
│   └── six>=1.11.0                          Python 2 and 3 compatibility utilities
├── pkginfo<2.0,>=1.4                        Query metadatdata from sdists / bdists / installed packages.
├── pyparsing<3.0,>=2.2                      Python parsing module
├── pyrsistent<0.15.0,>=0.14.2               Persistent/Functional/Immutable data structures
│   └── six                                  Python 2 and 3 compatibility utilities
├── requests-toolbelt<0.9.0,>=0.8.0          A utility belt for advanced users of python-requests
│   └── requests<3.0.0,>=2.0.1               Python HTTP for Humans.
│       ├── certifi>=2017.4.17               Python package for providing Mozilla's CA Bundle.
│       ├── chardet<3.1.0,>=3.0.2            Universal encoding detector for Python 2 and 3
│       ├── idna<2.9,>=2.5                   Internationalized Domain Names in Applications (IDNA)
│       └── urllib3<1.25,>=1.21.1            HTTP library with thread-safe connection pooling, file post, and more.
├── requests<3.0,>=2.18                      Python HTTP for Humans.
│   ├── certifi>=2017.4.17                   Python package for providing Mozilla's CA Bundle.
│   ├── chardet<3.1.0,>=3.0.2                Universal encoding detector for Python 2 and 3
│   ├── idna<2.9,>=2.5                       Internationalized Domain Names in Applications (IDNA)
│   └── urllib3<1.25,>=1.21.1                HTTP library with thread-safe connection pooling, file post, and more.
├── shellingham<2.0,>=1.1                    Tool to Detect Surrounding Shell
└── tomlkit<0.6.0,>=0.5.1                    Style preserving TOML library
```

So, jsonschema depends on attrs >17.4, but it looks like the time `pip install -e` happens happens, pip has already picked the latest attrs 19.x.

Here's a hypothesis: pip is picking the build system out of pyproject.toml during the `pip install -e `, installing poetry in the build env, but then getting sad when carly requests attrs 18.x, even though that's an install-time dependency.

To your comments about "confounding factors", I'm afraid I'm not sure I agree. Travis is great as we know it's using a sterile, up-to-date and reproducible environment (Circle CI does the same). The fact that poetry is involved is somewhat the crux of the matter here...
Pinging @sdispater in case he has inputs here.
My point was just that if an issue can't be reproduced independent of Travis and poetry, then it's not clear the issue lies with pip. If it's an issue with poetry, then it should be resolved on poetry's tracker rather than here..
> pip is picking the build system out of pyproject.toml during the pip install -e 

I don't believe pip looks at pyproject.toml when run in editable mode (that's what I was saying above).
Here's a minimal reproducer: https://github.com/cjw296/pip6314

It looks like pip is confusing install-time requirements of carly with build-time requirements.
I suspect this confusion comes from both a setup.py and a pyproject.toml being present.

It's unclear why, but the `[tool.poetry.dependencies]` section in pyproject.toml is required, otherwise you get a different error, from inside poetry.

I'm speculating, but this feels like a combination of poetry being unusual in generating a dynamic setup.py, and potentially bug(s) in both pip's PEP-517 implementation and poetry's (ab)use of pip.
Thanks for the simple reproducer!

Some preliminary thoughts are below. Note that this is just me thinking out loud and shouldn't be interpreted as anything ironclad.

Without having investigated fully, my initial instinct is that maybe pip should be erroring out if it's in the position of being instructed to use PEP 517 when `-e` is passed. One reason is that I don't think pip supports this yet, and if it seems to, I don't think it was intentional and its behavior thought through.

Another reason is that PEP 517 doesn't support editable installs yet ([link](https://www.python.org/dev/peps/pep-0517/#get-requires-for-build-sdist)):

> This [PEP 517] originally specified another hook, install_editable, to do an editable install (as with pip install -e). It was removed due to the complexity of the topic, but may be specified in a later PEP.

In addition, an advantage to erroring out now is that it preserves `pyproject.toml` with the `build-backend` key as being the "switch" to turning on PEP 517. Otherwise, if pip "supported" it now and PEP 517 adds support for editable installs later, existing applications would break if pip tried to implement the new behavior (unless PEP 517's description happened to follow pip's implementation exactly).

In terms of what's happening now, to avoid this issue it seems like it would address the issue and could only help if poetry either--

1. passes `--no-use-pep517` when it invokes `pip install -e`, or

2. ensures that `pyproject.toml` doesn't trigger PEP 517 behavior, either by removing the `pyproject.toml` when it auto-generates a `setup.py`, or replaces the `pyproject.toml` with one without the `build-backend` key (and updating the auto-generated `setup.py` accordingly).

1. Sadly not:

```
$ pip install --no-use-pep517  -e .
Obtaining file:///Users/chris/vcs/git/pip6314
Disabling PEP 517 processing is invalid: project specifies a build backend of poetry.masonry.api in pyproject.toml
```

2. I think you're right here, I suspect poetry should move `pyproject.toml` out of the way.

While it's probably "correct" that pip should error out when it finds both PEP 517 when doing a -e, in this case it would turn a bug that can be worked around into a total show stopper :-/
If `--no-use-pep517` could really mean that and totally ignore pyproject.toml, that feels right, and would make the change required to poetry less hacky than moving pyproject.toml out of the way...
Hmm, I'm a little surprised `--no-use-pep517` didn't work. It looks like it was coded that way deliberately: https://github.com/pypa/pip/blob/4589ed464edb37b199724db0637adf0c682fdee6/src/pip/_internal/pyproject.py#L84-L93

Maybe that logic can still be adjusted (e.g. at least in the case of editable mode which isn't currently supported by PEP 517 anyways).

Regarding poetry moving `pyproject.toml` out of the way, it doesn't seem so hacky to me given that poetry is already manually changing things by generating a `setup.py` and running `pip install -e`. Given that this is gearing up for a `setup.py`-based build, it seems natural that poetry would also have to remove the thing that would cause a PEP 517-based build rather than a `setup.py`-based build.

If PEP 517 is being used, `-e` should be rejected - as @cjerdonek said, PEP 517 doesn't support editable installs.

> Hmm, I'm a little surprised --no-use-pep517 didn't work. It looks like it was coded that way deliberately

That is correct. If you try to insist on not using PEP 517 in a project that has a `pyproject.toml` specifying a build backend, then you will change the behaviour, as a non-PEP 517 build will use setuptools rather than the specified backend (yes, it's possible that `pyproject.toml` says to use setuptools as a backend, but checking for that isn't easy, and the situation is rare enough that I don't think it's warranted). That sort of change in behaviour is **not** what `--no-use-pep517` was intended for, so we reject it to make sure people don't mistakenly break their build.

I'd take a hardline view here. Given that PEP 517 doesn't support editable installs, projects shouldn't be using PEP 517 if they want to support them. If this is a problem, then the solution should be for someone to do the work of defining a PEP 517 compatible standard for editable installs. Then pip (and any other frontends that are developed) can support it cleanly for *any* backend that provides editable install support.

> Regarding poetry moving pyproject.toml out of the way, it doesn't seem so hacky to me given that poetry is already manually changing things by generating a setup.py and running pip install -e. Given that this is gearing up for a setup.py-based build, it seems natural that poetry would also have to remove the thing that would cause a PEP 517-based build rather than a setup.py-based build.

+1 on this. I think that the standards should support poetry, and I'd be more than happy if the poetry project engaged with the standards process to make sure that happens. But as things stand right now, poetry seems to be doing its own thing and so adding some additional customisation to cover this situation seems entirely reasonable to me.

Okay, thanks a lot for weighing in here, @pfmoore. Then the way forward on this issue in the short-term (separate from the longer term effort to expand PEP 517 to cover editable installs) is a PR for pip to "fail fast" with a clear message if someone is trying to install in editable mode a project that requires PEP 517 to be used.
Yes, that sounds reasonable.
Hmm, while I don't disagree on the reasoning, that "bugfix" will kill poetry or force poetry users to say on pip 18, at a time when @sdispater doesn't appear to be available to work on poetry.

@pfmoore - as far as "poetry seems to be doing its own thing" goes, I think you pointed out why that is: there's no standard for editable installs, so I feel your wording is a little harsh. I wonder how flit does editable installs? @takluyver? (or does flit only deal with building wheels, rather than managing a development environment?)
Sorry if I sounded harsh, maybe I wasn't clear. All I meant was that as poetry is already modifying files here, moving `pyproject.toml` out of the way seems like a reasonable and fairly simple fix. (Basically I was just agreeing with @cjerdonek)

Flit doesn't support editable installs. That's precisely the reason editable installs were dropped from PEP 517 - there's no clear view on how *any* backend other than setuptools can handle them, so short of blessing setuptools' behaviour (and risking getting into a situation where no other backend can offer editable support, because the spec is too tided to details of how setuptools does things) it was better to defer support. Well, that plus the fact that everyone was so burned out getting as far as we did, that no-one wanted to open up yet another can of worms at that point.
> Hmm, while I don't disagree on the reasoning, that "bugfix" will kill poetry or force poetry users to say on pip 18, at a time when @sdispater doesn't appear to be available to work on poetry.

Remember @cjw296 that there's a fairly targeted (perhaps even simple and straightforward) change that poetry can make to resolve things, as we discussed above. So I wouldn't say it would "kill" poetry.
> Flit doesn't support editable installs.

Flit does, but not through the PEP 517 APIs, so you can't use pip to get an editable install of a Flit-packaged project. You can run `flit install --symlink`, or someone added `--pth-file` more recently for some platform which doesn't have good symlink support. :wink: 

My rationale is partly that the point of an editable install is to test a package you are actively working on, so it makes sense to set it up through a package authoring tool (flit) rather than a package consumption tool (pip). Plus, as you say, we were all exhausted by what we did try to standardise.
Thanks for the clarification. Your rationale makes sense, although there's a strong push from certain quarters for *everything* to be accessible via pip (or more precisely "the build frontend"). So I suspect the issue of having editable install support as an extension to PEP 517 could re-emerge at some point.
Yup, I'm not against trying to specify some hook for editable installs. But I'm happy enough to let it rest for now.

Resolving patch:
diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py
--- a/src/pip/_internal/pyproject.py
+++ b/src/pip/_internal/pyproject.py
@@ -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):
@@ -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
@@ -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:
@@ -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,
@@ -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,
+    )
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -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)


Test patch:
diff --git a/tests/unit/test_pep517.py b/tests/unit/test_pep517.py
--- a/tests/unit/test_pep517.py
+++ b/tests/unit/test_pep517.py
@@ -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),