__init__.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. """"Vendoring script, python 3.5 needed"""
  2. from pathlib import Path
  3. import os
  4. import re
  5. import shutil
  6. import invoke
  7. TASK_NAME = 'update'
  8. FILE_WHITE_LIST = (
  9. 'Makefile',
  10. 'vendor.txt',
  11. '__init__.py',
  12. 'README.rst',
  13. )
  14. def drop_dir(path, **kwargs):
  15. shutil.rmtree(str(path), **kwargs)
  16. def remove_all(paths):
  17. for path in paths:
  18. if path.is_dir():
  19. drop_dir(path)
  20. else:
  21. path.unlink()
  22. def log(msg):
  23. print('[vendoring.%s] %s' % (TASK_NAME, msg))
  24. def _get_vendor_dir(ctx):
  25. git_root = ctx.run('git rev-parse --show-toplevel', hide=True).stdout
  26. return Path(git_root.strip()) / 'src' / 'pip' / '_vendor'
  27. def clean_vendor(ctx, vendor_dir):
  28. # Old _vendor cleanup
  29. remove_all(vendor_dir.glob('*.pyc'))
  30. log('Cleaning %s' % vendor_dir)
  31. for item in vendor_dir.iterdir():
  32. if item.is_dir():
  33. shutil.rmtree(str(item))
  34. elif item.name not in FILE_WHITE_LIST:
  35. item.unlink()
  36. else:
  37. log('Skipping %s' % item)
  38. def detect_vendored_libs(vendor_dir):
  39. retval = []
  40. for item in vendor_dir.iterdir():
  41. if item.is_dir():
  42. retval.append(item.name)
  43. elif item.name.endswith(".pyi"):
  44. continue
  45. elif item.name not in FILE_WHITE_LIST:
  46. retval.append(item.name[:-3])
  47. return retval
  48. def rewrite_imports(package_dir, vendored_libs):
  49. for item in package_dir.iterdir():
  50. if item.is_dir():
  51. rewrite_imports(item, vendored_libs)
  52. elif item.name.endswith('.py'):
  53. rewrite_file_imports(item, vendored_libs)
  54. def rewrite_file_imports(item, vendored_libs):
  55. """Rewrite 'import xxx' and 'from xxx import' for vendored_libs"""
  56. text = item.read_text(encoding='utf-8')
  57. # Revendor pkg_resources.extern first
  58. text = re.sub(r'pkg_resources.extern', r'pip._vendor', text)
  59. for lib in vendored_libs:
  60. text = re.sub(
  61. r'(\n\s*|^)import %s(\n\s*)' % lib,
  62. r'\1from pip._vendor import %s\2' % lib,
  63. text,
  64. )
  65. text = re.sub(
  66. r'(\n\s*|^)from %s(\.|\s+)' % lib,
  67. r'\1from pip._vendor.%s\2' % lib,
  68. text,
  69. )
  70. item.write_text(text, encoding='utf-8')
  71. def apply_patch(ctx, patch_file_path):
  72. log('Applying patch %s' % patch_file_path.name)
  73. ctx.run('git apply --verbose %s' % patch_file_path)
  74. def vendor(ctx, vendor_dir):
  75. log('Reinstalling vendored libraries')
  76. # We use --no-deps because we want to ensure that all of our dependencies
  77. # are added to vendor.txt, this includes all dependencies recursively up
  78. # the chain.
  79. ctx.run(
  80. 'pip install -t {0} -r {0}/vendor.txt --no-compile --no-deps'.format(
  81. str(vendor_dir),
  82. )
  83. )
  84. remove_all(vendor_dir.glob('*.dist-info'))
  85. remove_all(vendor_dir.glob('*.egg-info'))
  86. # Cleanup setuptools unneeded parts
  87. (vendor_dir / 'easy_install.py').unlink()
  88. drop_dir(vendor_dir / 'setuptools')
  89. drop_dir(vendor_dir / 'pkg_resources' / '_vendor')
  90. drop_dir(vendor_dir / 'pkg_resources' / 'extern')
  91. # Drop the bin directory (contains easy_install, distro, chardetect etc.)
  92. # Might not appear on all OSes, so ignoring errors
  93. drop_dir(vendor_dir / 'bin', ignore_errors=True)
  94. # Drop interpreter and OS specific msgpack libs.
  95. # Pip will rely on the python-only fallback instead.
  96. remove_all(vendor_dir.glob('msgpack/*.so'))
  97. # Detect the vendored packages/modules
  98. vendored_libs = detect_vendored_libs(vendor_dir)
  99. log("Detected vendored libraries: %s" % ", ".join(vendored_libs))
  100. # Global import rewrites
  101. log("Rewriting all imports related to vendored libs")
  102. for item in vendor_dir.iterdir():
  103. if item.is_dir():
  104. rewrite_imports(item, vendored_libs)
  105. elif item.name not in FILE_WHITE_LIST:
  106. rewrite_file_imports(item, vendored_libs)
  107. # Special cases: apply stored patches
  108. log("Apply patches")
  109. patch_dir = Path(__file__).parent / 'patches'
  110. for patch in patch_dir.glob('*.patch'):
  111. apply_patch(ctx, patch)
  112. @invoke.task
  113. def update_stubs(ctx):
  114. vendor_dir = _get_vendor_dir(ctx)
  115. vendored_libs = detect_vendored_libs(vendor_dir)
  116. print("[vendoring.update_stubs] Add mypy stubs")
  117. extra_stubs_needed = {
  118. # Some projects need stubs other than a simple <name>.pyi
  119. "six": [
  120. "six.__init__",
  121. "six.moves.__init__",
  122. "six.moves.configparser",
  123. ],
  124. # Some projects should not have stubs coz they're single file modules
  125. "appdirs": [],
  126. }
  127. for lib in vendored_libs:
  128. if lib not in extra_stubs_needed:
  129. (vendor_dir / (lib + ".pyi")).write_text("from %s import *" % lib)
  130. continue
  131. for selector in extra_stubs_needed[lib]:
  132. fname = selector.replace(".", os.sep) + ".pyi"
  133. if selector.endswith(".__init__"):
  134. selector = selector[:-9]
  135. f_path = vendor_dir / fname
  136. if not f_path.parent.exists():
  137. f_path.parent.mkdir()
  138. f_path.write_text("from %s import *" % selector)
  139. @invoke.task(name=TASK_NAME, post=[update_stubs])
  140. def main(ctx):
  141. vendor_dir = _get_vendor_dir(ctx)
  142. log('Using vendor dir: %s' % vendor_dir)
  143. clean_vendor(ctx, vendor_dir)
  144. vendor(ctx, vendor_dir)
  145. log('Revendoring complete')