aboutsummaryrefslogtreecommitdiff
path: root/dev/deps.py
diff options
context:
space:
mode:
Diffstat (limited to 'dev/deps.py')
-rw-r--r--dev/deps.py287
1 files changed, 234 insertions, 53 deletions
diff --git a/dev/deps.py b/dev/deps.py
index 7014172..8f52336 100644
--- a/dev/deps.py
+++ b/dev/deps.py
@@ -96,7 +96,78 @@ def _tuple_from_ver(version_string):
A tuple of integers
"""
- return tuple(map(int, version_string.split('.')))
+ match = re.search(
+ r'(\d+(?:\.\d+)*)'
+ r'([-._]?(?:alpha|a|beta|b|preview|pre|c|rc)\.?\d*)?'
+ r'(-\d+|(?:[-._]?(?:rev|r|post)\.?\d*))?'
+ r'([-._]?dev\.?\d*)?',
+ version_string
+ )
+ if not match:
+ return tuple()
+
+ nums = tuple(map(int, match.group(1).split('.')))
+
+ pre = match.group(2)
+ if pre:
+ pre = pre.replace('alpha', 'a')
+ pre = pre.replace('beta', 'b')
+ pre = pre.replace('preview', 'rc')
+ pre = pre.replace('pre', 'rc')
+ pre = re.sub(r'(?<!r)c', 'rc', pre)
+ pre = pre.lstrip('._-')
+ pre_dig_match = re.search(r'\d+', pre)
+ if pre_dig_match:
+ pre_dig = int(pre_dig_match.group(0))
+ else:
+ pre_dig = 0
+ pre = pre.rstrip('0123456789')
+
+ pre_num = {
+ 'a': -3,
+ 'b': -2,
+ 'rc': -1,
+ }[pre]
+
+ pre_tup = (pre_num, pre_dig)
+ else:
+ pre_tup = tuple()
+
+ post = match.group(3)
+ if post:
+ post_dig_match = re.search(r'\d+', post)
+ if post_dig_match:
+ post_dig = int(post_dig_match.group(0))
+ else:
+ post_dig = 0
+ post_tup = (1, post_dig)
+ else:
+ post_tup = tuple()
+
+ dev = match.group(4)
+ if dev:
+ dev_dig_match = re.search(r'\d+', dev)
+ if dev_dig_match:
+ dev_dig = int(dev_dig_match.group(0))
+ else:
+ dev_dig = 0
+ dev_tup = (-4, dev_dig)
+ else:
+ dev_tup = tuple()
+
+ normalized = [nums]
+ if pre_tup:
+ normalized.append(pre_tup)
+ if post_tup:
+ normalized.append(post_tup)
+ if dev_tup:
+ normalized.append(dev_tup)
+ # This ensures regular releases happen after dev and prerelease, but
+ # before post releases
+ if not pre_tup and not post_tup and not dev_tup:
+ normalized.append((0, 0))
+
+ return tuple(normalized)
def _open_archive(path):
@@ -309,17 +380,99 @@ def _extract_package(deps_dir, pkg_path, pkg_dir):
shutil.rmtree(staging_dir)
-def _stage_requirements(deps_dir, path):
+def _sort_pep440_versions(releases, include_prerelease):
"""
- Installs requirements without using Python to download, since
- different services are limiting to TLS 1.2, and older version of
- Python do not support that
+ :param releases:
+ A list of unicode string PEP 440 version numbers
- :param deps_dir:
- A unicode path to a temporary diretory to use for downloads
+ :param include_prerelease:
+ A boolean indicating if prerelease versions should be included
- :param path:
- A unicode filesystem path to a requirements file
+ :return:
+ A sorted generator of 2-element tuples:
+ 0: A unicode string containing a PEP 440 version number
+ 1: A tuple of tuples containing integers - this is the output of
+ _tuple_from_ver() for the PEP 440 version number and is intended
+ for comparing versions
+ """
+
+ parsed_versions = []
+ for v in releases:
+ t = _tuple_from_ver(v)
+ if not include_prerelease and t[1][0] < 0:
+ continue
+ parsed_versions.append((v, t))
+
+ return sorted(parsed_versions, key=lambda v: v[1])
+
+
+def _is_valid_python_version(python_version, requires_python):
+ """
+ Verifies the "python_version" and "requires_python" keys from a PyPi
+ download record are applicable to the current version of Python
+
+ :param python_version:
+ The "python_version" value from a PyPi download JSON structure. This
+ should be one of: "py2", "py3", "py2.py3" or "source".
+
+ :param requires_python:
+ The "requires_python" value from a PyPi download JSON structure. This
+ will be None, or a comma-separated list of conditions that must be
+ true. Ex: ">=3.5", "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+ """
+
+ if python_version == "py2" and sys.version_info >= (3,):
+ return False
+ if python_version == "py3" and sys.version_info < (3,):
+ return False
+
+ if requires_python is not None:
+
+ def _ver_tuples(ver_str):
+ ver_str = ver_str.strip()
+ if ver_str.endswith('.*'):
+ ver_str = ver_str[:-2]
+ cond_tup = tuple(map(int, ver_str.split('.')))
+ return (sys.version_info[:len(cond_tup)], cond_tup)
+
+ for part in map(str_cls.strip, requires_python.split(',')):
+ if part.startswith('!='):
+ sys_tup, cond_tup = _ver_tuples(part[2:])
+ if sys_tup == cond_tup:
+ return False
+ elif part.startswith('>='):
+ sys_tup, cond_tup = _ver_tuples(part[2:])
+ if sys_tup < cond_tup:
+ return False
+ elif part.startswith('>'):
+ sys_tup, cond_tup = _ver_tuples(part[1:])
+ if sys_tup <= cond_tup:
+ return False
+ elif part.startswith('<='):
+ sys_tup, cond_tup = _ver_tuples(part[2:])
+ if sys_tup > cond_tup:
+ return False
+ elif part.startswith('<'):
+ sys_tup, cond_tup = _ver_tuples(part[1:])
+ if sys_tup >= cond_tup:
+ return False
+ elif part.startswith('=='):
+ sys_tup, cond_tup = _ver_tuples(part[2:])
+ if sys_tup != cond_tup:
+ return False
+
+ return True
+
+
+def _locate_suitable_download(downloads):
+ """
+ :param downloads:
+ A list of dicts containing a key "url", "python_version" and
+ "requires_python"
+
+ :return:
+ A unicode string URL, or None if not a valid release for the current
+ version of Python
"""
valid_tags = _pep425tags()
@@ -330,8 +483,64 @@ def _stage_requirements(deps_dir, path):
version_info = sys.version_info
exe_suffix = '.%s-py%d.%d.exe' % (win_arch, version_info[0], version_info[1])
+ wheels = {}
+ whl = None
+ tar_bz2 = None
+ tar_gz = None
+ exe = None
+ for download in downloads:
+ if not _is_valid_python_version(download.get('python_version'), download.get('requires_python')):
+ continue
+
+ if exe_suffix and download['url'].endswith(exe_suffix):
+ exe = download['url']
+ if download['url'].endswith('.whl'):
+ parts = os.path.basename(download['url']).split('-')
+ tag_impl = parts[-3]
+ tag_abi = parts[-2]
+ tag_arch = parts[-1].split('.')[0]
+ wheels[(tag_impl, tag_abi, tag_arch)] = download['url']
+ if download['url'].endswith('.tar.bz2'):
+ tar_bz2 = download['url']
+ if download['url'].endswith('.tar.gz'):
+ tar_gz = download['url']
+
+ # Find the most-specific wheel possible
+ for tag in valid_tags:
+ if tag in wheels:
+ whl = wheels[tag]
+ break
+
+ if exe_suffix and exe:
+ url = exe
+ elif whl:
+ url = whl
+ elif tar_bz2:
+ url = tar_bz2
+ elif tar_gz:
+ url = tar_gz
+ else:
+ return None
+
+ return url
+
+
+def _stage_requirements(deps_dir, path):
+ """
+ Installs requirements without using Python to download, since
+ different services are limiting to TLS 1.2, and older version of
+ Python do not support that
+
+ :param deps_dir:
+ A unicode path to a temporary diretory to use for downloads
+
+ :param path:
+ A unicode filesystem path to a requirements file
+ """
+
packages = _parse_requires(path)
for p in packages:
+ url = None
pkg = p['pkg']
pkg_sub_dir = None
if p['type'] == 'url':
@@ -359,53 +568,25 @@ def _stage_requirements(deps_dir, path):
if os.path.exists(json_dest):
os.remove(json_dest)
- latest = pkg_info['info']['version']
- if p['type'] == '>=':
- if _tuple_from_ver(p['ver']) > _tuple_from_ver(latest):
- raise Exception('Unable to find version %s of %s, newest is %s' % (p['ver'], pkg, latest))
- version = latest
- elif p['type'] == '==':
+ if p['type'] == '==':
if p['ver'] not in pkg_info['releases']:
raise Exception('Unable to find version %s of %s' % (p['ver'], pkg))
- version = p['ver']
- else:
- version = latest
-
- wheels = {}
- whl = None
- tar_bz2 = None
- tar_gz = None
- exe = None
- for download in pkg_info['releases'][version]:
- if exe_suffix and download['url'].endswith(exe_suffix):
- exe = download['url']
- if download['url'].endswith('.whl'):
- parts = os.path.basename(download['url']).split('-')
- tag_impl = parts[-3]
- tag_abi = parts[-2]
- tag_arch = parts[-1].split('.')[0]
- wheels[(tag_impl, tag_abi, tag_arch)] = download['url']
- if download['url'].endswith('.tar.bz2'):
- tar_bz2 = download['url']
- if download['url'].endswith('.tar.gz'):
- tar_gz = download['url']
-
- # Find the most-specific wheel possible
- for tag in valid_tags:
- if tag in wheels:
- whl = wheels[tag]
- break
-
- if exe_suffix and exe:
- url = exe
- elif whl:
- url = whl
- elif tar_bz2:
- url = tar_bz2
- elif tar_gz:
- url = tar_gz
+ url = _locate_suitable_download(pkg_info['releases'][p['ver']])
+ if not url:
+ raise Exception('Unable to find a compatible download of %s == %s' % (pkg, p['ver']))
else:
- raise Exception('Unable to find suitable download for %s' % pkg)
+ p_ver_tup = _tuple_from_ver(p['ver'])
+ for ver_str, ver_tup in reversed(_sort_pep440_versions(pkg_info['releases'], False)):
+ if p['type'] == '>=' and ver_tup < p_ver_tup:
+ break
+ url = _locate_suitable_download(pkg_info['releases'][ver_str])
+ if url:
+ break
+ if not url:
+ if p['type'] == '>=':
+ raise Exception('Unable to find a compatible download of %s >= %s' % (pkg, p['ver']))
+ else:
+ raise Exception('Unable to find a compatible download of %s' % pkg)
local_path = _download(url, deps_dir)