Skip to content

Commit 583c101

Browse files
committed
[Feature] Allow Environment Override
Allow the pip environment used for evaluating environment markers to be overriden, so requirements can be compiled for an environment different than the user's current environment.
1 parent e8ab3b9 commit 583c101

File tree

3 files changed

+161
-1
lines changed

3 files changed

+161
-1
lines changed

README.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ etc.). For an exact definition, refer to the possible combinations of `PEP 508
532532
environment markers`_.
533533

534534
As the resulting ``requirements.txt`` can differ for each environment, users must
535-
execute ``pip-compile`` **on each Python environment separately** to generate a
535+
execute ``pip-compile`` **for each Python environment separately** to generate a
536536
``requirements.txt`` valid for each said environment. The same ``requirements.in`` can
537537
be used as the source file for all environments, using `PEP 508 environment markers`_ as
538538
needed, the same way it would be done for regular ``pip`` cross-environment usage.
@@ -544,6 +544,12 @@ dependencies, making any newly generated ``requirements.txt`` environment-depend
544544
As a general rule, it's advised that users should still always execute ``pip-compile``
545545
on each targeted Python environment to avoid issues.
546546

547+
There is a feature (`--override-environment`) that can be used to
548+
specify the environment when gathering dependencies, allowing for cross-environment
549+
fetching. However, a different ``requirements.txt`` must still be generated per
550+
environment. It is recommended to override all keys in `PEP 508` when targetting a
551+
different environment so the environment is fully defined.
552+
547553
.. _PEP 508 environment markers: https://www.python.org/dev/peps/pep-0508/#environment-markers
548554

549555
Other useful tools

piptools/scripts/compile.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,13 @@ def _determine_linesep(
301301
help="Specify a package to consider unsafe; may be used more than once. "
302302
f"Replaces default unsafe packages: {', '.join(sorted(UNSAFE_PACKAGES))}",
303303
)
304+
@click.option(
305+
"--override-environment",
306+
multiple=True,
307+
type=(str, str),
308+
help="Specify an environment marker to override."
309+
"This can be used to fetch requirements for a different platform",
310+
)
304311
def cli(
305312
ctx: click.Context,
306313
verbose: int,
@@ -339,6 +346,7 @@ def cli(
339346
emit_index_url: bool,
340347
emit_options: bool,
341348
unsafe_package: tuple[str, ...],
349+
override_environment: tuple[tuple[str, str], ...],
342350
) -> None:
343351
"""
344352
Compiles requirements.txt from requirements.in, pyproject.toml, setup.cfg,
@@ -426,6 +434,34 @@ def cli(
426434
pip_args.extend(["--use-deprecated", "legacy-resolver"])
427435
pip_args.extend(right_args)
428436

437+
env_dict = dict(override_environment)
438+
if len(env_dict) > 0:
439+
# Since the environment is overriden globally, handle it here in the
440+
# top level instead of within the resolver.
441+
import pip._vendor.packaging.markers
442+
443+
default_env = pip._vendor.packaging.markers.default_environment()
444+
445+
def overriden_environment() -> dict[str, str]:
446+
return {
447+
k: env_dict.get(k, default_env[k])
448+
for k in [
449+
"implementation_name",
450+
"implementation_version",
451+
"os_name",
452+
"platform_machine",
453+
"platform_release",
454+
"platform_system",
455+
"platform_version",
456+
"python_full_version",
457+
"platform_python_implementation",
458+
"python_version",
459+
"sys_platform",
460+
]
461+
}
462+
463+
pip._vendor.packaging.markers.default_environment = overriden_environment
464+
429465
repository: BaseRepository
430466
repository = PyPIRepository(pip_args, cache_dir=cache_dir)
431467

tests/test_cli_compile.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2853,3 +2853,121 @@ def test_raise_error_when_input_and_output_filenames_are_matched(
28532853
f"Error: input and output filenames must not be matched: {req_out_path}"
28542854
)
28552855
assert expected_error in out.stderr.splitlines()
2856+
2857+
2858+
@pytest.mark.parametrize(
2859+
"platform",
2860+
(
2861+
"linux",
2862+
"darwin",
2863+
),
2864+
)
2865+
def test_cross_fetch_top_level(fake_dists, runner, platform):
2866+
"""
2867+
test passing `--override-environment` evaluates top level
2868+
requirements correctly.
2869+
"""
2870+
with open("requirements.in", "w") as req_in:
2871+
req_in.write('small-fake-a==0.1 ; sys_platform == "darwin"\n')
2872+
req_in.write('small-fake-b==0.2 ; sys_platform == "linux"\n')
2873+
2874+
out = runner.invoke(
2875+
cli,
2876+
[
2877+
"--output-file",
2878+
"-",
2879+
"--quiet",
2880+
"--find-links",
2881+
fake_dists,
2882+
"--no-annotate",
2883+
"--no-emit-options",
2884+
"--no-header",
2885+
"--override-environment",
2886+
"sys_platform",
2887+
platform,
2888+
],
2889+
)
2890+
2891+
if platform == "darwin":
2892+
expected_output = dedent(
2893+
"""\
2894+
small-fake-a==0.1 ; sys_platform == "darwin"
2895+
"""
2896+
)
2897+
elif platform == "linux":
2898+
expected_output = dedent(
2899+
"""\
2900+
small-fake-b==0.2 ; sys_platform == "linux"
2901+
"""
2902+
)
2903+
2904+
assert out.exit_code == 0, out
2905+
assert expected_output == out.stdout
2906+
2907+
2908+
@pytest.mark.network
2909+
@pytest.mark.parametrize(
2910+
"platform",
2911+
(
2912+
"linux",
2913+
"darwin",
2914+
),
2915+
)
2916+
def test_cross_fetch_transitive_deps(
2917+
runner, make_package, make_wheel, tmpdir, platform
2918+
):
2919+
"""
2920+
test passing `--override-environment` selects the correct
2921+
transitive dependencies.
2922+
"""
2923+
with open("requirements.in", "w") as req_in:
2924+
req_in.write("package-b\n")
2925+
2926+
package_a = make_package("package-a", version="1.0")
2927+
package_b = make_package(
2928+
"package-b",
2929+
version="1.0",
2930+
install_requires=['package-a ; sys_platform == "darwin"'],
2931+
)
2932+
2933+
dists_dir = tmpdir / "dists"
2934+
for pkg in [package_a, package_b]:
2935+
make_wheel(pkg, dists_dir)
2936+
2937+
out = runner.invoke(
2938+
cli,
2939+
[
2940+
"--output-file",
2941+
"-",
2942+
"--quiet",
2943+
"--find-links",
2944+
dists_dir,
2945+
"--no-annotate",
2946+
"--no-emit-options",
2947+
"--no-header",
2948+
"--override-environment",
2949+
"sys_platform",
2950+
platform,
2951+
],
2952+
)
2953+
2954+
print(out.stdout)
2955+
2956+
expected_output = dedent(
2957+
"""\
2958+
package-b==1.0
2959+
"""
2960+
)
2961+
2962+
if platform == "darwin":
2963+
expected_output = (
2964+
dedent(
2965+
"""\
2966+
package-a==1.0
2967+
"""
2968+
)
2969+
+ expected_output
2970+
)
2971+
2972+
assert out.exit_code == 0, out
2973+
assert expected_output == out.stdout

0 commit comments

Comments
 (0)