pypi.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. from distutils.version import LooseVersion
  2. import errno
  3. import hashlib
  4. import logging
  5. import re
  6. import zipfile
  7. import yarg
  8. from requests_download import download, HashTracker
  9. from .util import get_cache_dir
  10. logger = logging.getLogger(__name__)
  11. def find_pypi_release(requirement):
  12. if '==' in requirement:
  13. name, version = requirement.split('==', 1)
  14. return yarg.get(name).release(version)
  15. else:
  16. return yarg.get(requirement).latest_release
  17. class NoWheelError(Exception): pass
  18. class WheelDownloader(object):
  19. def __init__(self, requirement, py_version, bitness):
  20. self.requirement = requirement
  21. self.py_version = py_version
  22. self.bitness = bitness
  23. if requirement.count('==') != 1:
  24. raise ValueError("Requirement {!r} did not match name==version")
  25. self.name, self.version = requirement.split('==', 1)
  26. def score_platform(self, platform):
  27. target = 'win_amd64' if self.bitness == 64 else 'win32'
  28. d = {target: 2, 'any': 1}
  29. return max(d.get(p, 0) for p in platform.split('.'))
  30. def score_abi(self, abi):
  31. py_version_nodot = ''.join(self.py_version.split('.')[:2])
  32. # Are there other valid options here?
  33. d = {'cp%sm' % py_version_nodot: 3, # Is the m reliable?
  34. 'abi3': 2, 'none': 1}
  35. return max(d.get(a, 0) for a in abi.split('.'))
  36. def score_interpreter(self, interpreter):
  37. py_version_nodot = ''.join(self.py_version.split('.')[:2])
  38. py_version_major = self.py_version.split('.')[0]
  39. d = {'cp'+py_version_nodot: 4,
  40. 'cp'+py_version_major: 3,
  41. 'py'+py_version_nodot: 2,
  42. 'py'+py_version_major: 1
  43. }
  44. return max(d.get(i, 0) for i in interpreter.split('.'))
  45. def pick_best_wheel(self, release_list):
  46. best_score = (0, 0, 0)
  47. best = None
  48. for release in release_list:
  49. if release.package_type != 'wheel':
  50. continue
  51. m = re.search(r'-([^-]+)-([^-]+)-([^-]+)\.whl', release.filename)
  52. if not m:
  53. continue
  54. interpreter, abi, platform = m.group(1, 2, 3)
  55. score = (self.score_platform(platform),
  56. self.score_abi(abi),
  57. self.score_interpreter(interpreter)
  58. )
  59. if any(s==0 for s in score):
  60. # Incompatible
  61. continue
  62. if score > best_score:
  63. best = release
  64. best_score = score
  65. return best
  66. def check_cache(self):
  67. dist_dir = get_cache_dir() / 'pypi' / self.name
  68. if not dist_dir.is_dir():
  69. return None
  70. if self.version:
  71. release_dir = dist_dir / self.version
  72. else:
  73. versions = [p.name for p in dist_dir.iterdir()]
  74. if not versions:
  75. return None
  76. latest = max(versions, key=LooseVersion)
  77. release_dir = dist_dir / latest
  78. rel = self.pick_best_wheel(CachedRelease(p.name)
  79. for p in release_dir.iterdir())
  80. if rel is None:
  81. return None
  82. return release_dir / rel.filename
  83. def fetch(self):
  84. p = self.check_cache()
  85. if p is not None:
  86. return p
  87. release_list = yarg.get(self.name).release(self.version)
  88. preferred_release = self.pick_best_wheel(release_list)
  89. if preferred_release is None:
  90. raise NoWheelError('No compatible wheels found for {0.name} {0.version}'.format(self))
  91. download_to = get_cache_dir() / 'pypi' / self.name / self.version
  92. try:
  93. download_to.mkdir(parents=True)
  94. except OSError as e:
  95. # Py2 compatible equivalent of FileExistsError
  96. if e.errno != errno.EEXIST:
  97. raise
  98. target = download_to / preferred_release.filename
  99. from . import __version__
  100. hasher = HashTracker(hashlib.md5())
  101. headers = {'user-agent': 'pynsist/'+__version__}
  102. logger.info('Downloading wheel: %s', preferred_release.url)
  103. download(preferred_release.url, str(target), headers=headers,
  104. trackers=(hasher,))
  105. if hasher.hashobj.hexdigest() != preferred_release.md5_digest:
  106. target.unlink()
  107. raise ValueError('Downloaded wheel corrupted: {}'.format(preferred_release.url))
  108. return target
  109. class CachedRelease(object):
  110. # Mock enough of the yarg Release object to be compatible with
  111. # pick_best_release above
  112. def __init__(self, filename):
  113. self.filename = filename
  114. self.package_type = 'wheel' if filename.endswith('.whl') else ''
  115. def extract_wheel(whl_file, target_dir):
  116. with zipfile.ZipFile(str(whl_file), mode='r') as zf:
  117. names = zf.namelist()
  118. # TODO: Do anything with data and dist-info folders?
  119. pkg_files = [n for n in names \
  120. if not n.split('/')[0].endswith(('.data', '.dist-info'))]
  121. zf.extractall(target_dir, members=pkg_files)
  122. def fetch_pypi_wheels(requirements, target_dir, py_version, bitness):
  123. for req in requirements:
  124. wd = WheelDownloader(req, py_version, bitness)
  125. whl_file = wd.fetch()
  126. extract_wheel(whl_file, target_dir)