diff --git a/news/3691.feature b/news/3691.feature
new file mode 100644
index 00000000000..24eebbcc0fa
--- /dev/null
+++ b/news/3691.feature
@@ -0,0 +1,4 @@
+Support for packages specifying build dependencies in pyproject.toml (see `PEP
+518 `__). Packages which specify
+one or more build dependencies this way will be built into wheels in an
+isolated environment with those dependencies installed.
diff --git a/pip/_vendor/pytoml/__init__.py b/pip/_vendor/pytoml/__init__.py
new file mode 100644
index 00000000000..222a1967fde
--- /dev/null
+++ b/pip/_vendor/pytoml/__init__.py
@@ -0,0 +1,3 @@
+from .core import TomlError
+from .parser import load, loads
+from .writer import dump, dumps
diff --git a/pip/_vendor/pytoml/core.py b/pip/_vendor/pytoml/core.py
new file mode 100644
index 00000000000..0fcada48c64
--- /dev/null
+++ b/pip/_vendor/pytoml/core.py
@@ -0,0 +1,13 @@
+class TomlError(RuntimeError):
+ def __init__(self, message, line, col, filename):
+ RuntimeError.__init__(self, message, line, col, filename)
+ self.message = message
+ self.line = line
+ self.col = col
+ self.filename = filename
+
+ def __str__(self):
+ return '{}({}, {}): {}'.format(self.filename, self.line, self.col, self.message)
+
+ def __repr__(self):
+ return 'TomlError({!r}, {!r}, {!r}, {!r})'.format(self.message, self.line, self.col, self.filename)
diff --git a/pip/_vendor/pytoml/parser.py b/pip/_vendor/pytoml/parser.py
new file mode 100644
index 00000000000..8889c244c14
--- /dev/null
+++ b/pip/_vendor/pytoml/parser.py
@@ -0,0 +1,366 @@
+import string, re, sys, datetime
+from .core import TomlError
+
+if sys.version_info[0] == 2:
+ _chr = unichr
+else:
+ _chr = chr
+
+def load(fin, translate=lambda t, x, v: v):
+ return loads(fin.read(), translate=translate, filename=getattr(fin, 'name', repr(fin)))
+
+def loads(s, filename='', translate=lambda t, x, v: v):
+ if isinstance(s, bytes):
+ s = s.decode('utf-8')
+
+ s = s.replace('\r\n', '\n')
+
+ root = {}
+ tables = {}
+ scope = root
+
+ src = _Source(s, filename=filename)
+ ast = _p_toml(src)
+
+ def error(msg):
+ raise TomlError(msg, pos[0], pos[1], filename)
+
+ def process_value(v):
+ kind, text, value, pos = v
+ if kind == 'str' and value.startswith('\n'):
+ value = value[1:]
+ if kind == 'array':
+ if value and any(k != value[0][0] for k, t, v, p in value[1:]):
+ error('array-type-mismatch')
+ value = [process_value(item) for item in value]
+ elif kind == 'table':
+ value = dict([(k, process_value(value[k])) for k in value])
+ return translate(kind, text, value)
+
+ for kind, value, pos in ast:
+ if kind == 'kv':
+ k, v = value
+ if k in scope:
+ error('duplicate_keys. Key "{0}" was used more than once.'.format(k))
+ scope[k] = process_value(v)
+ else:
+ is_table_array = (kind == 'table_array')
+ cur = tables
+ for name in value[:-1]:
+ if isinstance(cur.get(name), list):
+ d, cur = cur[name][-1]
+ else:
+ d, cur = cur.setdefault(name, (None, {}))
+
+ scope = {}
+ name = value[-1]
+ if name not in cur:
+ if is_table_array:
+ cur[name] = [(scope, {})]
+ else:
+ cur[name] = (scope, {})
+ elif isinstance(cur[name], list):
+ if not is_table_array:
+ error('table_type_mismatch')
+ cur[name].append((scope, {}))
+ else:
+ if is_table_array:
+ error('table_type_mismatch')
+ old_scope, next_table = cur[name]
+ if old_scope is not None:
+ error('duplicate_tables')
+ cur[name] = (scope, next_table)
+
+ def merge_tables(scope, tables):
+ if scope is None:
+ scope = {}
+ for k in tables:
+ if k in scope:
+ error('key_table_conflict')
+ v = tables[k]
+ if isinstance(v, list):
+ scope[k] = [merge_tables(sc, tbl) for sc, tbl in v]
+ else:
+ scope[k] = merge_tables(v[0], v[1])
+ return scope
+
+ return merge_tables(root, tables)
+
+class _Source:
+ def __init__(self, s, filename=None):
+ self.s = s
+ self._pos = (1, 1)
+ self._last = None
+ self._filename = filename
+ self.backtrack_stack = []
+
+ def last(self):
+ return self._last
+
+ def pos(self):
+ return self._pos
+
+ def fail(self):
+ return self._expect(None)
+
+ def consume_dot(self):
+ if self.s:
+ self._last = self.s[0]
+ self.s = self[1:]
+ self._advance(self._last)
+ return self._last
+ return None
+
+ def expect_dot(self):
+ return self._expect(self.consume_dot())
+
+ def consume_eof(self):
+ if not self.s:
+ self._last = ''
+ return True
+ return False
+
+ def expect_eof(self):
+ return self._expect(self.consume_eof())
+
+ def consume(self, s):
+ if self.s.startswith(s):
+ self.s = self.s[len(s):]
+ self._last = s
+ self._advance(s)
+ return True
+ return False
+
+ def expect(self, s):
+ return self._expect(self.consume(s))
+
+ def consume_re(self, re):
+ m = re.match(self.s)
+ if m:
+ self.s = self.s[len(m.group(0)):]
+ self._last = m
+ self._advance(m.group(0))
+ return m
+ return None
+
+ def expect_re(self, re):
+ return self._expect(self.consume_re(re))
+
+ def __enter__(self):
+ self.backtrack_stack.append((self.s, self._pos))
+
+ def __exit__(self, type, value, traceback):
+ if type is None:
+ self.backtrack_stack.pop()
+ else:
+ self.s, self._pos = self.backtrack_stack.pop()
+ return type == TomlError
+
+ def commit(self):
+ self.backtrack_stack[-1] = (self.s, self._pos)
+
+ def _expect(self, r):
+ if not r:
+ raise TomlError('msg', self._pos[0], self._pos[1], self._filename)
+ return r
+
+ def _advance(self, s):
+ suffix_pos = s.rfind('\n')
+ if suffix_pos == -1:
+ self._pos = (self._pos[0], self._pos[1] + len(s))
+ else:
+ self._pos = (self._pos[0] + s.count('\n'), len(s) - suffix_pos)
+
+_ews_re = re.compile(r'(?:[ \t]|#[^\n]*\n|#[^\n]*\Z|\n)*')
+def _p_ews(s):
+ s.expect_re(_ews_re)
+
+_ws_re = re.compile(r'[ \t]*')
+def _p_ws(s):
+ s.expect_re(_ws_re)
+
+_escapes = { 'b': '\b', 'n': '\n', 'r': '\r', 't': '\t', '"': '"', '\'': '\'',
+ '\\': '\\', '/': '/', 'f': '\f' }
+
+_basicstr_re = re.compile(r'[^"\\\000-\037]*')
+_short_uni_re = re.compile(r'u([0-9a-fA-F]{4})')
+_long_uni_re = re.compile(r'U([0-9a-fA-F]{8})')
+_escapes_re = re.compile('[bnrt"\'\\\\/f]')
+_newline_esc_re = re.compile('\n[ \t\n]*')
+def _p_basicstr_content(s, content=_basicstr_re):
+ res = []
+ while True:
+ res.append(s.expect_re(content).group(0))
+ if not s.consume('\\'):
+ break
+ if s.consume_re(_newline_esc_re):
+ pass
+ elif s.consume_re(_short_uni_re) or s.consume_re(_long_uni_re):
+ res.append(_chr(int(s.last().group(1), 16)))
+ else:
+ s.expect_re(_escapes_re)
+ res.append(_escapes[s.last().group(0)])
+ return ''.join(res)
+
+_key_re = re.compile(r'[0-9a-zA-Z-_]+')
+def _p_key(s):
+ with s:
+ s.expect('"')
+ r = _p_basicstr_content(s, _basicstr_re)
+ s.expect('"')
+ return r
+ return s.expect_re(_key_re).group(0)
+
+_float_re = re.compile(r'[+-]?(?:0|[1-9](?:_?\d)*)(?:\.\d(?:_?\d)*)?(?:[eE][+-]?(?:\d(?:_?\d)*))?')
+_datetime_re = re.compile(r'(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(?:Z|([+-]\d{2}):(\d{2}))')
+
+_basicstr_ml_re = re.compile(r'(?:(?:|"|"")[^"\\\000-\011\013-\037])*')
+_litstr_re = re.compile(r"[^'\000-\037]*")
+_litstr_ml_re = re.compile(r"(?:(?:|'|'')(?:[^'\000-\011\013-\037]))*")
+def _p_value(s):
+ pos = s.pos()
+
+ if s.consume('true'):
+ return 'bool', s.last(), True, pos
+ if s.consume('false'):
+ return 'bool', s.last(), False, pos
+
+ if s.consume('"'):
+ if s.consume('""'):
+ r = _p_basicstr_content(s, _basicstr_ml_re)
+ s.expect('"""')
+ else:
+ r = _p_basicstr_content(s, _basicstr_re)
+ s.expect('"')
+ return 'str', r, r, pos
+
+ if s.consume('\''):
+ if s.consume('\'\''):
+ r = s.expect_re(_litstr_ml_re).group(0)
+ s.expect('\'\'\'')
+ else:
+ r = s.expect_re(_litstr_re).group(0)
+ s.expect('\'')
+ return 'str', r, r, pos
+
+ if s.consume_re(_datetime_re):
+ m = s.last()
+ s0 = m.group(0)
+ r = map(int, m.groups()[:6])
+ if m.group(7):
+ micro = float(m.group(7))
+ else:
+ micro = 0
+
+ if m.group(8):
+ g = int(m.group(8), 10) * 60 + int(m.group(9), 10)
+ tz = _TimeZone(datetime.timedelta(0, g * 60))
+ else:
+ tz = _TimeZone(datetime.timedelta(0, 0))
+
+ y, m, d, H, M, S = r
+ dt = datetime.datetime(y, m, d, H, M, S, int(micro * 1000000), tz)
+ return 'datetime', s0, dt, pos
+
+ if s.consume_re(_float_re):
+ m = s.last().group(0)
+ r = m.replace('_','')
+ if '.' in m or 'e' in m or 'E' in m:
+ return 'float', m, float(r), pos
+ else:
+ return 'int', m, int(r, 10), pos
+
+ if s.consume('['):
+ items = []
+ with s:
+ while True:
+ _p_ews(s)
+ items.append(_p_value(s))
+ s.commit()
+ _p_ews(s)
+ s.expect(',')
+ s.commit()
+ _p_ews(s)
+ s.expect(']')
+ return 'array', None, items, pos
+
+ if s.consume('{'):
+ _p_ws(s)
+ items = {}
+ if not s.consume('}'):
+ k = _p_key(s)
+ _p_ws(s)
+ s.expect('=')
+ _p_ws(s)
+ items[k] = _p_value(s)
+ _p_ws(s)
+ while s.consume(','):
+ _p_ws(s)
+ k = _p_key(s)
+ _p_ws(s)
+ s.expect('=')
+ _p_ws(s)
+ items[k] = _p_value(s)
+ _p_ws(s)
+ s.expect('}')
+ return 'table', None, items, pos
+
+ s.fail()
+
+def _p_stmt(s):
+ pos = s.pos()
+ if s.consume( '['):
+ is_array = s.consume('[')
+ _p_ws(s)
+ keys = [_p_key(s)]
+ _p_ws(s)
+ while s.consume('.'):
+ _p_ws(s)
+ keys.append(_p_key(s))
+ _p_ws(s)
+ s.expect(']')
+ if is_array:
+ s.expect(']')
+ return 'table_array' if is_array else 'table', keys, pos
+
+ key = _p_key(s)
+ _p_ws(s)
+ s.expect('=')
+ _p_ws(s)
+ value = _p_value(s)
+ return 'kv', (key, value), pos
+
+_stmtsep_re = re.compile(r'(?:[ \t]*(?:#[^\n]*)?\n)+[ \t]*')
+def _p_toml(s):
+ stmts = []
+ _p_ews(s)
+ with s:
+ stmts.append(_p_stmt(s))
+ while True:
+ s.commit()
+ s.expect_re(_stmtsep_re)
+ stmts.append(_p_stmt(s))
+ _p_ews(s)
+ s.expect_eof()
+ return stmts
+
+class _TimeZone(datetime.tzinfo):
+ def __init__(self, offset):
+ self._offset = offset
+
+ def utcoffset(self, dt):
+ return self._offset
+
+ def dst(self, dt):
+ return None
+
+ def tzname(self, dt):
+ m = self._offset.total_seconds() // 60
+ if m < 0:
+ res = '-'
+ m = -m
+ else:
+ res = '+'
+ h = m // 60
+ m = m - h * 60
+ return '{}{:.02}{:.02}'.format(res, h, m)
diff --git a/pip/_vendor/pytoml/writer.py b/pip/_vendor/pytoml/writer.py
new file mode 100644
index 00000000000..562884b83ac
--- /dev/null
+++ b/pip/_vendor/pytoml/writer.py
@@ -0,0 +1,121 @@
+from __future__ import unicode_literals
+import io, datetime, sys
+
+if sys.version_info[0] == 3:
+ long = int
+ unicode = str
+
+
+def dumps(obj, sort_keys=False):
+ fout = io.StringIO()
+ dump(obj, fout, sort_keys=sort_keys)
+ return fout.getvalue()
+
+
+_escapes = {'\n': 'n', '\r': 'r', '\\': '\\', '\t': 't', '\b': 'b', '\f': 'f', '"': '"'}
+
+
+def _escape_string(s):
+ res = []
+ start = 0
+
+ def flush():
+ if start != i:
+ res.append(s[start:i])
+ return i + 1
+
+ i = 0
+ while i < len(s):
+ c = s[i]
+ if c in '"\\\n\r\t\b\f':
+ start = flush()
+ res.append('\\' + _escapes[c])
+ elif ord(c) < 0x20:
+ start = flush()
+ res.append('\\u%04x' % ord(c))
+ i += 1
+
+ flush()
+ return '"' + ''.join(res) + '"'
+
+
+def _escape_id(s):
+ if any(not c.isalnum() and c not in '-_' for c in s):
+ return _escape_string(s)
+ return s
+
+
+def _format_list(v):
+ return '[{0}]'.format(', '.join(_format_value(obj) for obj in v))
+
+# Formula from:
+# https://docs.python.org/2/library/datetime.html#datetime.timedelta.total_seconds
+# Once support for py26 is dropped, this can be replaced by td.total_seconds()
+def _total_seconds(td):
+ return ((td.microseconds
+ + (td.seconds + td.days * 24 * 3600) * 10**6) / 10.0**6)
+
+def _format_value(v):
+ if isinstance(v, bool):
+ return 'true' if v else 'false'
+ if isinstance(v, int) or isinstance(v, long):
+ return unicode(v)
+ if isinstance(v, float):
+ return repr(v)
+ elif isinstance(v, unicode) or isinstance(v, bytes):
+ return _escape_string(v)
+ elif isinstance(v, datetime.datetime):
+ offs = v.utcoffset()
+ offs = _total_seconds(offs) // 60 if offs is not None else 0
+
+ if offs == 0:
+ suffix = 'Z'
+ else:
+ if offs > 0:
+ suffix = '+'
+ else:
+ suffix = '-'
+ offs = -offs
+ suffix = '{0}{1:.02}{2:.02}'.format(suffix, offs // 60, offs % 60)
+
+ if v.microsecond:
+ return v.strftime('%Y-%m-%dT%H:%M:%S.%f') + suffix
+ else:
+ return v.strftime('%Y-%m-%dT%H:%M:%S') + suffix
+ elif isinstance(v, list):
+ return _format_list(v)
+ else:
+ raise RuntimeError(v)
+
+
+def dump(obj, fout, sort_keys=False):
+ tables = [((), obj, False)]
+
+ while tables:
+ name, table, is_array = tables.pop()
+ if name:
+ section_name = '.'.join(_escape_id(c) for c in name)
+ if is_array:
+ fout.write('[[{0}]]\n'.format(section_name))
+ else:
+ fout.write('[{0}]\n'.format(section_name))
+
+ table_keys = sorted(table.keys()) if sort_keys else table.keys()
+ new_tables = []
+ for k in table_keys:
+ v = table[k]
+ if isinstance(v, dict):
+ new_tables.append((name + (k,), v, False))
+ elif isinstance(v, list) and v and all(isinstance(o, dict) for o in v):
+ new_tables.extend((name + (k,), d, True) for d in v)
+ elif v is None:
+ # based on mojombo's comment: https://github.com/toml-lang/toml/issues/146#issuecomment-25019344
+ fout.write(
+ '#{} = null # To use: uncomment and replace null with value\n'.format(_escape_id(k)))
+ else:
+ fout.write('{0} = {1}\n'.format(_escape_id(k), _format_value(v)))
+
+ tables.extend(reversed(new_tables))
+
+ if tables:
+ fout.write('\n')
diff --git a/pip/_vendor/vendor.txt b/pip/_vendor/vendor.txt
index 5421ae471e7..4a90605a0dc 100644
--- a/pip/_vendor/vendor.txt
+++ b/pip/_vendor/vendor.txt
@@ -11,6 +11,7 @@ progress==1.3
ipaddress==1.0.18 # Only needed on 2.6 and 2.7
packaging==16.8
pyparsing==2.2.0
+pytoml==0.1.12
retrying==1.3.3
requests==2.13.0
setuptools==34.3.3
diff --git a/pip/commands/wheel.py b/pip/commands/wheel.py
index d2fea830e82..a631dba03b1 100644
--- a/pip/commands/wheel.py
+++ b/pip/commands/wheel.py
@@ -164,6 +164,7 @@ def run(self, options, args):
finder,
build_options=options.build_options or [],
global_options=options.global_options or [],
+ no_clean=options.no_clean,
)
if not wb.build():
raise CommandError(
diff --git a/pip/req/req_install.py b/pip/req/req_install.py
index 86f4c550e57..504439b2c2d 100644
--- a/pip/req/req_install.py
+++ b/pip/req/req_install.py
@@ -427,6 +427,18 @@ def setup_py(self):
return setup_py
+ @property
+ def pyproject_toml(self):
+ assert self.source_dir, "No source dir for %s" % self
+
+ pp_toml = os.path.join(self.setup_py_dir, 'pyproject.toml')
+
+ # Python2 __file__ should not be unicode
+ if six.PY2 and isinstance(pp_toml, six.text_type):
+ pp_toml = pp_toml.encode(sys.getfilesystemencoding())
+
+ return pp_toml
+
def run_egg_info(self):
assert self.source_dir
if self.name:
diff --git a/pip/wheel.py b/pip/wheel.py
index 8abe958deca..5a1cacc39ea 100644
--- a/pip/wheel.py
+++ b/pip/wheel.py
@@ -4,6 +4,7 @@
from __future__ import absolute_import
import compileall
+import copy
import csv
import errno
import hashlib
@@ -19,11 +20,6 @@
from base64 import urlsafe_b64encode
from email.parser import Parser
-from pip._vendor import pkg_resources
-from pip._vendor.distlib.scripts import ScriptMaker
-from pip._vendor.packaging.utils import canonicalize_name
-from pip._vendor.six import StringIO
-
import pip
from pip import pep425tags
from pip.compat import expanduser
@@ -38,6 +34,13 @@
from pip.utils.logging import indent_log
from pip.utils.setuptools_build import SETUPTOOLS_SHIM
from pip.utils.ui import open_spinner
+from pip._vendor.distlib.scripts import ScriptMaker
+from pip._vendor import pkg_resources
+from pip._vendor.packaging.utils import canonicalize_name
+from pip._vendor import pytoml
+from pip._vendor.six import StringIO
+
+from sysconfig import get_paths
wheel_ext = '.whl'
@@ -186,6 +189,7 @@ def fix_script(path):
script.write(rest)
return True
+
dist_info_re = re.compile(r"""^(?P(?P.+?)(-(?P.+?))?)
\.dist-info$""", re.VERBOSE)
@@ -630,26 +634,128 @@ def supported(self, tags=None):
return bool(set(tags).intersection(self.file_tags))
+class BuildEnvironment(object):
+ """Context manager to install build deps in a simple temporary environment
+ """
+ def __init__(self, no_clean=False):
+ self.prefix = tempfile.mkdtemp('pip-build-env-')
+ self.no_clean = no_clean
+
+ def __enter__(self):
+ self.save_path = os.environ.get('PATH', None)
+ self.save_pythonpath = os.environ.get('PYTHONPATH', None)
+
+ install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix'
+ install_dirs = get_paths(install_scheme, vars={
+ 'base': self.prefix,
+ 'platbase': self.prefix,
+ })
+
+ scripts = install_dirs['scripts']
+ if self.save_path:
+ os.environ['PATH'] = scripts + os.pathsep + self.save_path
+ else:
+ os.environ['PATH'] = scripts + os.pathsep + os.defpath
+
+ if install_dirs['purelib'] == install_dirs['platlib']:
+ lib_dirs = install_dirs['purelib']
+ else:
+ lib_dirs = install_dirs['purelib'] + os.pathsep + \
+ install_dirs['platlib']
+ if self.save_pythonpath:
+ os.environ['PYTHONPATH'] = lib_dirs + os.pathsep + \
+ self.save_pythonpath
+ else:
+ os.environ['PYTHONPATH'] = lib_dirs
+
+ return self.prefix
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if self.save_path is None:
+ os.environ.pop('PATH', None)
+ else:
+ os.environ['PATH'] = self.save_path
+
+ if self.save_pythonpath is None:
+ os.environ.pop('PYTHONPATH', None)
+ else:
+ os.environ['PYTHONPATH'] = self.save_pythonpath
+
+ if not self.no_clean:
+ rmtree(self.prefix)
+
+
class WheelBuilder(object):
"""Build wheels from a RequirementSet."""
def __init__(self, requirement_set, finder, build_options=None,
- global_options=None):
+ global_options=None, no_clean=False):
self.requirement_set = requirement_set
self.finder = finder
self._cache_root = requirement_set._wheel_cache._cache_dir
self._wheel_dir = requirement_set.wheel_download_dir
self.build_options = build_options or []
self.global_options = global_options or []
+ self.no_clean = no_clean
+
+ def _find_build_reqs(self, req):
+ """Get a list of the packages required to build the project, if any,
+ and a flag indicating whether pyproject.toml is present, indicating
+ that the build should be isolated.
+
+ Build requirements can be specified in a pyproject.toml, as described
+ in PEP 518. If this file exists but doesn't specify build
+ requirements, pip will default to installing setuptools and wheel.
+ """
+ if os.path.isfile(req.pyproject_toml):
+ with open(req.pyproject_toml) as f:
+ pp_toml = pytoml.load(f)
+ return pp_toml.get('build-system', {})\
+ .get('requires', ['setuptools', 'wheel']), True
+
+ return ['setuptools', 'wheel'], False
+
+ def _install_build_reqs(self, reqs, prefix):
+ # Local import to avoid circular import (wheel <-> req_install)
+ from pip.req.req_install import InstallRequirement
+ from pip.index import FormatControl
+ # Ignore the --no-binary option when installing the build system, so
+ # we don't recurse trying to build a self-hosting build system.
+ finder = copy.copy(self.finder)
+ finder.format_control = FormatControl(set(), set())
+ urls = [finder.find_requirement(InstallRequirement.from_line(r),
+ upgrade=False).url
+ for r in reqs]
+
+ args = [sys.executable, '-m', 'pip', 'install', '--ignore-installed',
+ '--prefix', prefix] + list(urls)
+ with open_spinner("Installing build dependencies") as spinner:
+ call_subprocess(args, show_stdout=False, spinner=spinner)
def _build_one(self, req, output_dir, python_tag=None):
"""Build one wheel.
:return: The filename of the built wheel, or None if the build failed.
"""
+ build_reqs, isolate = self._find_build_reqs(req)
+ if 'setuptools' not in build_reqs:
+ logger.warning(
+ "This version of pip does not implement PEP 516, so "
+ "it cannot build a wheel without setuptools. You may need to "
+ "upgrade to a newer version of pip.")
+ # Install build deps into temporary prefix (PEP 518)
+ with BuildEnvironment(no_clean=self.no_clean) as prefix:
+ self._install_build_reqs(build_reqs, prefix)
+ return self._build_one_inside_env(req, output_dir,
+ python_tag=python_tag,
+ isolate=True)
+
+ def _build_one_inside_env(self, req, output_dir, python_tag=None,
+ isolate=False):
tempd = tempfile.mkdtemp('pip-wheel-')
try:
- if self.__build_one(req, tempd, python_tag=python_tag):
+ if self.__build_one(req, tempd, python_tag=python_tag,
+ isolate=isolate):
try:
wheel_name = os.listdir(tempd)[0]
wheel_path = os.path.join(output_dir, wheel_name)
@@ -664,14 +770,20 @@ def _build_one(self, req, output_dir, python_tag=None):
finally:
rmtree(tempd)
- def _base_setup_args(self, req):
+ def _base_setup_args(self, req, isolate=False):
+ flags = '-u'
+ # The -S flag currently breaks Python in virtualenvs, because it relies
+ # on site.py to find parts of the standard library outside the env. So
+ # isolation is disabled for now.
+ # if isolate:
+ # flags += 'S'
return [
- sys.executable, "-u", '-c',
+ sys.executable, flags, '-c',
SETUPTOOLS_SHIM % req.setup_py
] + list(self.global_options)
- def __build_one(self, req, tempd, python_tag=None):
- base_args = self._base_setup_args(req)
+ def __build_one(self, req, tempd, python_tag=None, isolate=False):
+ base_args = self._base_setup_args(req, isolate=isolate)
spin_message = 'Running setup.py bdist_wheel for %s' % (req.name,)
with open_spinner(spin_message) as spinner:
@@ -682,8 +794,13 @@ def __build_one(self, req, tempd, python_tag=None):
if python_tag is not None:
wheel_args += ["--python-tag", python_tag]
+ env = {}
+ if isolate:
+ env['PYTHONNOUSERSITE'] = '1'
+
try:
call_subprocess(wheel_args, cwd=req.setup_py_dir,
+ extra_environ=env,
show_stdout=False, spinner=spinner)
return True
except:
diff --git a/tests/data/packages/pep518-3.0.tar.gz b/tests/data/packages/pep518-3.0.tar.gz
new file mode 100644
index 00000000000..b80d4e71089
Binary files /dev/null and b/tests/data/packages/pep518-3.0.tar.gz differ
diff --git a/tests/data/src/pep518-3.0/MANIFEST.in b/tests/data/src/pep518-3.0/MANIFEST.in
new file mode 100644
index 00000000000..bec201fc83b
--- /dev/null
+++ b/tests/data/src/pep518-3.0/MANIFEST.in
@@ -0,0 +1 @@
+include pyproject.toml
diff --git a/tests/data/src/pep518-3.0/pyproject.toml b/tests/data/src/pep518-3.0/pyproject.toml
new file mode 100644
index 00000000000..5e3bb233223
--- /dev/null
+++ b/tests/data/src/pep518-3.0/pyproject.toml
@@ -0,0 +1,2 @@
+[build-system]
+requires=["simple==3.0", "setuptools", "wheel"]
diff --git a/tests/data/src/pep518-3.0/setup.cfg b/tests/data/src/pep518-3.0/setup.cfg
new file mode 100644
index 00000000000..861a9f55426
--- /dev/null
+++ b/tests/data/src/pep518-3.0/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/tests/data/src/pep518-3.0/setup.py b/tests/data/src/pep518-3.0/setup.py
new file mode 100644
index 00000000000..7ff29e276df
--- /dev/null
+++ b/tests/data/src/pep518-3.0/setup.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python
+from setuptools import setup, find_packages
+
+setup(name='pep518',
+ version='3.0',
+ packages=find_packages()
+ )
diff --git a/tests/data/src/pep518-3.0/simple/__init__.py b/tests/data/src/pep518-3.0/simple/__init__.py
new file mode 100644
index 00000000000..7986d11379a
--- /dev/null
+++ b/tests/data/src/pep518-3.0/simple/__init__.py
@@ -0,0 +1 @@
+#dummy
diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py
index 995ee3471b2..a26a7c51d7e 100644
--- a/tests/functional/test_freeze.py
+++ b/tests/functional/test_freeze.py
@@ -494,13 +494,17 @@ def test_freeze_with_requirement_option_multiple(script):
assert result.stdout.count("--index-url http://ignore") == 1
-def test_freeze_user(script, virtualenv):
+@pytest.mark.network
+def test_freeze_user(script, virtualenv, data):
"""
Testing freeze with --user, first we have to install some stuff.
"""
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
virtualenv.system_site_packages = True
- script.pip_install_local('--user', 'simple==2.0')
- script.pip_install_local('simple2==3.0')
+ script.pip_install_local('--find-links', data.find_links,
+ '--user', 'simple==2.0')
+ script.pip_install_local('--find-links', data.find_links,
+ 'simple2==3.0')
result = script.pip('freeze', '--user', expect_stderr=True)
expected = textwrap.dedent("""\
simple==2.0
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index 88a5d1fba3c..6d1cf014e21 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -925,6 +925,7 @@ def test_install_topological_sort(script, data):
@pytest.mark.network
def test_install_wheel_broken(script, data):
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
res = script.pip(
'install', '--no-index', '-f', data.find_links, 'wheelbroken',
expect_stderr=True)
@@ -934,6 +935,7 @@ def test_install_wheel_broken(script, data):
@pytest.mark.network
def test_cleanup_after_failed_wheel(script, data):
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
res = script.pip(
'install', '--no-index', '-f', data.find_links, 'wheelbrokenafter',
expect_stderr=True)
@@ -952,6 +954,7 @@ def test_install_builds_wheels(script, data):
# see test_install_editable_from_git_autobuild_wheel for editable
# vcs coverage.
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
to_install = data.packages.join('requires_wheelbroken_upper')
res = script.pip(
'install', '--no-index', '-f', data.find_links,
@@ -988,6 +991,7 @@ def test_install_builds_wheels(script, data):
@pytest.mark.network
def test_install_no_binary_disables_building_wheels(script, data):
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
to_install = data.packages.join('requires_wheelbroken_upper')
res = script.pip(
'install', '--no-index', '--no-binary=upper', '-f', data.find_links,
@@ -1020,6 +1024,7 @@ def test_install_no_binary_disables_building_wheels(script, data):
@pytest.mark.network
def test_install_no_binary_disables_cached_wheels(script, data):
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
# Seed the cache
script.pip(
'install', '--no-index', '-f', data.find_links,
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
index 797f1f30aa8..18eebc187fc 100644
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -225,6 +225,7 @@ def test_install_local_with_subdirectory(script):
def test_wheel_user_with_prefix_in_pydistutils_cfg(script, data, virtualenv):
# Make sure wheel is available in the virtualenv
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
virtualenv.system_site_packages = True
homedir = script.environ["HOME"]
script.scratch_path.join("bin").mkdir()
@@ -337,7 +338,7 @@ def test_constrained_to_url_install_same_url(script, data):
@pytest.mark.network
-def test_double_install_spurious_hash_mismatch(script, tmpdir):
+def test_double_install_spurious_hash_mismatch(script, tmpdir, data):
"""Make sure installing the same hashed sdist twice doesn't throw hash
mismatch errors.
@@ -348,11 +349,13 @@ def test_double_install_spurious_hash_mismatch(script, tmpdir):
"""
script.pip('install', 'wheel') # Otherwise, it won't try to build wheels.
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
with requirements_file('simple==1.0 --hash=sha256:393043e672415891885c9a2a'
'0929b1af95fb866d6ca016b42d2e6ce53619b653',
tmpdir) as reqs_file:
# Install a package (and build its wheel):
result = script.pip_install_local(
+ '--find-links', data.find_links,
'-r', reqs_file.abspath, expect_error=False)
assert 'Successfully installed simple-1.0' in str(result)
@@ -362,6 +365,7 @@ def test_double_install_spurious_hash_mismatch(script, tmpdir):
# Then install it again. We should not hit a hash mismatch, and the
# package should install happily.
result = script.pip_install_local(
+ '--find-links', data.find_links,
'-r', reqs_file.abspath, expect_error=False)
assert 'Successfully installed simple-1.0' in str(result)
diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py
index 041270263ef..80cd22f237b 100644
--- a/tests/functional/test_list.py
+++ b/tests/functional/test_list.py
@@ -122,12 +122,14 @@ def test_local_legacy_flag(script, data):
assert 'simple (1.0)' in result.stdout
+@pytest.mark.network
def test_user_flag(script, data, virtualenv):
"""
Test the behavior of --user flag in the list command
"""
virtualenv.system_site_packages = True
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
script.pip('install', '-f', data.find_links, '--no-index',
'--user', 'simple2==2.0')
@@ -137,12 +139,14 @@ def test_user_flag(script, data, virtualenv):
assert {"name": "simple2", "version": "2.0"} in json.loads(result.stdout)
+@pytest.mark.network
def test_user_columns_flag(script, data, virtualenv):
"""
Test the behavior of --user --format=columns flags in the list command
"""
virtualenv.system_site_packages = True
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
script.pip('install', '-f', data.find_links, '--no-index',
'--user', 'simple2==2.0')
@@ -153,12 +157,14 @@ def test_user_columns_flag(script, data, virtualenv):
assert 'simple2 2.0' in result.stdout, str(result)
+@pytest.mark.network
def test_user_legacy(script, data, virtualenv):
"""
Test the behavior of --user flag in the list command
"""
virtualenv.system_site_packages = True
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
script.pip('install', '-f', data.find_links, '--no-index',
'--user', 'simple2==2.0')
diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py
index b97f320e00c..f776b23d283 100644
--- a/tests/functional/test_wheel.py
+++ b/tests/functional/test_wheel.py
@@ -44,6 +44,7 @@ def test_pip_wheel_success(script, data):
Test 'pip wheel' success.
"""
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
result = script.pip(
'wheel', '--no-index', '-f', data.find_links, 'simple==3.0',
)
@@ -71,6 +72,7 @@ def test_pip_wheel_downloads_wheels(script, data):
@pytest.mark.network
def test_pip_wheel_builds_when_no_binary_set(script, data):
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
data.packages.join('simple-3.0-py2.py3-none-any.whl').touch()
# Check that the wheel package is ignored
res = script.pip(
@@ -85,6 +87,7 @@ def test_pip_wheel_builds_editable_deps(script, data):
Test 'pip wheel' finds and builds dependencies of editables
"""
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
editable_path = os.path.join(data.src, 'requires_simple')
result = script.pip(
'wheel', '--no-index', '-f', data.find_links, '-e', editable_path
@@ -100,9 +103,10 @@ def test_pip_wheel_builds_editable(script, data):
Test 'pip wheel' builds an editable package
"""
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
editable_path = os.path.join(data.src, 'simplewheel-1.0')
result = script.pip(
- 'wheel', '--no-index', '-e', editable_path
+ 'wheel', '--no-index', '-f', data.find_links, '-e', editable_path
)
wheel_file_name = 'simplewheel-1.0-py%s-none-any.whl' % pyversion[0]
wheel_file_path = script.scratch / wheel_file_name
@@ -115,6 +119,7 @@ def test_pip_wheel_fail(script, data):
Test 'pip wheel' failure.
"""
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
result = script.pip(
'wheel', '--no-index', '-f', data.find_links, 'wheelbroken==0.1',
expect_error=True,
@@ -136,10 +141,12 @@ def test_no_clean_option_blocks_cleaning_after_wheel(script, data):
Test --no-clean option blocks cleaning after wheel build
"""
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
build = script.venv_path / 'build'
result = script.pip(
'wheel', '--no-clean', '--no-index', '--build', build,
'--find-links=%s' % data.find_links, 'simple',
+ expect_temp=True,
)
build = build / 'simple'
assert exists(build), "build/simple should still exist %s" % str(result)
@@ -153,6 +160,7 @@ def test_pip_wheel_source_deps(script, data):
"""
# 'requires_source' is a wheel that depends on the 'source' project
script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
result = script.pip(
'wheel', '--no-index', '-f', data.find_links, 'requires_source',
)
@@ -196,3 +204,17 @@ def test_wheel_package_with_latin1_setup(script, data):
pkg_to_wheel = data.packages.join("SetupPyLatin1")
result = script.pip('wheel', pkg_to_wheel)
assert 'Successfully built SetupPyUTF8' in result.stdout
+
+
+@pytest.mark.network
+def test_pip_wheel_with_pep518_build_reqs(script, data):
+ script.pip('install', 'wheel')
+ script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
+ result = script.pip(
+ 'wheel', '--no-index', '-f', data.find_links, 'pep518==3.0',
+ )
+ wheel_file_name = 'pep518-3.0-py%s-none-any.whl' % pyversion[0]
+ wheel_file_path = script.scratch / wheel_file_name
+ assert wheel_file_path in result.files_created, result.stdout
+ assert "Successfully built pep518" in result.stdout, result.stdout
+ assert "Installing build dependencies" in result.stdout, result.stdout