Skip to content

improve handling of PEP 518 build requirements #5286

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

Merged
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
49 changes: 21 additions & 28 deletions docs/reference/pip.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,10 @@ using an incorrect encoding (mojibake).
PEP 518 Support
~~~~~~~~~~~~~~~

Pip supports projects declaring dependencies that are required at install time
using a ``pyproject.toml`` file, in the form described in `PEP518`_. When
building a project, pip will install the required dependencies locally, and
make them available to the build process.

As noted in the PEP, the minimum requirements for pip to be able to build a
project are::

[build-system]
# Minimum requirements for the build system to execute.
requires = ["setuptools", "wheel"]

``setuptools`` and ``wheel`` **must** be included in any ``pyproject.toml``
provided by a project - pip will assume these as a default, but will not add
them to an explicitly supplied list in a project supplied ``pyproject.toml``
file. Once `PEP517`_ support is added, this restriction will be lifted and
alternative build tools will be allowed.
As of 10.0, pip supports projects declaring dependencies that are required at
install time using a ``pyproject.toml`` file, in the form described in
`PEP518`_. When building a project, pip will install the required dependencies
locally, and make them available to the build process.

When making build requirements available, pip does so in an *isolated
environment*. That is, pip does not install those requirements into the user's
Expand All @@ -152,17 +139,23 @@ appropriately.

.. _pep-518-limitations:

The current implementation of `PEP518`_ in pip requires that any dependencies
specified in ``pyproject.toml`` are available as wheels. This is a technical
limitation of the implementation - dependencies only available as source would
require a build step of their own, which would recursively invoke the `PEP518`_
dependency installation process. The potentially unbounded recursion involved
was not considered acceptable, and so installation of build dependencies from
source has been disabled until a safe resolution of this issue has been found.

Further, it also doesn't support the use of environment markers and extras,
only version specifiers are respected. Support for markers and extras will be
added in a future release.
**Limitations**:

* until `PEP517`_ support is added, ``setuptools`` and ``wheel`` **must** be
included in the list of build requirements: pip will assume these as default,
but will not automatically add them to the list of build requirements if
explicitly defined in ``pyproject.toml``.

* the current implementation only support installing build requirements from
wheels: this is a technical limitation of the implementation - source
installs would require a build step of their own, potentially recursively
triggering another `PEP518`_ dependency installation process. The possible
unbounded recursion involved was not considered acceptable, and so
installation of build dependencies from source has been disabled until a safe
resolution of this issue is found.

* ``pip<18.0`` does not support the use of environment markers and extras, only
version specifiers are respected.

.. _PEP517: http://www.python.org/dev/peps/pep-0517/
.. _PEP518: http://www.python.org/dev/peps/pep-0518/
Expand Down
1 change: 1 addition & 0 deletions news/5230.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve handling of PEP 518 build requirements: support environment markers and extras.
1 change: 1 addition & 0 deletions news/5265.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve handling of PEP 518 build requirements: support environment markers and extras.
48 changes: 40 additions & 8 deletions src/pip/_internal/build_env.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
"""Build Environment used for isolation during sdist building
"""

import logging
import os
import sys
from distutils.sysconfig import get_python_lib
from sysconfig import get_paths

from pip._internal.utils.misc import call_subprocess
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.ui import open_spinner


logger = logging.getLogger(__name__)


class BuildEnvironment(object):
"""Creates and manages an isolated environment to install build deps
"""

def __init__(self, no_clean):
def __init__(self):
self._temp_dir = TempDirectory(kind="build-env")
self._no_clean = no_clean
self._temp_dir.create()

@property
def path(self):
return self._temp_dir.path

def __enter__(self):
self._temp_dir.create()

self.save_path = os.environ.get('PATH', None)
self.save_pythonpath = os.environ.get('PYTHONPATH', None)
self.save_nousersite = os.environ.get('PYTHONNOUSERSITE', None)
Expand Down Expand Up @@ -58,9 +63,6 @@ def __enter__(self):
return self.path

def __exit__(self, exc_type, exc_val, exc_tb):
if not self._no_clean:
self._temp_dir.cleanup()

