Skip to content

Commit

Permalink
[Feature] Allow Environment Override
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
avi-jois committed Apr 6, 2023
1 parent e8ab3b9 commit 583c101
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 1 deletion.
8 changes: 7 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ etc.). For an exact definition, refer to the possible combinations of `PEP 508
environment markers`_.

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

There is a feature (`--override-environment`) that can be used to
specify the environment when gathering dependencies, allowing for cross-environment
fetching. However, a different ``requirements.txt`` must still be generated per
environment. It is recommended to override all keys in `PEP 508` when targetting a
different environment so the environment is fully defined.

.. _PEP 508 environment markers: https://www.python.org/dev/peps/pep-0508/#environment-markers

Other useful tools
Expand Down
36 changes: 36 additions & 0 deletions piptools/scripts/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,13 @@ def _determine_linesep(
help="Specify a package to consider unsafe; may be used more than once. "
f"Replaces default unsafe packages: {', '.join(sorted(UNSAFE_PACKAGES))}",
)
@click.option(
"--override-environment",
multiple=True,
type=(str, str),
help="Specify an environment marker to override."
"This can be used to fetch requirements for a different platform",
)
def cli(
ctx: click.Context,
verbose: int,
Expand Down Expand Up @@ -339,6 +346,7 @@ def cli(
emit_index_url: bool,
emit_options: bool,
unsafe_package: tuple[str, ...],
override_environment: tuple[tuple[str, str], ...],
) -> None:
"""
Compiles requirements.txt from requirements.in, pyproject.toml, setup.cfg,
Expand Down Expand Up @@ -426,6 +434,34 @@ def cli(
pip_args.extend(["--use-deprecated", "legacy-resolver"])
pip_args.extend(right_args)

env_dict = dict(override_environment)
if len(env_dict) > 0:
# Since the environment is overriden globally, handle it here in the
# top level instead of within the resolver.
import pip._vendor.packaging.markers

default_env = pip._vendor.packaging.markers.default_environment()

def overriden_environment() -> dict[str, str]:
return {
k: env_dict.get(k, default_env[k])
for k in [
"implementation_name",
"implementation_version",
"os_name",
"platform_machine",
"platform_release",
"platform_system",
"platform_version",
"python_full_version",
"platform_python_implementation",
"python_version",
"sys_platform",
]
}

pip._vendor.packaging.markers.default_environment = overriden_environment

repository: BaseRepository
repository = PyPIRepository(pip_args, cache_dir=cache_dir)

Expand Down
118 changes: 118 additions & 0 deletions tests/test_cli_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2853,3 +2853,121 @@ def test_raise_error_when_input_and_output_filenames_are_matched(
f"Error: input and output filenames must not be matched: {req_out_path}"
)
assert expected_error in out.stderr.splitlines()


@pytest.mark.parametrize(
"platform",
(
"linux",
"darwin",
),
)
def test_cross_fetch_top_level(fake_dists, runner, platform):
"""
test passing `--override-environment` evaluates top level
requirements correctly.
"""
with open("requirements.in", "w") as req_in:
req_in.write('small-fake-a==0.1 ; sys_platform == "darwin"\n')
req_in.write('small-fake-b==0.2 ; sys_platform == "linux"\n')

out = runner.invoke(
cli,
[
"--output-file",
"-",
"--quiet",
"--find-links",
fake_dists,
"--no-annotate",
"--no-emit-options",
"--no-header",
"--override-environment",
"sys_platform",
platform,
],
)

if platform == "darwin":
expected_output = dedent(
"""\
small-fake-a==0.1 ; sys_platform == "darwin"
"""
)
elif platform == "linux":
expected_output = dedent(
"""\
small-fake-b==0.2 ; sys_platform == "linux"
"""
)

assert out.exit_code == 0, out
assert expected_output == out.stdout


@pytest.mark.network
@pytest.mark.parametrize(
"platform",
(
"linux",
"darwin",
),
)
def test_cross_fetch_transitive_deps(
runner, make_package, make_wheel, tmpdir, platform
):
"""
test passing `--override-environment` selects the correct
transitive dependencies.
"""
with open("requirements.in", "w") as req_in:
req_in.write("package-b\n")

package_a = make_package("package-a", version="1.0")
package_b = make_package(
"package-b",
version="1.0",
install_requires=['package-a ; sys_platform == "darwin"'],
)

dists_dir = tmpdir / "dists"
for pkg in [package_a, package_b]:
make_wheel(pkg, dists_dir)

out = runner.invoke(
cli,
[
"--output-file",
"-",
"--quiet",
"--find-links",
dists_dir,
"--no-annotate",
"--no-emit-options",
"--no-header",
"--override-environment",
"sys_platform",
platform,
],
)

print(out.stdout)

expected_output = dedent(
"""\
package-b==1.0
"""
)

if platform == "darwin":
expected_output = (
dedent(
"""\
package-a==1.0
"""
)
+ expected_output
)

assert out.exit_code == 0, out
assert expected_output == out.stdout

0 comments on commit 583c101

Please sign in to comment.