Skip to content

Commit 29c5bc5

Browse files
committed
Add fixtures & tooling to simplify compile tests
Provide `pip_produces_absolute_paths` as a fixture, removing the need for some individual tests to check the pip version directly. The `TestFilesCollection` dataclass is a tool for writing parameters which includes the ability to populate a temp dir based on the structured data within. This simplifies test parameters and behavior. Additionally, mildly cleanup tests to use monkeypatch.context()
1 parent 4122e63 commit 29c5bc5

1 file changed

Lines changed: 154 additions & 150 deletions

File tree

tests/test_cli_compile.py

Lines changed: 154 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

3+
import dataclasses
34
import hashlib
45
import os
56
import pathlib
67
import re
78
import shutil
89
import subprocess
910
import sys
11+
import typing
1012
from textwrap import dedent
1113
from unittest import mock
1214
from unittest.mock import MagicMock
@@ -39,6 +41,55 @@
3941
)
4042

4143

44+
@pytest.fixture(scope="session")
45+
def installed_pip_version():
46+
return get_pip_version_for_python_executable(sys.executable)
47+
48+
49+
@pytest.fixture(scope="session")
50+
def pip_produces_absolute_paths(installed_pip_version):
51+
# in pip v24.3, new normalization will occur because `comes_from` started
52+
# to be normalized to abspaths
53+
return installed_pip_version >= Version("24.3")
54+
55+
56+
@dataclasses.dataclass
57+
class TestFilesCollection:
58+
"""
59+
A small data-builder for setting up files in a tmp dir.
60+
61+
Contains a name for use as the ID in parametrized tests and contents.
62+
'contents' maps from subpaths in the tmp dir to file content or callables
63+
which produce file content given the tmp dir.
64+
"""
65+
66+
# the name for the collection of files
67+
name: str
68+
# static or computed contents
69+
contents: dict[str, str | typing.Callable[[pathlib.Path], str]]
70+
71+
def __str__(self) -> str:
72+
return self.name
73+
74+
def populate(self, tmp_path: pathlib.Path) -> None:
75+
"""Populate the tmp dir with file contents."""
76+
for path_str, content in self.contents.items():
77+
path = tmp_path / path_str
78+
path.parent.mkdir(exist_ok=True, parents=True)
79+
if isinstance(content, str):
80+
path.write_text(content)
81+
else:
82+
path.write_text(content(tmp_path))
83+
84+
def get_path_to(self, filename: str) -> str:
85+
"""Given a filename, find the (first) path to that filename in the contents."""
86+
return next(
87+
k
88+
for k in self.contents.keys()
89+
if (k == filename) or k.endswith(f"/{filename}")
90+
)
91+
92+
4293
@pytest.fixture(
4394
autouse=True,
4495
params=[
@@ -3840,31 +3891,34 @@ def test_stdout_should_not_be_read_when_stdin_is_not_a_plain_file(
38403891
assert out.exit_code == 0, out
38413892

38423893

3894+
@pytest.mark.parametrize("input_path_absolute", (True, False))
38433895
@pytest.mark.parametrize(
3896+
"test_files_collection",
38443897
(
3845-
"first_order_path_absolute",
3846-
"second_order_path_absolute",
3847-
"normed_output_absolute",
3848-
),
3849-
(
3850-
# requirements.in + requirements2.in -> requirements2.in
3851-
pytest.param(False, False, False, id="both-relative"),
3852-
# /foo/requirements.in + /foo/requirements2.in -> /foo/requirements2.in
3853-
pytest.param(True, True, True, id="both-absolute"),
3854-
# /foo/requirements.in + requirements2.in -> /foo/requirements2.in
3855-
pytest.param(True, False, True, id="absolute-includes-relative"),
3856-
# requirements.in + /foo/requirements2.in -> requirements2.in
3857-
pytest.param(False, True, False, id="relative-includes-absolute"),
3898+
TestFilesCollection(
3899+
"relative_include",
3900+
{
3901+
"requirements2.in": "small-fake-a\n",
3902+
"requirements.in": "-r requirements2.in\n",
3903+
},
3904+
),
3905+
TestFilesCollection(
3906+
"absolute_include",
3907+
{
3908+
"requirements2.in": "small-fake-a\n",
3909+
"requirements.in": lambda tmpdir: f"-r {(tmpdir / 'requirements2.in').as_posix()}",
3910+
},
3911+
),
38583912
),
3913+
ids=str,
38593914
)
38603915
def test_second_order_requirements_path_handling(
38613916
pip_conf,
38623917
runner,
38633918
tmp_path,
38643919
monkeypatch,
3865-
first_order_path_absolute,
3866-
second_order_path_absolute,
3867-
normed_output_absolute,
3920+
input_path_absolute,
3921+
test_files_collection,
38683922
):
38693923
"""
38703924
Given nested requirements files, the internal requirements will be written
@@ -3876,67 +3930,34 @@ def test_second_order_requirements_path_handling(
38763930
On lower pip versions, assert the previous output is produced, in which
38773931
`-r` inputs are preserved.
38783932
"""
3879-
# in pip v24.3, new normalization will occur because `comes_from` started
3880-
# to be normalized to abspaths
3881-
pip_current_version = get_pip_version_for_python_executable(sys.executable)
3882-
expect_new_normalization = pip_current_version >= Version("24.3")
3883-
3884-
# setup: there are two files, the second one has an actual package name
3885-
req_in = tmp_path / "requirements.in"
3886-
req_in2 = tmp_path / "requirements2.in"
3887-
req_in2.write_text("small-fake-a\n")
3888-
# the data for the first file is written based on whether or not the
3889-
# second-order path is absolute
3890-
if second_order_path_absolute:
3891-
req_in.write_text(f"-r {req_in2.as_posix()}\n")
3892-
else:
3893-
req_in.write_text("-r ./requirements2.in\n")
3933+
test_files_collection.populate(tmp_path)
38943934

3895-
# and the first order path is given on the CLI as absolute or relative
3896-
if first_order_path_absolute:
3897-
input_path = req_in.as_posix()
3935+
# the input path is given on the CLI as absolute or relative
3936+
# and this determines the expected output path as well
3937+
if input_path_absolute:
3938+
input_path = (tmp_path / "requirements.in").as_posix()
3939+
output_path = (tmp_path / "requirements2.in").as_posix()
38983940
else:
38993941
input_path = "requirements.in"
3942+
output_path = "requirements2.in"
39003943

3901-
monkeypatch.chdir(tmp_path)
3902-
out = runner.invoke(
3903-
cli,
3904-
[
3905-
"--output-file",
3906-
"-",
3907-
"--quiet",
3908-
"--no-header",
3909-
"--no-emit-options",
3910-
"-r",
3911-
input_path,
3912-
],
3913-
)
3914-
monkeypatch.undo()
3944+
with monkeypatch.context() as m:
3945+
m.chdir(tmp_path)
3946+
3947+
out = runner.invoke(
3948+
cli,
3949+
[
3950+
"--output-file",
3951+
"-",
3952+
"--quiet",
3953+
"--no-header",
3954+
"--no-emit-options",
3955+
"-r",
3956+
input_path,
3957+
],
3958+
)
39153959

39163960
assert out.exit_code == 0
3917-
# either we have the new normalization steps apply, and we'll get the abspath
3918-
# or the relative path
3919-
if expect_new_normalization:
3920-
if normed_output_absolute:
3921-
output_path = req_in2.as_posix()
3922-
else:
3923-
output_path = "requirements2.in"
3924-
# or else it's the older codepath (earlier pip versions) and each of the four input
3925-
# cases produces slightly different output
3926-
# exact input path in the output
3927-
else:
3928-
if first_order_path_absolute and second_order_path_absolute: # True, True
3929-
output_path = req_in2.as_posix()
3930-
elif (
3931-
not first_order_path_absolute and second_order_path_absolute
3932-
): # False, True
3933-
output_path = "requirements2.in"
3934-
elif (
3935-
first_order_path_absolute and not second_order_path_absolute
3936-
): # True, False
3937-
output_path = tmp_path.as_posix() + "/./requirements2.in"
3938-
else: # False, False
3939-
output_path = "./requirements2.in"
39403961
assert out.stdout == dedent(
39413962
f"""\
39423963
small-fake-a==0.2
@@ -3946,10 +3967,39 @@ def test_second_order_requirements_path_handling(
39463967

39473968

39483969
@pytest.mark.parametrize(
3949-
"relative_requirement_location", ("parent", "subdir", "sibling_dir")
3970+
"test_files_collection",
3971+
(
3972+
TestFilesCollection(
3973+
"parent_dir",
3974+
{
3975+
"requirements2.in": "small-fake-a\n",
3976+
"subdir/requirements.in": "-r ../requirements2.in\n",
3977+
},
3978+
),
3979+
TestFilesCollection(
3980+
"subdir",
3981+
{
3982+
"requirements.in": "-r ./subdir/requirements2.in",
3983+
"subdir/requirements2.in": "small-fake-a\n",
3984+
},
3985+
),
3986+
TestFilesCollection(
3987+
"sibling_dir",
3988+
{
3989+
"subdir1/requirements.in": "-r ../subdir2/requirements2.in",
3990+
"subdir2/requirements2.in": "small-fake-a\n",
3991+
},
3992+
),
3993+
),
3994+
ids=str,
39503995
)
39513996
def test_second_order_requirements_relative_path_in_separate_dir(
3952-
pip_conf, runner, tmp_path, monkeypatch, relative_requirement_location
3997+
pip_conf,
3998+
runner,
3999+
tmp_path,
4000+
monkeypatch,
4001+
test_files_collection,
4002+
pip_produces_absolute_paths,
39534003
):
39544004
"""
39554005
As in the above test, nested requirements file path handling.
@@ -3959,85 +4009,39 @@ def test_second_order_requirements_relative_path_in_separate_dir(
39594009
The expectation is that the output path will be relative to the current
39604010
working directory, *not* relative to the dir containing the initial
39614011
requirements file.
3962-
3963-
Parent dir layout:
3964-
- root_dir/
3965-
- requirements2.in
3966-
- subdir/
3967-
- requirements.in # containing '-r ../requirements2.in'
3968-
3969-
Subdir layout:
3970-
- root_dir/
3971-
- requirements.in # containing '-r ./subdir/requirements2.in'
3972-
- subdir/
3973-
- requirements2.in
3974-
3975-
Sibling dir layout:
3976-
- root_dir/
3977-
- subdir1/
3978-
- requirements.in # containing '-r ../subdir2/requirements2.in'
3979-
- subdir2/
3980-
- requirements2.in
39814012
"""
3982-
pip_current_version = get_pip_version_for_python_executable(sys.executable)
3983-
expect_path_normalization = pip_current_version >= Version("24.3")
3984-
3985-
# setup the two files in the requested dir layout
3986-
if relative_requirement_location == "parent":
3987-
(tmp_path / "subdir").mkdir()
3988-
req_in = tmp_path / "subdir" / "requirements.in"
3989-
req_in.write_text("-r ../requirements2.in\n")
3990-
req_in2 = tmp_path / "requirements2.in"
3991-
req_in2.write_text("small-fake-a\n")
3992-
3993-
if expect_path_normalization:
3994-
output_path = "requirements2.in"
3995-
else:
3996-
output_path = "subdir/../requirements2.in"
3997-
elif relative_requirement_location == "subdir":
3998-
req_in = tmp_path / "requirements.in"
3999-
req_in.write_text("-r ./subdir/requirements2.in\n")
4000-
(tmp_path / "subdir").mkdir()
4001-
req_in2 = tmp_path / "subdir" / "requirements2.in"
4002-
req_in2.write_text("small-fake-a\n")
4003-
4004-
if expect_path_normalization:
4005-
output_path = "subdir/requirements2.in"
4006-
else:
4007-
output_path = "./subdir/requirements2.in"
4008-
elif relative_requirement_location == "sibling_dir":
4009-
(tmp_path / "subdir1").mkdir()
4010-
req_in = tmp_path / "subdir1" / "requirements.in"
4011-
req_in.write_text("-r ../subdir2/requirements2.in\n")
4012-
(tmp_path / "subdir2").mkdir()
4013-
req_in2 = tmp_path / "subdir2" / "requirements2.in"
4014-
req_in2.write_text("small-fake-a\n")
4015-
4016-
if expect_path_normalization:
4017-
output_path = "subdir2/requirements2.in"
4018-
else:
4019-
output_path = "subdir1/../subdir2/requirements2.in"
4020-
else:
4021-
# test params didn't match body
4022-
raise NotImplementedError(relative_requirement_location)
4023-
4024-
# the input is given relative to the starting dir
4025-
input_path = req_in.relative_to(tmp_path).as_posix()
4013+
test_files_collection.populate(tmp_path)
4014+
# the input is the path to 'requirements.in' relative to the starting dir
4015+
input_path = test_files_collection.get_path_to("requirements.in")
4016+
# the output should also be relative to the starting dir, the path to 'requirements2.in'
4017+
output_path = test_files_collection.get_path_to("requirements2.in")
4018+
4019+
# for older pip versions, recompute the output path to be relative to the input path
4020+
if not pip_produces_absolute_paths:
4021+
# traverse upwards to the root tmp dir, and append the output path to that
4022+
# similar to pathlib.Path.relative_to(..., walk_up=True)
4023+
relative_segments = len(pathlib.Path(input_path).parents) - 1
4024+
output_path = str(
4025+
pathlib.Path(input_path).parent / ("../" * relative_segments) / output_path
4026+
)
4027+
# pathlib paths normalize `./foo` to `foo` automatically; de-norm that case
4028+
if str(pathlib.Path(input_path).parent) == ".":
4029+
output_path = "./" + output_path
40264030

4027-
monkeypatch.chdir(tmp_path)
4028-
out = runner.invoke(
4029-
cli,
4030-
[
4031-
"--output-file",
4032-
"-",
4033-
"--quiet",
4034-
"--no-header",
4035-
"--no-emit-options",
4036-
"-r",
4037-
input_path,
4038-
],
4039-
)
4040-
monkeypatch.undo()
4031+
with monkeypatch.context() as m:
4032+
m.chdir(tmp_path)
4033+
out = runner.invoke(
4034+
cli,
4035+
[
4036+
"--output-file",
4037+
"-",
4038+
"--quiet",
4039+
"--no-header",
4040+
"--no-emit-options",
4041+
"-r",
4042+
input_path,
4043+
],
4044+
)
40414045

40424046
assert out.exit_code == 0
40434047
assert out.stdout == dedent(

0 commit comments

Comments
 (0)