def restore_var(varname, old_value):
if old_value is None:
os.environ.pop(varname, None)
Expand All @@ -74,12 +76,39 @@ def restore_var(varname, old_value):
def cleanup(self):
self._temp_dir.cleanup()

def install_requirements(self, finder, requirements, message):
args = [
sys.executable, '-m', 'pip', 'install', '--ignore-installed',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for adding --ignore-installed here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call is not run with build isolation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha.

Isolating this run would make pip not visible -> python -m pip won't work, and a temporary prefix for the installation of packages makes this non env-breaking.

'--no-user', '--prefix', self.path, '--no-warn-script-location',
'--only-binary', ':all:',
]
if logger.getEffectiveLevel() <= logging.DEBUG:
args.append('-v')
if finder.index_urls:
args.extend(['-i', finder.index_urls[0]])
for extra_index in finder.index_urls[1:]:
args.extend(['--extra-index-url', extra_index])
else:
args.append('--no-index')
for link in finder.find_links:
args.extend(['--find-links', link])
for _, host, _ in finder.secure_origins:
args.extend(['--trusted-host', host])
if finder.allow_all_prereleases:
args.append('--pre')
if finder.process_dependency_links:
args.append('--process-dependency-links')
args.append('--')
args.extend(requirements)
with open_spinner(message) as spinner:
call_subprocess(args, show_stdout=False, spinner=spinner)


class NoOpBuildEnvironment(BuildEnvironment):
"""A no-op drop-in replacement for BuildEnvironment
"""

def __init__(self, no_clean):
def __init__(self):
pass

def __enter__(self):
Expand All @@ -90,3 +119,6 @@ def __exit__(self, exc_type, exc_val, exc_tb):

def cleanup(self):
pass

def install_requirements(self, finder, requirements, message):
raise NotImplementedError()
42 changes: 6 additions & 36 deletions src/pip/_internal/operations/prepare.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
"""Prepares a distribution for installation
"""

import itertools
import logging
import os
import sys
from copy import copy

from pip._vendor import pkg_resources, requests

from pip._internal.build_env import NoOpBuildEnvironment
from pip._internal.build_env import BuildEnvironment
from pip._internal.compat import expanduser
from pip._internal.download import (
is_dir_url, is_file_url, is_vcs_url, unpack_url, url_to_path,
Expand All @@ -18,14 +15,9 @@
DirectoryUrlHashUnsupported, HashUnpinned, InstallationError,
PreviousBuildDirError, VcsHashUnsupported,
)
from pip._internal.index import FormatControl
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.hashes import MissingHashes
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import (
call_subprocess, display_path, normalize_path,
)
from pip._internal.utils.ui import open_spinner
from pip._internal.utils.misc import display_path, normalize_path
from pip._internal.vcs import vcs

logger = logging.getLogger(__name__)
Expand All @@ -47,26 +39,6 @@ def make_abstract_dist(req):
return IsSDist(req)


def _install_build_reqs(finder, prefix, build_requirements):
# NOTE: What follows is not a very good thing.
# Eventually, this should move into the BuildEnvironment class and
# that should handle all the isolation and sub-process invocation.
finder = copy(finder)
finder.format_control = FormatControl(set(), set([":all:"]))
urls = [
finder.find_requirement(
InstallRequirement.from_line(r), upgrade=False).url
for r in build_requirements
]
args = [
sys.executable, '-m', 'pip', 'install', '--ignore-installed',
'--no-user', '--prefix', prefix,
] + list(urls)

with open_spinner("Installing build dependencies") as spinner:
call_subprocess(args, show_stdout=False, spinner=spinner)


