Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --python-executable option for pip-sync #1333

Merged
merged 38 commits into from
Jun 13, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2fb4874
Support custom python-executable
maratk-ms Feb 27, 2021
5a68984
add tests
maratk-ms Mar 9, 2021
1100dab
Merge remote-tracking branch 'piptools/master' into feature/python-ex…
maratk-ms Mar 9, 2021
1c321cd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 9, 2021
f78866f
Merge commit 'f49b3fa8aa1b5d8c6dda80f719eaae706feff6b0' into feature/…
maratk-ms Mar 16, 2021
a3fd7a5
fix test
maratk-ms Mar 16, 2021
fc4b11e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 16, 2021
7cde62d
fix tests for older python versions
maratk-ms Mar 16, 2021
86ad4cc
Merge branch 'feature/python-executable' of https://github.com/MaratF…
maratk-ms Mar 16, 2021
786671a
code coverage
maratk-ms Mar 16, 2021
c25dc6f
comsetic updates
maratk-ms Mar 16, 2021
9733491
use type=click.Path
maratk-ms Mar 17, 2021
13bb9a6
Merge branch 'master' into feature/python-executable
MaratFM Mar 17, 2021
b01feb5
apply suggestion
maratk-ms Mar 18, 2021
75a13c5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 18, 2021
a5a4bf0
Merge remote-tracking branch 'piptools/master' into feature/python-ex…
maratk-ms Apr 1, 2021
b3e9529
add pip version check and support python executable resolving
maratk-ms Apr 1, 2021
92247e4
add test for invalid pip ver
maratk-ms Apr 1, 2021
d8750ba
fix tests for *nix
maratk-ms Apr 1, 2021
7c7f15b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 1, 2021
9f370e9
Merge branch 'master' into feature/python-executable
MaratFM Apr 12, 2021
3fb2077
Merge remote-tracking branch 'pip-tools/master' into feature/python-e…
maratk-ms Apr 22, 2021
451cc99
Merge branch 'master' into feature/python-executable
MaratFM May 3, 2021
3d20383
Merge remote-tracking branch 'pip-tools/master' into feature/python-e…
maratk-ms May 11, 2021
17cc535
pr comments
maratk-ms May 11, 2021
1e96d26
Merge branch 'master' into feature/python-executable
webknjaz Jun 8, 2021
36667bc
Extract _validate_python_executable, use logging formatting, PEP 257 …
maratk-ms Jun 8, 2021
1d6e2a8
fix mypy warning
maratk-ms Jun 8, 2021
24e2149
fix tests in python 3.6
maratk-ms Jun 8, 2021
86f13b5
Merge branch 'master' into feature/python-executable
ssbarnea Jun 9, 2021
7319cc0
revert log formatting
maratk-ms Jun 9, 2021
5849b41
Merge branch 'feature/python-executable' of https://github.com/MaratF…
maratk-ms Jun 9, 2021
5e8c59d
review comments
maratk-ms Jun 10, 2021
cd26a3a
Update piptools/utils.py
MaratFM Jun 11, 2021
390a150
try no cover
maratk-ms Jun 11, 2021
13eb866
Update piptools/utils.py
MaratFM Jun 12, 2021
cdb6c71
Merge branch 'master' into feature/python-executable
atugushev Jun 12, 2021
a138d07
fix tests
maratk-ms Jun 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions piptools/scripts/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ..exceptions import PipToolsError
from ..logging import log
from ..repositories import PyPIRepository
from ..utils import flat_map
from ..utils import flat_map, get_sys_path_for_python_executable

DEFAULT_REQUIREMENTS_FILE = "requirements.txt"

