feat: support pyproject.toml in compile_pip_requirements (#1519)

With this PR we can also use `pyproject.toml` in addition
to `requirements.in` which helps in making the requirements
in a more structured form. For example, we could parse the
toml itself and create aliases in the hub repos only for the
packages outlined in the `pyproject.toml` file. The same for
`gazelle`, we could restrict `gazelle_python.yaml` contents
to only the dependencies listed in `pyproject.toml`.

Examples can be migrated once we agree on the interface.

Summary:
- feat: support pyproject.toml in compile_pip_requirements
- chore: use pyproject.toml for sphinx doc requirements

---------

Co-authored-by: Richard Levasseur <rlevasseur@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 185ac37..ebbde9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,10 @@
   default, which will cause `gazelle` to change third-party dependency labels
   from `@pip_foo//:pkg` to `@pip//foo` by default.
 
+* The `compile_pip_requirements` now defaults to `pyproject.toml` if the `src`
+  or `requirements_in` attributes are unspecified, matching the upstream
+  `pip-compile` behaviour more closely.
+
 Breaking changes:
 
 * (pip) `pip_install` repository rule in this release has been disabled and
@@ -76,6 +80,9 @@
 * (bzlmod) Added `.whl` patching support via `patches` and `patch_strip`
   arguments to the new `pip.override` tag class.
 
+* (pip) Support for using [PEP621](https://peps.python.org/pep-0621/) compliant
+  `pyproject.toml` for creating a resolved `requirements.txt` file.
+
 ## [0.26.0] - 2023-10-06
 
 ### Changed
diff --git a/docs/sphinx/BUILD.bazel b/docs/sphinx/BUILD.bazel
index 1990269..7c99f77 100644
--- a/docs/sphinx/BUILD.bazel
+++ b/docs/sphinx/BUILD.bazel
@@ -102,8 +102,8 @@
 # Run bazel run //docs/sphinx:requirements.update
 compile_pip_requirements(
     name = "requirements",
+    src = "pyproject.toml",
     requirements_darwin = "requirements_darwin.txt",
-    requirements_in = "requirements.in",
     requirements_txt = "requirements_linux.txt",
     target_compatible_with = _TARGET_COMPATIBLE_WITH,
 )
diff --git a/docs/sphinx/pyproject.toml b/docs/sphinx/pyproject.toml
new file mode 100644
index 0000000..02e0f36
--- /dev/null
+++ b/docs/sphinx/pyproject.toml
@@ -0,0 +1,12 @@
+[project]
+name = "rules_python_docs"
+version = "0.0.0"
+
+dependencies = [
+    # NOTE: This is only used as input to create the resolved requirements.txt
+    # file, which is what builds, both Bazel and Readthedocs, both use.
+    "sphinx",
+    "myst-parser",
+    "sphinx_rtd_theme",
+    "readthedocs-sphinx-ext",
+]
diff --git a/docs/sphinx/requirements.in b/docs/sphinx/requirements.in
deleted file mode 100644
index c403778..0000000
--- a/docs/sphinx/requirements.in
+++ /dev/null
@@ -1,6 +0,0 @@
-# NOTE: This is only used as input to create the resolved requirements.txt file,
-# which is what builds, both Bazel and Readthedocs, both use.
-sphinx
-myst-parser
-sphinx_rtd_theme
-readthedocs-sphinx-ext
diff --git a/docs/sphinx/requirements_darwin.txt b/docs/sphinx/requirements_darwin.txt
index 1f47b83..5e3fd19 100644
--- a/docs/sphinx/requirements_darwin.txt
+++ b/docs/sphinx/requirements_darwin.txt
@@ -184,7 +184,7 @@
 myst-parser==1.0.0 \
     --hash=sha256:502845659313099542bd38a2ae62f01360e7dd4b1310f025dd014dfc0439cdae \
     --hash=sha256:69fb40a586c6fa68995e6521ac0a525793935db7e724ca9bac1d33be51be9a4c
-    # via -r docs/sphinx/requirements.in
+    # via rules-python-docs (docs/sphinx/pyproject.toml)
 packaging==23.0 \
     --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
     --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
@@ -240,7 +240,7 @@
 readthedocs-sphinx-ext==2.2.3 \
     --hash=sha256:6583c26791a5853ee9e57ce9db864e2fb06808ba470f805d74d53fc50811e012 \
     --hash=sha256:e9d911792789b88ae12e2be94d88c619f89a4fa1fe9e42c1505c9930a07163d8
-    # via -r docs/sphinx/requirements.in
+    # via rules-python-docs (docs/sphinx/pyproject.toml)
 requests==2.31.0 \
     --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
     --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
@@ -255,14 +255,14 @@
     --hash=sha256:0dac3b698538ffef41716cf97ba26c1c7788dba73ce6f150c1ff5b4720786dd2 \
     --hash=sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc
     # via
-    #   -r docs/sphinx/requirements.in
     #   myst-parser
+    #   rules-python-docs (docs/sphinx/pyproject.toml)
     #   sphinx-rtd-theme
     #   sphinxcontrib-jquery
 sphinx-rtd-theme==1.2.0 \
     --hash=sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8 \
     --hash=sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2
-    # via -r docs/sphinx/requirements.in
+    # via rules-python-docs (docs/sphinx/pyproject.toml)
 sphinxcontrib-applehelp==1.0.4 \
     --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
     --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
diff --git a/docs/sphinx/requirements_linux.txt b/docs/sphinx/requirements_linux.txt
index 1f47b83..5e3fd19 100644
--- a/docs/sphinx/requirements_linux.txt
+++ b/docs/sphinx/requirements_linux.txt
@@ -184,7 +184,7 @@
 myst-parser==1.0.0 \
     --hash=sha256:502845659313099542bd38a2ae62f01360e7dd4b1310f025dd014dfc0439cdae \
     --hash=sha256:69fb40a586c6fa68995e6521ac0a525793935db7e724ca9bac1d33be51be9a4c
-    # via -r docs/sphinx/requirements.in
+    # via rules-python-docs (docs/sphinx/pyproject.toml)
 packaging==23.0 \
     --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
     --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
@@ -240,7 +240,7 @@
 readthedocs-sphinx-ext==2.2.3 \
     --hash=sha256:6583c26791a5853ee9e57ce9db864e2fb06808ba470f805d74d53fc50811e012 \
     --hash=sha256:e9d911792789b88ae12e2be94d88c619f89a4fa1fe9e42c1505c9930a07163d8
-    # via -r docs/sphinx/requirements.in
+    # via rules-python-docs (docs/sphinx/pyproject.toml)
 requests==2.31.0 \
     --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
     --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
@@ -255,14 +255,14 @@
     --hash=sha256:0dac3b698538ffef41716cf97ba26c1c7788dba73ce6f150c1ff5b4720786dd2 \
     --hash=sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc
     # via
-    #   -r docs/sphinx/requirements.in
     #   myst-parser
+    #   rules-python-docs (docs/sphinx/pyproject.toml)
     #   sphinx-rtd-theme
     #   sphinxcontrib-jquery
 sphinx-rtd-theme==1.2.0 \
     --hash=sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8 \
     --hash=sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2
-    # via -r docs/sphinx/requirements.in
+    # via rules-python-docs (docs/sphinx/pyproject.toml)
 sphinxcontrib-applehelp==1.0.4 \
     --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
     --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
diff --git a/examples/build_file_generation/BUILD.bazel b/examples/build_file_generation/BUILD.bazel
index 5b01215..7b9766e 100644
--- a/examples/build_file_generation/BUILD.bazel
+++ b/examples/build_file_generation/BUILD.bazel
@@ -11,7 +11,7 @@
 
 compile_pip_requirements(
     name = "requirements",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock.txt",
     requirements_windows = "requirements_windows.txt",
 )
diff --git a/examples/bzlmod/BUILD.bazel b/examples/bzlmod/BUILD.bazel
index ff14016..5e2509a 100644
--- a/examples/bzlmod/BUILD.bazel
+++ b/examples/bzlmod/BUILD.bazel
@@ -16,7 +16,7 @@
 # with pip-compile.
 compile_pip_requirements_3_9(
     name = "requirements_3_9",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock_3_9.txt",
     requirements_windows = "requirements_windows_3_9.txt",
 )
@@ -25,7 +25,7 @@
 # with pip-compile.
 compile_pip_requirements_3_10(
     name = "requirements_3_10",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock_3_10.txt",
     requirements_windows = "requirements_windows_3_10.txt",
 )
diff --git a/examples/bzlmod/other_module/BUILD.bazel b/examples/bzlmod/other_module/BUILD.bazel
index d50a3a0..a93b92a 100644
--- a/examples/bzlmod/other_module/BUILD.bazel
+++ b/examples/bzlmod/other_module/BUILD.bazel
@@ -4,6 +4,6 @@
 # override in the MODULE.bazel.
 compile_pip_requirements_311(
     name = "requirements",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock_3_11.txt",
 )
diff --git a/examples/bzlmod_build_file_generation/BUILD.bazel b/examples/bzlmod_build_file_generation/BUILD.bazel
index 1205817..bca3b36 100644
--- a/examples/bzlmod_build_file_generation/BUILD.bazel
+++ b/examples/bzlmod_build_file_generation/BUILD.bazel
@@ -16,7 +16,7 @@
 # with pip-compile.
 compile_pip_requirements(
     name = "requirements",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock.txt",
     requirements_windows = "requirements_windows.txt",
 )
diff --git a/examples/multi_python_versions/requirements/BUILD.bazel b/examples/multi_python_versions/requirements/BUILD.bazel
index e3e821a..f67333a 100644
--- a/examples/multi_python_versions/requirements/BUILD.bazel
+++ b/examples/multi_python_versions/requirements/BUILD.bazel
@@ -5,24 +5,24 @@
 
 compile_pip_requirements_3_8(
     name = "requirements_3_8",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock_3_8.txt",
 )
 
 compile_pip_requirements_3_9(
     name = "requirements_3_9",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock_3_9.txt",
 )
 
 compile_pip_requirements_3_10(
     name = "requirements_3_10",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock_3_10.txt",
 )
 
 compile_pip_requirements_3_11(
     name = "requirements_3_11",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock_3_11.txt",
 )
diff --git a/examples/pip_parse/BUILD.bazel b/examples/pip_parse/BUILD.bazel
index cf5d0f6..c2cc9a3 100644
--- a/examples/pip_parse/BUILD.bazel
+++ b/examples/pip_parse/BUILD.bazel
@@ -53,7 +53,7 @@
 # This rule adds a convenient way to update the requirements file.
 compile_pip_requirements(
     name = "requirements",
-    requirements_in = "requirements.in",
+    src = "requirements.in",
     requirements_txt = "requirements_lock.txt",
 )
 
diff --git a/examples/pip_parse_vendored/BUILD.bazel b/examples/pip_parse_vendored/BUILD.bazel
index b87b2aa..8741c5a 100644
--- a/examples/pip_parse_vendored/BUILD.bazel
+++ b/examples/pip_parse_vendored/BUILD.bazel
@@ -4,7 +4,10 @@
 
 # This rule adds a convenient way to update the requirements.txt
 # lockfile based on the requirements.in.
-compile_pip_requirements(name = "requirements")
+compile_pip_requirements(
+    name = "requirements",
+    src = "requirements.in",
+)
 
 # The requirements.bzl file is generated with a reference to the interpreter for the host platform.
 # In order to check in a platform-agnostic file, we have to replace that reference with the symbol
diff --git a/examples/pip_repository_annotations/BUILD.bazel b/examples/pip_repository_annotations/BUILD.bazel
index 5b924e1..bdf9df1 100644
--- a/examples/pip_repository_annotations/BUILD.bazel
+++ b/examples/pip_repository_annotations/BUILD.bazel
@@ -9,6 +9,7 @@
 # This rule adds a convenient way to update the requirements file.
 compile_pip_requirements(
     name = "requirements",
+    src = "requirements.in",
 )
 
 py_test(
diff --git a/python/pip_install/requirements.bzl b/python/pip_install/requirements.bzl
index 3935add..5caf762 100644
--- a/python/pip_install/requirements.bzl
+++ b/python/pip_install/requirements.bzl
@@ -19,6 +19,7 @@
 
 def compile_pip_requirements(
         name,
+        src = None,
         extra_args = [],
         extra_deps = [],
         generate_hashes = True,
@@ -48,12 +49,17 @@
 
     Args:
         name: base name for generated targets, typically "requirements".
+        src: file containing inputs to dependency resolution. If not specified,
+            defaults to `pyproject.toml`. Supported formats are:
+            * a requirements text file, usually named `requirements.in`
+            * A `.toml` file, where the `project.dependencies` list is used as per
+              [PEP621](https://peps.python.org/pep-0621/).
         extra_args: passed to pip-compile.
         extra_deps: extra dependencies passed to pip-compile.
         generate_hashes: whether to put hashes in the requirements_txt file.
         py_binary: the py_binary rule to be used.
         py_test: the py_test rule to be used.
-        requirements_in: file expressing desired dependencies.
+        requirements_in: file expressing desired dependencies. Deprecated, use src instead.
         requirements_txt: result of "compiling" the requirements.in file.
         requirements_linux: File of linux specific resolve output to check validate if requirement.in has changes.
         requirements_darwin: File of darwin specific resolve output to check validate if requirement.in has changes.
@@ -62,7 +68,11 @@
         visibility: passed to both the _test and .update rules.
         **kwargs: other bazel attributes passed to the "_test" rule.
     """
-    requirements_in = name + ".in" if requirements_in == None else requirements_in
+    if requirements_in and src:
+        fail("Only one of 'src' and 'requirements_in' attributes can be used")
+    else:
+        src = requirements_in or src or "pyproject.toml"
+
     requirements_txt = name + ".txt" if requirements_txt == None else requirements_txt
 
     # "Default" target produced by this macro
@@ -74,7 +84,7 @@
         visibility = visibility,
     )
 
-    data = [name, requirements_in, requirements_txt] + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None]
+    data = [name, requirements_txt, src] + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None]
 
     # Use the Label constructor so this is expanded in the context of the file
     # where it appears, which is to say, in @rules_python
@@ -83,7 +93,7 @@
     loc = "$(rlocationpath {})"
 
     args = [
-        loc.format(requirements_in),
+        loc.format(src),
         loc.format(requirements_txt),
         "//%s:%s.update" % (native.package_name(), name),
         "--resolver=backtracking",
@@ -105,6 +115,7 @@
         requirement("colorama"),
         requirement("importlib_metadata"),
         requirement("more_itertools"),
+        requirement("packaging"),
         requirement("pep517"),
         requirement("pip"),
         requirement("pip_tools"),
diff --git a/tests/compile_pip_requirements/BUILD.bazel b/tests/compile_pip_requirements/BUILD.bazel
index cadb59a..6df46b8 100644
--- a/tests/compile_pip_requirements/BUILD.bazel
+++ b/tests/compile_pip_requirements/BUILD.bazel
@@ -21,22 +21,22 @@
 
 compile_pip_requirements(
     name = "requirements",
+    src = "requirements.txt",
     data = [
         "requirements.in",
         "requirements_extra.in",
     ],
-    requirements_in = "requirements.txt",
     requirements_txt = "requirements_lock.txt",
 )
 
 compile_pip_requirements(
     name = "requirements_nohashes",
+    src = "requirements.txt",
     data = [
         "requirements.in",
         "requirements_extra.in",
     ],
     generate_hashes = False,
-    requirements_in = "requirements.txt",
     requirements_txt = "requirements_nohashes_lock.txt",
 )
 
@@ -55,12 +55,12 @@
 
 compile_pip_requirements(
     name = "os_specific_requirements",
+    src = "requirements_os_specific.in",
     data = [
         "requirements_extra.in",
         "requirements_os_specific.in",
     ],
     requirements_darwin = "requirements_lock_darwin.txt",
-    requirements_in = "requirements_os_specific.in",
     requirements_linux = "requirements_lock_linux.txt",
     requirements_txt = "requirements_lock.txt",
     requirements_windows = "requirements_lock_windows.txt",
diff --git a/tests/pip_repository_entry_points/BUILD.bazel b/tests/pip_repository_entry_points/BUILD.bazel
index f0204ca..c39b1f0 100644
--- a/tests/pip_repository_entry_points/BUILD.bazel
+++ b/tests/pip_repository_entry_points/BUILD.bazel
@@ -5,6 +5,7 @@
 # This rule adds a convenient way to update the requirements file.
 compile_pip_requirements(
     name = "requirements",
+    src = "requirements.in",
     requirements_windows = ":requirements_windows.txt",
 )
 
diff --git a/tools/publish/BUILD.bazel b/tools/publish/BUILD.bazel
index 065e56b..4759a31 100644
--- a/tools/publish/BUILD.bazel
+++ b/tools/publish/BUILD.bazel
@@ -2,6 +2,7 @@
 
 compile_pip_requirements(
     name = "requirements",
+    src = "requirements.in",
     requirements_darwin = "requirements_darwin.txt",
     requirements_windows = "requirements_windows.txt",
 )