class DistAbstraction(object):
"""Abstracts out the wheel vs non-wheel Resolver.resolve() logic.

Expand Down Expand Up @@ -144,12 +116,10 @@ def format_reqs(rs):
)

if should_isolate:
with self.req.build_env:
pass
_install_build_reqs(finder, self.req.build_env.path,
build_requirements)
else:
self.req.build_env = NoOpBuildEnvironment(no_clean=False)
self.req.build_env = BuildEnvironment()
self.req.build_env.install_requirements(
finder, build_requirements,
"Installing build dependencies")

self.req.run_egg_info()
self.req.assert_source_matches_version()
Expand Down
4 changes: 2 additions & 2 deletions src/pip/_internal/req/req_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from pip._vendor.pkg_resources import RequirementParseError, parse_requirements

from pip._internal import wheel
from pip._internal.build_env import BuildEnvironment
from pip._internal.build_env import NoOpBuildEnvironment
from pip._internal.compat import native_str
from pip._internal.download import (
is_archive_file, is_url, path_to_url, url_to_path,
Expand Down Expand Up @@ -127,7 +127,7 @@ def __init__(self, req, comes_from, source_dir=None, editable=False,
self.is_direct = False

self.isolated = isolated
self.build_env = BuildEnvironment(no_clean=True)
self.build_env = NoOpBuildEnvironment()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea. :)


@classmethod
def from_editable(cls, editable_req, comes_from=None, isolated=False,
Expand Down
1 change: 0 additions & 1 deletion src/pip/_internal/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from pip._vendor.six import StringIO

from pip._internal import pep425tags
from pip._internal.build_env import BuildEnvironment
from pip._internal.download import path_to_url, unpack_url
from pip._internal.exceptions import (
InstallationError, InvalidWheelFilename, UnsupportedWheel,
Expand Down
Binary file modified tests/data/packages/pep518-3.0.tar.gz
Binary file not shown.
Binary file not shown.
Binary file modified tests/data/packages/simplewheel-1.0-py2.py3-none-any.whl
Binary file not shown.
Binary file modified tests/data/packages/simplewheel-2.0-1-py2.py3-none-any.whl
Binary file not shown.
Binary file modified tests/data/packages/simplewheel-2.0-py2.py3-none-any.whl
Binary file not shown.
Binary file not shown.
5 changes: 0 additions & 5 deletions tests/data/src/pep518-3.0/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +0,0 @@
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

6 changes: 3 additions & 3 deletions tests/data/src/pep518-3.0/setup.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env python
from setuptools import find_packages, setup
from setuptools import setup

import simple # ensure dependency is installed
import simplewheel # ensure dependency is installed

setup(name='pep518',
version='3.0',
packages=find_packages()
py_modules=['pep518'],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#dummy
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[build-system]
requires=[
"requires_simple_extra[extra]",
"simplewheel==1.0; python_version < '3'",
"simplewheel==2.0; python_version >= '3'",
"setuptools", "wheel",
]
Empty file.
15 changes: 15 additions & 0 deletions tests/data/src/pep518_with_extra_and_markers-1.0/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env python
import sys

from setuptools import setup

# ensure dependencies are installed
import simple
import simplewheel

assert simplewheel.__version__ == '1.0' if sys.version_info < (3,) else '2.0'

setup(name='pep518_with_extra_and_markers',
version='1.0',
py_modules=['pep518_with_extra_and_markers'],
)
Empty file.
8 changes: 5 additions & 3 deletions tests/data/src/simplewheel-1.0/setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env python
from setuptools import find_packages, setup
from setuptools import setup

import simplewheel

setup(name='simplewheel',
version='1.0',
packages=find_packages()
version=simplewheel.__version__,
packages=['simplewheel'],
)
1 change: 0 additions & 1 deletion tests/data/src/simplewheel-1.0/simple/__init__.py

This file was deleted.

1 change: 1 addition & 0 deletions tests/data/src/simplewheel-1.0/simplewheel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = '1.0'
Empty file.
8 changes: 5 additions & 3 deletions tests/data/src/simplewheel-2.0/setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env python
from setuptools import find_packages, setup
from setuptools import setup

import simplewheel

setup(name='simplewheel',
version='2.0',
packages=find_packages()
version=simplewheel.__version__,
packages=['simplewheel'],
)
1 change: 0 additions & 1 deletion tests/data/src/simplewheel-2.0/simple/__init__.py

This file was deleted.

1 change: 1 addition & 0 deletions tests/data/src/simplewheel-2.0/simplewheel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = '2.0'
Loading