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

Fix revealed default config in header if requirements in subfolder #1904

Merged
merged 15 commits into from Aug 7, 2023
4 changes: 2 additions & 2 deletions piptools/locations.py
Expand Up @@ -5,5 +5,5 @@
# The user_cache_dir helper comes straight from pip itself
CACHE_DIR = user_cache_dir("pip-tools")

# The project defaults specific to pip-tools should be written to this filename
CONFIG_FILE_NAME = ".pip-tools.toml"
# The project defaults specific to pip-tools should be written to this filenames
DEFAULT_CONFIG_FILE_NAMES = (".pip-tools.toml", "pyproject.toml")
8 changes: 5 additions & 3 deletions piptools/scripts/compile.py
Expand Up @@ -20,7 +20,7 @@
from .._compat import parse_requirements
from ..cache import DependencyCache
from ..exceptions import NoCandidateFound, PipToolsError
from ..locations import CACHE_DIR, CONFIG_FILE_NAME
from ..locations import CACHE_DIR, DEFAULT_CONFIG_FILE_NAMES
from ..logging import log
from ..repositories import LocalRequirementsRepository, PyPIRepository
from ..repositories.base import BaseRepository
Expand Down Expand Up @@ -314,8 +314,10 @@ def _determine_linesep(
allow_dash=False,
path_type=str,
),
help=f"Read configuration from TOML file. By default, looks for a {CONFIG_FILE_NAME} or "
"pyproject.toml.",
help=(
f"Read configuration from TOML file. By default, looks for the following "
f"files in the given order: {', '.join(DEFAULT_CONFIG_FILE_NAMES)}."
),
is_eager=True,
callback=override_defaults_from_config_file,
)
Expand Down
8 changes: 5 additions & 3 deletions piptools/scripts/sync.py
Expand Up @@ -17,7 +17,7 @@
from .. import sync
from .._compat import Distribution, parse_requirements
from ..exceptions import PipToolsError
from ..locations import CONFIG_FILE_NAME
from ..locations import DEFAULT_CONFIG_FILE_NAMES
from ..logging import log
from ..repositories import PyPIRepository
from ..utils import (
Expand Down Expand Up @@ -98,8 +98,10 @@
allow_dash=False,
path_type=str,
),
help=f"Read configuration from TOML file. By default, looks for a {CONFIG_FILE_NAME} or "
"pyproject.toml.",
help=(
f"Read configuration from TOML file. By default, looks for the following "
f"files in the given order: {', '.join(DEFAULT_CONFIG_FILE_NAMES)}."
),
is_eager=True,
callback=override_defaults_from_config_file,
)
Expand Down
13 changes: 9 additions & 4 deletions piptools/utils.py
Expand Up @@ -12,6 +12,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, TypeVar, cast

from click.core import ParameterSource

if sys.version_info >= (3, 11):
import tomllib
else:
Expand All @@ -31,7 +33,7 @@
from pip._vendor.pkg_resources import get_distribution

from piptools._compat import PIP_VERSION
from piptools.locations import CONFIG_FILE_NAME
from piptools.locations import DEFAULT_CONFIG_FILE_NAMES
from piptools.subprocess_utils import run_python_snippet

if TYPE_CHECKING:
Expand Down Expand Up @@ -367,8 +369,11 @@ def get_compile_command(click_ctx: click.Context) -> str:

# Exclude config option if it's the default one
if option_long_name == "--config":
default_config = select_config_file(click_ctx.params.get("src_files", ()))
if value == default_config:
parameter_source = click_ctx.get_parameter_source(option_name)
if (
str(value) in DEFAULT_CONFIG_FILE_NAMES
or parameter_source == ParameterSource.DEFAULT
):
continue

