pyproject.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. from __future__ import annotations
  2. import os
  3. from collections import namedtuple
  4. from typing import Any
  5. from pip._vendor.packaging.requirements import InvalidRequirement
  6. from pip._internal.exceptions import (
  7. InstallationError,
  8. InvalidPyProjectBuildRequires,
  9. MissingPyProjectBuildRequires,
  10. )
  11. from pip._internal.utils.compat import tomllib
  12. from pip._internal.utils.packaging import get_requirement
  13. def _is_list_of_str(obj: Any) -> bool:
  14. return isinstance(obj, list) and all(isinstance(item, str) for item in obj)
  15. def make_pyproject_path(unpacked_source_directory: str) -> str:
  16. return os.path.join(unpacked_source_directory, "pyproject.toml")
  17. BuildSystemDetails = namedtuple(
  18. "BuildSystemDetails", ["requires", "backend", "check", "backend_path"]
  19. )
  20. def load_pyproject_toml(
  21. pyproject_toml: str, setup_py: str, req_name: str
  22. ) -> BuildSystemDetails:
  23. """Load the pyproject.toml file.
  24. Parameters:
  25. pyproject_toml - Location of the project's pyproject.toml file
  26. setup_py - Location of the project's setup.py file
  27. req_name - The name of the requirement we're processing (for
  28. error reporting)
  29. Returns:
  30. None if we should use the legacy code path, otherwise a tuple
  31. (
  32. requirements from pyproject.toml,
  33. name of PEP 517 backend,
  34. requirements we should check are installed after setting
  35. up the build environment
  36. directory paths to import the backend from (backend-path),
  37. relative to the project root.
  38. )
  39. """
  40. has_pyproject = os.path.isfile(pyproject_toml)
  41. has_setup = os.path.isfile(setup_py)
  42. if not has_pyproject and not has_setup:
  43. raise InstallationError(
  44. f"{req_name} does not appear to be a Python project: "
  45. f"neither 'setup.py' nor 'pyproject.toml' found."
  46. )
  47. if has_pyproject:
  48. with open(pyproject_toml, encoding="utf-8") as f:
  49. pp_toml = tomllib.loads(f.read())
  50. build_system = pp_toml.get("build-system")
  51. else:
  52. build_system = None
  53. if build_system is None:
  54. # In the absence of any explicit backend specification, we
  55. # assume the setuptools backend that most closely emulates the
  56. # traditional direct setup.py execution, and require wheel and
  57. # a version of setuptools that supports that backend.
  58. build_system = {
  59. "requires": ["setuptools>=40.8.0"],
  60. "build-backend": "setuptools.build_meta:__legacy__",
  61. }
  62. # Ensure that the build-system section in pyproject.toml conforms
  63. # to PEP 518.
  64. # Specifying the build-system table but not the requires key is invalid
  65. if "requires" not in build_system:
  66. raise MissingPyProjectBuildRequires(package=req_name)
  67. # Error out if requires is not a list of strings
  68. requires = build_system["requires"]
  69. if not _is_list_of_str(requires):
  70. raise InvalidPyProjectBuildRequires(
  71. package=req_name,
  72. reason="It is not a list of strings.",
  73. )
  74. # Each requirement must be valid as per PEP 508
  75. for requirement in requires:
  76. try:
  77. get_requirement(requirement)
  78. except InvalidRequirement as error:
  79. raise InvalidPyProjectBuildRequires(
  80. package=req_name,
  81. reason=f"It contains an invalid requirement: {requirement!r}",
  82. ) from error
  83. backend = build_system.get("build-backend")
  84. backend_path = build_system.get("backend-path", [])
  85. check: list[str] = []
  86. if backend is None:
  87. # If the user didn't specify a backend, we assume they want to use
  88. # the setuptools backend. But we can't be sure they have included
  89. # a version of setuptools which supplies the backend. So we
  90. # make a note to check that this requirement is present once
  91. # we have set up the environment.
  92. # This is quite a lot of work to check for a very specific case. But
  93. # the problem is, that case is potentially quite common - projects that
  94. # adopted PEP 518 early for the ability to specify requirements to
  95. # execute setup.py, but never considered needing to mention the build
  96. # tools themselves. The original PEP 518 code had a similar check (but
  97. # implemented in a different way).
  98. backend = "setuptools.build_meta:__legacy__"
  99. check = ["setuptools>=40.8.0"]
  100. return BuildSystemDetails(requires, backend, check, backend_path)