You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

wrappers.py 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. from contextlib import contextmanager
  2. import os
  3. from os.path import dirname, abspath, join as pjoin
  4. import shutil
  5. from subprocess import check_call
  6. import sys
  7. from tempfile import mkdtemp
  8. from . import compat
  9. _in_proc_script = pjoin(dirname(abspath(__file__)), '_in_process.py')
  10. @contextmanager
  11. def tempdir():
  12. td = mkdtemp()
  13. try:
  14. yield td
  15. finally:
  16. shutil.rmtree(td)
  17. class BackendUnavailable(Exception):
  18. """Will be raised if the backend cannot be imported in the hook process."""
  19. class UnsupportedOperation(Exception):
  20. """May be raised by build_sdist if the backend indicates that it can't."""
  21. def default_subprocess_runner(cmd, cwd=None, extra_environ=None):
  22. """The default method of calling the wrapper subprocess."""
  23. env = os.environ.copy()
  24. if extra_environ:
  25. env.update(extra_environ)
  26. check_call(cmd, cwd=cwd, env=env)
  27. class Pep517HookCaller(object):
  28. """A wrapper around a source directory to be built with a PEP 517 backend.
  29. source_dir : The path to the source directory, containing pyproject.toml.
  30. backend : The build backend spec, as per PEP 517, from pyproject.toml.
  31. """
  32. def __init__(self, source_dir, build_backend):
  33. self.source_dir = abspath(source_dir)
  34. self.build_backend = build_backend
  35. self._subprocess_runner = default_subprocess_runner
  36. # TODO: Is this over-engineered? Maybe frontends only need to
  37. # set this when creating the wrapper, not on every call.
  38. @contextmanager
  39. def subprocess_runner(self, runner):
  40. prev = self._subprocess_runner
  41. self._subprocess_runner = runner
  42. yield
  43. self._subprocess_runner = prev
  44. def get_requires_for_build_wheel(self, config_settings=None):
  45. """Identify packages required for building a wheel
  46. Returns a list of dependency specifications, e.g.:
  47. ["wheel >= 0.25", "setuptools"]
  48. This does not include requirements specified in pyproject.toml.
  49. It returns the result of calling the equivalently named hook in a
  50. subprocess.
  51. """
  52. return self._call_hook('get_requires_for_build_wheel', {
  53. 'config_settings': config_settings
  54. })
  55. def prepare_metadata_for_build_wheel(
  56. self, metadata_directory, config_settings=None):
  57. """Prepare a *.dist-info folder with metadata for this project.
  58. Returns the name of the newly created folder.
  59. If the build backend defines a hook with this name, it will be called
  60. in a subprocess. If not, the backend will be asked to build a wheel,
  61. and the dist-info extracted from that.
  62. """
  63. return self._call_hook('prepare_metadata_for_build_wheel', {
  64. 'metadata_directory': abspath(metadata_directory),
  65. 'config_settings': config_settings,
  66. })
  67. def build_wheel(
  68. self, wheel_directory, config_settings=None,
  69. metadata_directory=None):
  70. """Build a wheel from this project.
  71. Returns the name of the newly created file.
  72. In general, this will call the 'build_wheel' hook in the backend.
  73. However, if that was previously called by
  74. 'prepare_metadata_for_build_wheel', and the same metadata_directory is
  75. used, the previously built wheel will be copied to wheel_directory.
  76. """
  77. if metadata_directory is not None:
  78. metadata_directory = abspath(metadata_directory)
  79. return self._call_hook('build_wheel', {
  80. 'wheel_directory': abspath(wheel_directory),
  81. 'config_settings': config_settings,
  82. 'metadata_directory': metadata_directory,
  83. })
  84. def get_requires_for_build_sdist(self, config_settings=None):
  85. """Identify packages required for building a wheel
  86. Returns a list of dependency specifications, e.g.:
  87. ["setuptools >= 26"]
  88. This does not include requirements specified in pyproject.toml.
  89. It returns the result of calling the equivalently named hook in a
  90. subprocess.
  91. """
  92. return self._call_hook('get_requires_for_build_sdist', {
  93. 'config_settings': config_settings
  94. })
  95. def build_sdist(self, sdist_directory, config_settings=None):
  96. """Build an sdist from this project.
  97. Returns the name of the newly created file.
  98. This calls the 'build_sdist' backend hook in a subprocess.
  99. """
  100. return self._call_hook('build_sdist', {
  101. 'sdist_directory': abspath(sdist_directory),
  102. 'config_settings': config_settings,
  103. })
  104. def _call_hook(self, hook_name, kwargs):
  105. # On Python 2, pytoml returns Unicode values (which is correct) but the
  106. # environment passed to check_call needs to contain string values. We
  107. # convert here by encoding using ASCII (the backend can only contain
  108. # letters, digits and _, . and : characters, and will be used as a
  109. # Python identifier, so non-ASCII content is wrong on Python 2 in
  110. # any case).
  111. if sys.version_info[0] == 2:
  112. build_backend = self.build_backend.encode('ASCII')
  113. else:
  114. build_backend = self.build_backend
  115. with tempdir() as td:
  116. compat.write_json({'kwargs': kwargs}, pjoin(td, 'input.json'),
  117. indent=2)
  118. # Run the hook in a subprocess
  119. self._subprocess_runner(
  120. [sys.executable, _in_proc_script, hook_name, td],
  121. cwd=self.source_dir,
  122. extra_environ={'PEP517_BUILD_BACKEND': build_backend}
  123. )
  124. data = compat.read_json(pjoin(td, 'output.json'))
  125. if data.get('unsupported'):
  126. raise UnsupportedOperation
  127. if data.get('no_backend'):
  128. raise BackendUnavailable
  129. return data['return_val']