pyproject.py 7.1 KB

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