Skip to content

Commit bdfca5b

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 09d4816 commit bdfca5b

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
@@ -520,7 +520,7 @@ etc.). For an exact definition, refer to the possible combinations of `PEP 508
520520
environment markers`_.
521521

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

535+
There is a feature (`--override-environment`) that can be used to
536+
specify the environment when gathering dependencies, allowing for cross-environment
537+
fetching. However, a different ``requirements.txt`` must still be generated per
538+
environment. It is recommended to override all keys in `PEP 508` when targetting a
539+
different environment so the environment is fully defined.
540+
535541
.. _PEP 508 environment markers: https://www.python.org/dev/peps/pep-0508/#environment-markers
536542

537543
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,
@@ -428,6 +436,34 @@ def cli(
428436
pip_args.extend(["--cache-dir", cache_dir])
429437
pip_args.extend(right_args)
430438

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

tests/test_cli_compile.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2868,3 +2868,121 @@ def test_pass_pip_cache_to_pip_args(tmpdir, runner, current_resolver):
28682868
)
28692869
assert out.exit_code == 0
28702870
assert os.listdir(os.path.join(str(cache_dir), "http"))
2871+
2872+
2873+
@pytest.mark.parametrize(
2874+
"platform",
2875+
(
2876+
"linux",
2877+
"darwin",
2878+
),
2879+
)
2880+
def test_cross_fetch_top_level(fake_dists, runner, platform):
2881+
"""
2882+
test passing `--override-environment` evaluates top level
2883+
requirements correctly.
2884+
"""
2885+
with open("requirements.in", "w") as req_in:
2886+
req_in.write('small-fake-a==0.1 ; sys_platform == "darwin"\n')
2887+
req_in.write('small-fake-b==0.2 ; sys_platform == "linux"\n')
2888+
2889+
out = runner.invoke(
2890+
cli,
2891+
[
2892+
"--output-file",
2893+
"-",
2894+
"--quiet",
2895+
"--find-links",
2896+
fake_dists,
2897+
"--no-annotate",
2898+
"--no-emit-options",
2899+
"--no-header",
2900+
"--override-environment",
2901+
"sys_platform",
2902+
platform,
2903+
],
2904+
)
2905+
2906+
if platform == "darwin":
2907+
expected_output = dedent(
2908+
"""\
2909+
small-fake-a==0.1 ; sys_platform == "darwin"
2910+
"""
2911+
)
2912+
elif platform == "linux":
2913+
expected_output = dedent(
2914+
"""\
2915+
small-fake-b==0.2 ; sys_platform == "linux"
2916+
"""
2917+
)
2918+
2919+
assert out.exit_code == 0, out
2920+
assert expected_output == out.stdout
2921+
2922+
2923+
@pytest.mark.network
2924+
@pytest.mark.parametrize(
2925+
"platform",
2926+
(
2927+
"linux",
2928+
"darwin",
2929+
),
2930+
)
2931+
def test_cross_fetch_transitive_deps(
2932+
runner, make_package, make_wheel, tmpdir, platform
2933+
):
2934+
"""
2935+
test passing `--override-environment` selects the correct
2936+
transitive dependencies.
2937+
"""
2938+
with open("requirements.in", "w") as req_in:
2939+
req_in.write("package-b\n")
2940+
2941+
package_a = make_package("package-a", version="1.0")
2942+
package_b = make_package(
2943+
"package-b",
2944+
version="1.0",
2945+
install_requires=['package-a ; sys_platform == "darwin"'],
2946+
)
2947+
2948+
dists_dir = tmpdir / "dists"
2949+
for pkg in [package_a, package_b]:
2950+
make_wheel(pkg, dists_dir)
2951+
2952+
out = runner.invoke(
2953+
cli,
2954+
[
2955+
"--output-file",
2956+
"-",
2957+
"--quiet",
2958+
"--find-links",
2959+
dists_dir,
2960+
"--no-annotate",
2961+
"--no-emit-options",
2962+
"--no-header",
2963+
"--override-environment",
2964+
"sys_platform",
2965+
platform,
2966+
],
2967+
)
2968+
2969+
print(out.stdout)
2970+
2971+
expected_output = dedent(
2972+
"""\
2973+
package-b==1.0
2974+
"""
2975+
)
2976+
2977+
if platform == "darwin":
2978+
expected_output = (
2979+
dedent(
2980+
"""\
2981+
package-a==1.0
2982+
"""
2983+
)
2984+
+ expected_output
2985+
)
2986+
2987+
assert out.exit_code == 0, out
2988+
assert expected_output == out.stdout

0 commit comments

Comments
 (0)