Skip to content

Commit e604dec

Browse files
authored
Merge pull request #2145 from webknjaz/maintenance/reproducible-ci-fixes-pr2106-pr2141-pr2142
A combo of CI unblocks via PRs #2106 #2134 #2141 #2142 #2146
2 parents 5330964 + fb0f69b commit e604dec

File tree

7 files changed

+170
-27
lines changed

7 files changed

+170
-27
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,15 @@ jobs:
6161
pip-version: >-
6262
${{
6363
fromJSON(
64-
github.job_workflow_sha
64+
inputs.cpython-pip-version
6565
&& inputs.cpython-pip-version
66-
|| '["latest", "previous"]'
66+
|| '["supported", "lowest"]'
6767
)
6868
}}
69-
include:
70-
- os: Ubuntu
71-
python-version: >-
72-
${{
73-
github.job_workflow_sha
74-
&& '3.12-dev'
75-
|| '3.8'
76-
}}
77-
pip-version: main
7869
env:
7970
TOXENV: >-
8071
pip${{ matrix.pip-version }}${{
81-
!github.job_workflow_sha
72+
!inputs.cpython-pip-version
8273
&& '-coverage'
8374
|| ''
8475
}}
@@ -121,7 +112,7 @@ jobs:
121112
run: tox --skip-pkg-install
122113
- name: Upload coverage to Codecov
123114
if: >-
124-
!github.job_workflow_sha
115+
!inputs.cpython-pip-version
125116
uses: codecov/codecov-action@v3
126117
with:
127118
files: ./coverage.xml
@@ -148,9 +139,9 @@ jobs:
148139
- MacOS
149140
- Windows
150141
python-version:
151-
- pypy-3.8
142+
- pypy-3.10
152143
pip-version:
153-
- latest
144+
- supported
154145
env:
155146
TOXENV: pip${{ matrix.pip-version }}
156147
steps:

.github/workflows/cron.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ jobs:
1111
uses: ./.github/workflows/ci.yml
1212
with:
1313
cpython-pip-version: >-
14-
["main", "latest", "previous"]
14+
["main", "latest", "supported", "lowest"]

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ repos:
3535
- build==1.0.0
3636
- pyproject_hooks==1.0.0
3737
- pytest==7.4.2
38-
language_version: python3.8
38+
language_version: python3.9
3939
- repo: https://github.com/PyCQA/bandit
4040
rev: 1.7.8
4141
hooks:

