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