Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for self-referential extras #1791

Merged
merged 13 commits into from
Apr 6, 2023
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ repos:
- pep517==0.10.0
- toml==0.10.2
- pip==20.3.4
- build==0.9.0
- repo: https://github.com/PyCQA/bandit
rev: 1.7.4
hooks:
Expand Down
4 changes: 2 additions & 2 deletions piptools/_compat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations

from .pip_compat import PIP_VERSION, parse_requirements
from .pip_compat import PIP_VERSION, install_req_from_line, parse_requirements

__all__ = ["PIP_VERSION", "parse_requirements"]
__all__ = ["PIP_VERSION", "install_req_from_line", "parse_requirements"]
26 changes: 24 additions & 2 deletions piptools/_compat/pip_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
from pip._internal.network.session import PipSession
from pip._internal.req import InstallRequirement
from pip._internal.req import parse_requirements as _parse_requirements
from pip._internal.req.constructors import install_req_from_parsed_requirement
from pip._internal.req.constructors import (
install_req_from_parsed_requirement,
parse_req_from_line,
)
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.pkg_resources import Requirement

PIP_VERSION = tuple(map(int, parse_version(pip.__version__).base_version.split(".")))


__all__ = [
"dist_requires",
"uses_pkg_resources",
Expand All @@ -36,6 +38,26 @@ def parse_requirements(
yield install_req_from_parsed_requirement(parsed_req, isolated=isolated)


def install_req_from_line(
name: str,
comes_from: str | InstallRequirement | None = None,
base_package: str | None = None,
base_dir: str | None = None,
) -> InstallRequirement:
q0w marked this conversation as resolved.
Show resolved Hide resolved
parts = parse_req_from_line(name, comes_from)
if base_package and base_dir and parts.requirement.name == base_package:
name = name.replace(base_package, base_dir, 1)
parts = parse_req_from_line(name, comes_from)
atugushev marked this conversation as resolved.
Show resolved Hide resolved

return InstallRequirement(
parts.requirement,
comes_from,
link=parts.link,
markers=parts.markers,
extras=parts.extras,
)


# The Distribution interface has changed between pkg_resources and
# importlib.metadata, so this compat layer allows for a consistent access
# pattern. In pip 22.1, importlib.metadata became the default on Python 3.11
Expand Down
13 changes: 9 additions & 4 deletions piptools/scripts/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@
from click.utils import LazyFile, safecall
from pip._internal.commands import create_command
from pip._internal.req import InstallRequirement
from pip._internal.req.constructors import install_req_from_line
from pip._internal.utils.misc import redact_auth_from_url

from .._compat import parse_requirements
from .._compat import install_req_from_line, parse_requirements
from ..cache import DependencyCache
from ..exceptions import NoCandidateFound, PipToolsError
from ..locations import CACHE_DIR
Expand Down Expand Up @@ -498,10 +497,16 @@ def cli(
log.error(str(e))
log.error(f"Failed to parse {os.path.abspath(src_file)}")
sys.exit(2)
comes_from = f"{metadata.get_all('Name')[0]} ({src_file})"
package_name = metadata.get_all("Name")[0]
comes_from = f"{package_name} ({src_file})"
constraints.extend(
[
install_req_from_line(req, comes_from=comes_from)
install_req_from_line(
req,
comes_from,
package_name,
os.path.dirname(os.path.abspath(src_file)),
)
for req in metadata.get_all("Requires-Dist") or []
]
)
Expand Down
36 changes: 36 additions & 0 deletions tests/test_cli_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2804,3 +2804,39 @@ def test_raise_error_when_input_and_output_filenames_are_matched(
f"Error: input and output filenames must not be matched: {req_out_path}"
)
assert expected_error in out.stderr.splitlines()


@backtracking_resolver_only
def test_compile_recursive_extras(runner, tmp_path, current_resolver):
(tmp_path / "pyproject.toml").write_text(
dedent(
"""
[project]
name = "foo"
version = "0.0.1"
dependencies = ["small-fake-a"]
[project.optional-dependencies]
footest = ["small-fake-b"]
dev = ["foo[footest]"]
"""
)
)
out = runner.invoke(
cli,
[
"--no-header",
"--no-annotate",
"--no-emit-find-links",
"--extra",
"dev",
"--find-links",
os.fspath(MINIMAL_WHEELS_PATH),
os.fspath(tmp_path / "pyproject.toml"),
],
)
expected = rf"""foo @ {tmp_path.as_uri()}
small-fake-a==0.2
small-fake-b==0.3
"""
atugushev marked this conversation as resolved.
Show resolved Hide resolved
assert out.exit_code == 0
assert expected == out.stderr