pyproject.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. from __future__ import absolute_import
  2. import io
  3. import os
  4. import sys
  5. from collections import namedtuple
  6. from pip._vendor import six, toml
  7. from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
  8. from pip._internal.exceptions import InstallationError
  9. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  10. if MYPY_CHECK_RUNNING:
  11. from typing import Any, Optional, List
  12. def _is_list_of_str(obj):
  13. # type: (Any) -> bool
  14. return (
  15. isinstance(obj, list) and
  16. all(isinstance(item, six.string_types) for item in obj)
  17. )
  18. def make_pyproject_path(unpacked_source_directory):
  19. # type: (str) -> str
  20. path = os.path.join(unpacked_source_directory, 'pyproject.toml')
  21. # Python2 __file__ should not be unicode
  22. if six.PY2 and isinstance(path, six.text_type):
  23. path = path.encode(sys.getfilesystemencoding())
  24. return path
  25. BuildSystemDetails = namedtuple('BuildSystemDetails', [
  26. 'requires', 'backend', 'check', 'backend_path'
  27. ])
  28. def load_pyproject_toml(
  29. use_pep517, # type: Optional[bool]
  30. pyproject_toml, # type: str
  31. setup_py, # type: str
  32. req_name # type: str
  33. ):
  34. # type: (...) -> Optional[BuildSystemDetails]
  35. """Load the pyproject.toml file.
  36. Parameters:
  37. use_pep517 - Has the user requested PEP 517 processing? None
  38. means the user hasn't explicitly specified.
  39. pyproject_toml - Location of the project's pyproject.toml file
  40. setup_py - Location of the project's setup.py file
  41. req_name - The name of the requirement we're processing (for
  42. error reporting)
  43. Returns:
  44. None if we should use the legacy code path, otherwise a tuple
  45. (
  46. requirements from pyproject.toml,
  47. name of PEP 517 backend,
  48. requirements we should check are installed after setting
  49. up the build environment
  50. directory paths to import the backend from (backend-path),
  51. relative to the project root.
  52. )
  53. """
  54. has_pyproject = os.path.isfile(pyproject_toml)
  55. has_setup = os.path.isfile(setup_py)
  56. if has_pyproject:
  57. with io.open(pyproject_toml, encoding="utf-8") as f:
  58. pp_toml = toml.load(f)
  59. build_system = pp_toml.get("build-system")
  60. else:
  61. build_system = None
  62. # The following cases must use PEP 517
  63. # We check for use_pep517 being non-None and falsey because that means
  64. # the user explicitly requested --no-use-pep517. The value 0 as
  65. # opposed to False can occur when the value is provided via an
  66. # environment variable or config file option (due to the quirk of
  67. # strtobool() returning an integer in pip's configuration code).
  68. if has_pyproject and not has_setup:
  69. if use_pep517 is not None and not use_pep517:
  70. raise InstallationError(
  71. "Disabling PEP 517 processing is invalid: "
  72. "project does not have a setup.py"
  73. )
  74. use_pep517 = True
  75. elif build_system and "build-backend" in build_system:
  76. if use_pep517 is not None and not use_pep517:
  77. raise InstallationError(
  78. "Disabling PEP 517 processing is invalid: "
  79. "project specifies a build backend of {} "
  80. "in pyproject.toml".format(
  81. build_system["build-backend"]
  82. )
  83. )
  84. use_pep517 = True
  85. # If we haven't worked out whether to use PEP 517 yet,
  86. # and the user hasn't explicitly stated a preference,
  87. # we do so if the project has a pyproject.toml file.
  88. elif use_pep517 is None:
  89. use_pep517 = has_pyproject
  90. # At this point, we know whether we're going to use PEP 517.
  91. assert use_pep517 is not None
  92. # If we're using the legacy code path, there is nothing further
  93. # for us to do here.
  94. if not use_pep517:
  95. return None
  96. if build_system is None:
  97. # Either the user has a pyproject.toml with no build-system
  98. # section, or the user has no pyproject.toml, but has opted in
  99. # explicitly via --use-pep517.
  100. # In the absence of any explicit backend specification, we
  101. # assume the setuptools backend that most closely emulates the
  102. # traditional direct setup.py execution, and require wheel and
  103. # a version of setuptools that supports that backend.
  104. build_system = {
  105. "requires": ["setuptools>=40.8.0", "wheel"],
  106. "build-backend": "setuptools.build_meta:__legacy__",
  107. }
  108. # If we're using PEP 517, we have build system information (either
  109. # from pyproject.toml, or defaulted by the code above).
  110. # Note that at this point, we do not know if the user has actually
  111. # specified a backend, though.
  112. assert build_system is not None
  113. # Ensure that the build-system section in pyproject.toml conforms
  114. # to PEP 518.
  115. error_template = (
  116. "{package} has a pyproject.toml file that does not comply "
  117. "with PEP 518: {reason}"
  118. )
  119. # Specifying the build-system table but not the requires key is invalid
  120. if "requires" not in build_system:
  121. raise InstallationError(
  122. error_template.format(package=req_name, reason=(
  123. "it has a 'build-system' table but not "
  124. "'build-system.requires' which is mandatory in the table"
  125. ))
  126. )
  127. # Error out if requires is not a list of strings
  128. requires = build_system["requires"]
  129. if not _is_list_of_str(requires):
  130. raise InstallationError(error_template.format(
  131. package=req_name,
  132. reason="'build-system.requires' is not a list of strings.",
  133. ))
  134. # Each requirement must be valid as per PEP 508
  135. for requirement in requires:
  136. try:
  137. Requirement(requirement)
  138. except InvalidRequirement:
  139. raise InstallationError(
  140. error_template.format(
  141. package=req_name,
  142. reason=(
  143. "'build-system.requires' contains an invalid "
  144. "requirement: {!r}".format(requirement)
  145. ),
  146. )
  147. )
  148. backend = build_system.get("build-backend")
  149. backend_path = build_system.get("backend-path", [])
  150. check = [] # type: List[str]
  151. if backend is None:
  152. # If the user didn't specify a backend, we assume they want to use
  153. # the setuptools backend. But we can't be sure they have included
  154. # a version of setuptools which supplies the backend, or wheel
  155. # (which is needed by the backend) in their requirements. So we
  156. # make a note to check that those requirements are present once
  157. # we have set up the environment.
  158. # This is quite a lot of work to check for a very specific case. But
  159. # the problem is, that case is potentially quite common - projects that
  160. # adopted PEP 518 early for the ability to specify requirements to
  161. # execute setup.py, but never considered needing to mention the build
  162. # tools themselves. The original PEP 518 code had a similar check (but
  163. # implemented in a different way).
  164. backend = "setuptools.build_meta:__legacy__"
  165. check = ["setuptools>=40.8.0", "wheel"]
  166. return BuildSystemDetails(requires, backend, check, backend_path)