piptools/build.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import collections
44
import contextlib
5+
import os
56
import pathlib
67
import sys
78
import tempfile
@@ -119,6 +120,7 @@ def build_project_metadata(
119120
src_file: pathlib.Path,
120121
build_targets: tuple[str, ...],
121122
*,
123+
upgrade_packages: tuple[str, ...] | None = None,
122124
attempt_static_parse: bool,
123125
isolated: bool,
124126
quiet: bool,
@@ -159,7 +161,12 @@ def build_project_metadata(
159161
return project_metadata
160162

161163
src_dir = src_file.parent
162-
with _create_project_builder(src_dir, isolated=isolated, quiet=quiet) as builder:
164+
with _create_project_builder(
165+
src_dir,
166+
upgrade_packages=upgrade_packages,
167+
isolated=isolated,
168+
quiet=quiet,
169+
) as builder:
163170
metadata = _build_project_wheel_metadata(builder)
164171
extras = tuple(metadata.get_all("Provides-Extra") or ())
165172
requirements = tuple(
@@ -180,9 +187,80 @@ def build_project_metadata(
180187
)
181188

182189

190+
@contextlib.contextmanager
191+
def _env_var(
192+
env_var_name: str,
193+
env_var_value: str,
194+
/,
195+
) -> Iterator[None]:
196+
sentinel = object()
197+
original_pip_constraint = os.getenv(env_var_name, sentinel)
198+
pip_constraint_was_unset = original_pip_constraint is sentinel
199+
200+
os.environ[env_var_name] = env_var_value
201+
try:
202+
yield
203+
finally:
204+
if pip_constraint_was_unset:
205+
del os.environ[env_var_name]
206+
return
207+
208+
# Assert here is necessary because MyPy can't infer type
209+
# narrowing in the complex case.
210+
assert isinstance(original_pip_constraint, str)
211+
os.environ[env_var_name] = original_pip_constraint
212+
213+
214+
@contextlib.contextmanager
215+
def _temporary_constraints_file_set_for_pip(
216+
upgrade_packages: tuple[str, ...],
217+
) -> Iterator[None]:
218+
with tempfile.NamedTemporaryFile(
219+
mode="w+t",
220+
delete=False, # FIXME: switch to `delete_on_close` in Python 3.12+
221+
) as tmpfile:
222+
# NOTE: `delete_on_close=False` here (or rather `delete=False`,
223+
# NOTE: temporarily) is important for cross-platform execution. It is
224+
# NOTE: required on Windows so that the underlying `pip install`
225+
# NOTE: invocation by pypa/build will be able to access the constraint
226+
# NOTE: file via a subprocess and not fail installing it due to a
227+
# NOTE: permission error related to this file handle still open in our
228+
# NOTE: parent process. To achieve this, we `.close()` the file
229+
# NOTE: descriptor before we hand off the control to the build frontend
230+
# NOTE: and with `delete_on_close=False`, the
231+
# NOTE: `tempfile.NamedTemporaryFile()` context manager does not remove
232+
# NOTE: it from disk right away.
233+
# NOTE: Due to support of versions below Python 3.12, we are forced to
234+
# NOTE: temporarily resort to using `delete=False`, meaning that the CM
235+
# NOTE: never attempts removing the file from disk, not even on exit.
236+
# NOTE: So we do this manually until we can migrate to using the more
237+
# NOTE: ergonomic argument `delete_on_close=False`.
238+
239+
# Write packages to upgrade to a temporary file to set as
240+
# constraints for the installation to the builder environment,
241+
# in case build requirements are among them
242+
tmpfile.write("\n".join(upgrade_packages))
243+
244+
# FIXME: replace `delete` with `delete_on_close` in Python 3.12+
245+
# FIXME: and replace `.close()` with `.flush()`
246+
tmpfile.close()
247+
248+
try:
249+
with _env_var("PIP_CONSTRAINT", tmpfile.name):
250+
yield
251+
finally:
252+
# FIXME: replace `delete` with `delete_on_close` in Python 3.12+
253+
# FIXME: and drop this manual deletion
254+
os.unlink(tmpfile.name)
255+
256+
183257
@contextlib.contextmanager
184258
def _create_project_builder(
185-
src_dir: pathlib.Path, *, isolated: bool, quiet: bool
259+
src_dir: pathlib.Path,
260+
*,
261+
upgrade_packages: tuple[str, ...] | None = None,
262+
isolated: bool,
263+
quiet: bool,
186264
) -> Iterator[build.ProjectBuilder]:
187265
if quiet:
188266
runner = pyproject_hooks.quiet_subprocess_runner
@@ -193,7 +271,13 @@ def _create_project_builder(
193271
yield build.ProjectBuilder(src_dir, runner=runner)
194272
return
195273

196-
with build.env.DefaultIsolatedEnv() as env:
274+
maybe_pip_constrained_context = (
275+
contextlib.nullcontext()
276+
if upgrade_packages is None
277+
else _temporary_constraints_file_set_for_pip(upgrade_packages)
278+
)
279+
280+
with maybe_pip_constrained_context, build.env.DefaultIsolatedEnv() as env:
197281
builder = build.ProjectBuilder.from_isolated_env(env, src_dir, runner)
198282
env.install(builder.build_system_requires)
199283
env.install(builder.get_requires_for_build("wheel"))

piptools/scripts/compile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ def cli(
370370
metadata = build_project_metadata(
371371
src_file=Path(src_file),
372372
build_targets=build_deps_targets,
373+
upgrade_packages=upgrade_packages,
373374
attempt_static_parse=not bool(build_deps_targets),
374375
isolated=build_isolation,
375376
quiet=log.verbosity <= 0,

tests/test_cli_compile.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3431,7 +3431,6 @@ def test_compile_recursive_extras_build_targets(runner, tmp_path, current_resolv
34313431
"""
34323432
)
34333433
)
3434-
(tmp_path / "constraints.txt").write_text("wheel<0.43")
34353434
out = runner.invoke(
34363435
cli,
34373436
[
@@ -3446,15 +3445,82 @@ def test_compile_recursive_extras_build_targets(runner, tmp_path, current_resolv
34463445
"--find-links",
34473446
os.fspath(MINIMAL_WHEELS_PATH),
34483447
os.fspath(tmp_path / "pyproject.toml"),
3449-
"--constraint",
3450-
os.fspath(tmp_path / "constraints.txt"),
34513448
"--output-file",
34523449
"-",
34533450
],
34543451
)
34553452
expected = rf"""foo[footest] @ {tmp_path.as_uri()}
34563453
small-fake-a==0.2
34573454
small-fake-b==0.3
3455+
3456+
# The following packages are considered to be unsafe in a requirements file:
3457+
# setuptools
3458+
"""
3459+
try:
3460+
assert out.exit_code == 0
3461+
assert expected == out.stdout
3462+
except Exception: # pragma: no cover
3463+
print(out.stdout)
3464+
print(out.stderr)
3465+
raise
3466+
3467+
3468+
@backtracking_resolver_only
3469+
def test_compile_build_targets_setuptools_no_wheel_dep(
3470+
runner,
3471+
tmp_path,
3472+
current_resolver,
3473+
):
3474+
"""Check that user requests apply to build dependencies.
3475+
3476+
This verifies that build deps compilation would not use the latest version
3477+
of an unconstrained build requirements list, when the user requested
3478+
restricting them.
3479+
3480+
It is implemented against `setuptools < 70.1.0` which is known to inject a
3481+
dependency on `wheel` (newer `setuptools` vendor it). The idea is that
3482+
`pyproject.toml` does not have an upper bound for `setuptools` but the CLI
3483+
arg does. And when this works correctly, the `wheel` entry will be included
3484+
into the resolved output.
3485+
3486+
This is a regression test for
3487+
https://github.com/jazzband/pip-tools/pull/1681#issuecomment-2212541289.
3488+
"""
3489+
(tmp_path / "pyproject.toml").write_text(
3490+
dedent(
3491+
"""
3492+
[project]
3493+
name = "foo"
3494+
version = "0.0.1"
3495+
dependencies = ["small-fake-a"]
3496+
"""
3497+
)
3498+
)
3499+
(tmp_path / "constraints.txt").write_text("wheel<0.43")
3500+
out = runner.invoke(
3501+
cli,
3502+
[
3503+
"--build-isolation",
3504+
"--no-header",
3505+
"--no-annotate",
3506+
"--no-emit-options",
3507+
"--extra",
3508+
"dev",
3509+
"--build-deps-for",
3510+
"wheel",
3511+
"--find-links",
3512+
os.fspath(MINIMAL_WHEELS_PATH),
3513+
os.fspath(tmp_path / "pyproject.toml"),
3514+
"--constraint",
3515+
os.fspath(tmp_path / "constraints.txt"),
3516+
"--upgrade-package",
3517+
"setuptools < 70.1.0", # setuptools>=70.1.0 doesn't require wheel any more
3518+
"--output-file",
3519+
"-",
3520+
],
3521+
catch_exceptions=True,
3522+
)
3523+
expected = r"""small-fake-a==0.2
34583524
wheel==0.42.0
34593525
34603526
# The following packages are considered to be unsafe in a requirements file:

tox.ini

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[tox]
22
envlist =
33
# NOTE: keep this in sync with the env list in .github/workflows/ci.yml.
4-
py{38,39,310,311,312,py3}-pip{previous,latest,main}-coverage
5-
pip{previous,latest,main}-coverage
6-
pip{previous,latest,main}
4+
py{38,39,310,311,312,py3}-pip{supported,lowest,latest,main}-coverage
5+
pip{supported,lowest,latest,main}-coverage
6+
pip{supported,lowest,latest,main}
77
checkqa
88
readme
99
skip_missing_interpreters = True
@@ -14,7 +14,8 @@ extras =
1414
testing
1515
coverage: coverage
1616
deps =
17-
pipprevious: pip==22.2.*
17+
pipsupported: pip==24.2
18+
piplowest: pip==22.2.*
1819
piplatest: pip
1920
pipmain: https://github.com/pypa/pip/archive/main.zip
2021
setenv =

0 commit comments

Comments
 (0)