# Skip options without a value
Expand Down Expand Up @@ -654,7 +659,7 @@ def select_config_file(src_files: tuple[str, ...]) -> Path | None:
(
candidate_dir / config_file
for candidate_dir in candidate_dirs
for config_file in (CONFIG_FILE_NAME, "pyproject.toml")
for config_file in DEFAULT_CONFIG_FILE_NAMES
if (candidate_dir / config_file).is_file()
),
None,
Expand Down
15 changes: 9 additions & 6 deletions tests/conftest.py
Expand Up @@ -31,7 +31,7 @@
from piptools._compat import PIP_VERSION, Distribution
from piptools.cache import DependencyCache
from piptools.exceptions import NoCandidateFound
from piptools.locations import CONFIG_FILE_NAME
from piptools.locations import DEFAULT_CONFIG_FILE_NAMES
from piptools.logging import log
from piptools.repositories import PyPIRepository
from piptools.repositories.base import BaseRepository
Expand Down Expand Up @@ -452,13 +452,16 @@ def make_config_file(tmpdir_cwd):
"""

def _maker(
pyproject_param: str, new_default: Any, config_file_name: str = CONFIG_FILE_NAME
pyproject_param: str,
new_default: Any,
config_file_name: str = DEFAULT_CONFIG_FILE_NAMES[0],
) -> Path:
# Make a config file with this one config default override
config_path = tmpdir_cwd / pyproject_param
config_file = config_path / config_file_name
config_path.mkdir(exist_ok=True)
# Create a nested directory structure if config_file_name includes directories
config_dir = Path(tmpdir_cwd / config_file_name).parent
atugushev marked this conversation as resolved.
Show resolved Hide resolved
config_dir.mkdir(exist_ok=True, parents=True)

# Make a config file with this one config default override
config_file = tmpdir_cwd / config_file_name
config_to_dump = {"tool": {"pip-tools": {pyproject_param: new_default}}}
config_file.write_text(tomli_w.dumps(config_to_dump))
return cast(Path, config_file.relative_to(tmpdir_cwd))
Expand Down
12 changes: 12 additions & 0 deletions tests/test_cli_compile.py
Expand Up @@ -2975,6 +2975,18 @@ def test_config_option(pip_conf, runner, tmp_path, make_config_file):
assert "Dry-run, so nothing updated" in out.stderr


def test_default_config_option(pip_conf, runner, make_config_file, tmpdir_cwd):
make_config_file("dry-run", True)

req_in = tmpdir_cwd / "requirements.in"
req_in.touch()

out = runner.invoke(cli)

assert out.exit_code == 0
assert "Dry-run, so nothing updated" in out.stderr


def test_no_config_option_overrides_config_with_defaults(
pip_conf, runner, tmp_path, make_config_file
):
Expand Down
13 changes: 13 additions & 0 deletions tests/test_cli_sync.py
Expand Up @@ -374,6 +374,19 @@ def test_default_python_executable_option(run, runner):
]


@mock.patch("piptools.sync.run")
def test_default_config_option(run, runner, make_config_file, tmpdir_cwd):
make_config_file("dry-run", True)

with open(sync.DEFAULT_REQUIREMENTS_FILE, "w") as reqs_txt:
reqs_txt.write("six==1.10.0")

out = runner.invoke(cli)

assert out.exit_code == 1
assert "Would install:" in out.stdout


@mock.patch("piptools.sync.run")
def test_config_option(run, runner, make_config_file):
config_file = make_config_file("dry-run", True)
Expand Down
81 changes: 81 additions & 0 deletions tests/test_utils.py
Expand Up @@ -6,6 +6,7 @@
import shlex
import sys
from pathlib import Path
from textwrap import dedent

import pip
import pytest
Expand All @@ -31,6 +32,7 @@
lookup_table,
lookup_table_from_tuples,
override_defaults_from_config_file,
select_config_file,
)


Expand Down Expand Up @@ -410,6 +412,36 @@ def test_get_compile_command_with_config(tmpdir_cwd, config_file, expected_comma
assert get_compile_command(ctx) == expected_command


@pytest.mark.parametrize("config_file", ("pyproject.toml", ".pip-tools.toml"))
@pytest.mark.parametrize(
"config_file_content",
(
pytest.param("", id="empty config file"),
pytest.param("[tool.pip-tools]", id="empty config section"),
pytest.param("[tool.pip-tools]\ndry-run = true", id="non-empty config section"),
),
)
def test_get_compile_command_does_not_include_default_config_if_reqs_file_in_subdir(
tmpdir_cwd, config_file, config_file_content
):
"""
Test that ``get_compile_command`` does not include default config file
if requirements file is in a subdirectory.
Regression test for issue GH-1903.
"""
default_config_file = Path(config_file)
default_config_file.write_text(config_file_content)

(tmpdir_cwd / "subdir").mkdir()
req_file = Path("subdir/requirements.in")
req_file.touch()
req_file.write_bytes(b"")

# Make sure that the default config file is not included
with compile_cli.make_context("pip-compile", [req_file.as_posix()]) as ctx:
assert get_compile_command(ctx) == f"pip-compile {req_file.as_posix()}"


def test_get_compile_command_escaped_filenames(tmpdir_cwd):
"""
Test that get_compile_command output (re-)escapes ' -- '-escaped filenames.
Expand Down Expand Up @@ -678,3 +710,52 @@ def test_callback_config_file_defaults_unreadable_toml(make_config_file):
"config",
"/dev/null/path/does/not/exist/my-config.toml",
)


def test_select_config_file_no_files(tmpdir_cwd):
assert select_config_file(()) is None


@pytest.mark.parametrize("filename", ("pyproject.toml", ".pip-tools.toml"))
def test_select_config_file_returns_config_in_cwd(make_config_file, filename):
config_file = make_config_file("dry-run", True, filename)
assert select_config_file(()) == config_file


def test_select_config_file_returns_empty_config_file_in_cwd(tmpdir_cwd):
config_file = Path(".pip-tools.toml")
config_file.touch()

assert select_config_file(()) == config_file


def test_select_config_file_cannot_find_config_in_cwd(tmpdir_cwd, make_config_file):
make_config_file("dry-run", True, "subdir/pyproject.toml")
assert select_config_file(()) is None


def test_select_config_file_with_config_file_in_subdir(tmpdir_cwd, make_config_file):
config_file = make_config_file("dry-run", True, "subdir/.pip-tools.toml")

requirement_file = Path("subdir/requirements.in")
requirement_file.touch()

assert select_config_file((requirement_file.as_posix(),)) == config_file


def test_select_config_file_prefers_pip_tools_toml_over_pyproject_toml(tmpdir_cwd):
pip_tools_file = Path(".pip-tools.toml")
pip_tools_file.touch()

pyproject_file = Path("pyproject.toml")
pyproject_file.write_text(
dedent(
"""\
[build-system]
requires = ["setuptools>=63", "setuptools_scm[toml]>=7"]
build-backend = "setuptools.build_meta"
"""
)
)

assert select_config_file(()) == pip_tools_file