diff --git a/news/11440.feature.rst b/news/11440.feature.rst new file mode 100644 index 00000000000..7ce3bdee3da --- /dev/null +++ b/news/11440.feature.rst @@ -0,0 +1 @@ +Handle pyproject.toml in "install -r". diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index dc1328ff019..3d69dc67587 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -31,8 +31,8 @@ install_req_from_parsed_requirement, install_req_from_req_string, ) +from pip._internal.req.dependencies_file import parse_dependencies, parse_requirements from pip._internal.req.req_dependency_group import parse_dependency_groups -from pip._internal.req.req_file import parse_requirements from pip._internal.req.req_install import InstallRequirement from pip._internal.resolution.base import BaseResolver from pip._internal.utils.temp_dir import ( @@ -269,7 +269,7 @@ def get_requirements( # NOTE: options.require_hashes may be set if --require-hashes is True for filename in options.requirements: - for parsed_req in parse_requirements( + for parsed_req in parse_dependencies( filename, finder=finder, options=options, session=session ): req_to_add = install_req_from_parsed_requirement( diff --git a/src/pip/_internal/req/dependencies_file.py b/src/pip/_internal/req/dependencies_file.py new file mode 100644 index 00000000000..7dc38c411d8 --- /dev/null +++ b/src/pip/_internal/req/dependencies_file.py @@ -0,0 +1,31 @@ +""" +Common actions for dealing with dependencies file, +either requirements.txt or pyproject.toml +""" + +from __future__ import annotations + +import optparse +import pathlib +from collections.abc import Generator + +from pip._internal.index.package_finder import PackageFinder +from pip._internal.network.session import PipSession +from pip._internal.req.pyproject_file import parse_pyproject_requirements +from pip._internal.req.req_file import ParsedRequirement, parse_requirements + + +def parse_dependencies( + filename: str, + session: PipSession, + finder: PackageFinder | None = None, + options: optparse.Values | None = None, +) -> Generator[ParsedRequirement, None, None]: + if pathlib.PurePath(filename).name == "pyproject.toml": + return parse_pyproject_requirements( + filename, finder=finder, options=options, session=session + ) + else: + return parse_requirements( + filename, finder=finder, options=options, session=session + ) diff --git a/src/pip/_internal/req/pyproject_file.py b/src/pip/_internal/req/pyproject_file.py new file mode 100644 index 00000000000..aa2f65d60c6 --- /dev/null +++ b/src/pip/_internal/req/pyproject_file.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import optparse +from collections.abc import Generator + +from pip._internal.exceptions import InstallationError +from pip._internal.index.package_finder import PackageFinder +from pip._internal.network.session import PipSession +from pip._internal.req.req_file import ParsedRequirement +from pip._internal.utils.compat import tomllib + + +def parse_pyproject_requirements( + filename: str, + session: PipSession, + finder: PackageFinder | None = None, + options: optparse.Values | None = None, +) -> Generator[ParsedRequirement, None, None]: + try: + with open(filename, "rb") as f: + pyproject = tomllib.load(f) + except OSError as exc: + raise InstallationError(f"Could not open requirements file: {exc}") + + project = pyproject.get("project", {}) + dynamic = project.get("dynamic", []) + + if "dependencies" in dynamic: + raise InstallationError( + "Installing dynamic dependencies is not supported " + "(dynamic dependencies in {filename})" + ) + + for dependency_line in project.get("dependencies", []): + yield ParsedRequirement( + requirement=dependency_line, + is_editable=False, + comes_from=f"-r {filename}", + constraint=False, + options={}, # TODO + line_source=filename, + )