Expand Down Expand Up @@ -55,6 +55,10 @@
is_flag=True,
help="Ignore package index (only looking at --find-links URLs instead)",
)
@click.option(
"--python-executable",
atugushev marked this conversation as resolved.
Show resolved Hide resolved
atugushev marked this conversation as resolved.
Show resolved Hide resolved
help="Custom python executable path if targeting an environment other than current",
)
@click.option("-v", "--verbose", count=True, help="Show more output")
@click.option("-q", "--quiet", count=True, help="Give less output")
@click.option(
Expand All @@ -77,6 +81,7 @@ def cli(
extra_index_url: Tuple[str, ...],
trusted_host: Tuple[str, ...],
no_index: bool,
python_executable: Optional[str],
verbose: int,
quiet: int,
user_only: bool,
Expand Down Expand Up @@ -108,6 +113,11 @@ def cli(
log.error("ERROR: " + msg)
sys.exit(2)

if python_executable is not None and not os.path.exists(python_executable):
msg = "Python executable {} not found"
log.error(msg.format(python_executable))
sys.exit(2)

install_command = cast(InstallCommand, create_command("install"))
options, _ = install_command.parse_args([])
session = install_command._build_session(options)
Expand All @@ -125,7 +135,13 @@ def cli(
log.error(str(e))
sys.exit(2)

installed_dists = get_installed_distributions(skip=[], user_only=user_only)
paths = None
if python_executable:
paths = get_sys_path_for_python_executable(python_executable)
MaratFM marked this conversation as resolved.
Show resolved Hide resolved

installed_dists = get_installed_distributions(
skip=[], user_only=user_only, paths=paths, local_only=python_executable is None
)
to_install, to_uninstall = sync.diff(merged_requirements, installed_dists)

install_flags = (
Expand All @@ -149,6 +165,7 @@ def cli(
dry_run=dry_run,
install_flags=install_flags,
ask=ask,
python_executable=python_executable,
)
)

Expand Down
7 changes: 5 additions & 2 deletions piptools/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,15 @@ def sync(
dry_run: bool = False,
install_flags: Optional[List[str]] = None,
ask: bool = False,
python_executable: Optional[str] = None,
) -> int:
"""
Install and uninstalls the given sets of modules.
"""
exit_code = 0

python_executable = python_executable or sys.executable

if not to_uninstall and not to_install:
log.info("Everything up-to-date", err=False)
return exit_code
Expand Down Expand Up @@ -216,7 +219,7 @@ def sync(
if to_uninstall:
run( # nosec
[
sys.executable,
python_executable,
"-m",
"pip",
"uninstall",
Expand Down Expand Up @@ -244,7 +247,7 @@ def sync(
try:
run( # nosec
[
sys.executable,
python_executable,
"-m",
"pip",
"install",
Expand Down
17 changes: 17 additions & 0 deletions piptools/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import collections
import itertools
import json
import os
import shlex
import subprocess # nosec
from typing import (
Callable,
Dict,
Iterable,
Iterator,
List,
Optional,
Set,
Tuple,
Expand Down Expand Up @@ -306,3 +310,16 @@ def get_compile_command(click_ctx: click.Context) -> str:
left_args.append(f"{option_long_name}={shlex.quote(str(val))}")

return " ".join(["pip-compile", *sorted(left_args), *sorted(right_args)])


def get_sys_path_for_python_executable(python_executable: str) -> List[str]:
"""
Returns sys.path list for the given python executable.
"""
result = subprocess.check_output( # nosec
[python_executable, "-c", "import sys;import json;print(json.dumps(sys.path))"]
)
paths = json.loads(result)
assert isinstance(paths, list)
assert all(isinstance(i, str) for i in paths)
return [os.path.abspath(path) for path in paths]
75 changes: 75 additions & 0 deletions tests/test_cli_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,78 @@ def test_sync_dry_run_returns_non_zero_exit_code(runner):
out = runner.invoke(cli, ["--dry-run"])

assert out.exit_code == 1


@mock.patch("piptools.scripts.sync.get_sys_path_for_python_executable")
@mock.patch("piptools.scripts.sync.get_installed_distributions")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are too much of monkeypatches. Ideally, there should be only @mock.patch("piptools.sync.run").

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored

@mock.patch("piptools.sync.run")
def test_python_executable_option(
run,
get_installed_distributions,
get_sys_path_for_python_executable,
runner,
fake_dist,
):
"""
Make sure sync command can run with `--python-executable` option
atugushev marked this conversation as resolved.
Show resolved Hide resolved
"""
with open("requirements.txt", "w") as req_in:
req_in.write("small-fake-a==1.10.0")

custom_executable = "custom_executable"
with open(custom_executable, "w") as exec_file:
exec_file.write("")

sys_paths = ["", "./"]
get_sys_path_for_python_executable.return_value = sys_paths
get_installed_distributions.return_value = [fake_dist("django==1.8")]

runner.invoke(cli, ["--python-executable", custom_executable])

get_installed_distributions.assert_called_once_with(
skip=[], user_only=False, paths=sys_paths, local_only=False
)

assert run.call_count == 2

call_args = [call[0][0] for call in run.call_args_list]
called_uninstall_options = [args for args in call_args if args[3] == "uninstall"]
called_install_options = [args[:-1] for args in call_args if args[3] == "install"]

assert called_uninstall_options == [
[custom_executable, "-m", "pip", "uninstall", "-y", "django"]
]
assert called_install_options == [[custom_executable, "-m", "pip", "install", "-r"]]


def test_invalid_python_executable(runner):
with open("requirements.txt", "w") as req_in:
req_in.write("small-fake-a==1.10.0")

out = runner.invoke(cli, ["--python-executable", "/tmp/invalid_executable"])
assert out.exit_code == 2, out


@mock.patch("piptools.sync.run")
def test_default_python_executable_option(run, runner):
"""
Make sure sys.executable is used when --python-executable is not provided
"""
with open("requirements.txt", "w") as req_in:
req_in.write("small-fake-a==1.10.0")

runner.invoke(cli)

assert run.call_count == 2

call_args = [call[0][0] for call in run.call_args_list]
called_install_options = [args[:-1] for args in call_args if args[3] == "install"]
assert called_install_options == [
[
sys.executable,
"-m",
"pip",
"install",
"-r",
]
]
9 changes: 9 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import operator
import os
import shlex
import sys

import pytest

Expand All @@ -14,6 +15,7 @@
format_specifier,
get_compile_command,
get_hashes_from_ireq,
get_sys_path_for_python_executable,
is_pinned_requirement,
is_url_requirement,
lookup_table,
Expand Down Expand Up @@ -367,3 +369,10 @@ def test_lookup_table_from_tuples_with_empty_values():

def test_lookup_table_with_empty_values():
assert lookup_table((), operator.itemgetter(0)) == {}


def test_get_sys_path_for_python_executable():
result = get_sys_path_for_python_executable(sys.executable)
# not testing for equality, because pytest adds extra paths into current sys.path
for path in result:
atugushev marked this conversation as resolved.
Show resolved Hide resolved
assert path in sys.path