Skip to content

Commit

Permalink
Add command pip-check to check locked requirements files
Browse files Browse the repository at this point in the history
  • Loading branch information
lfdebrux committed Feb 19, 2020
1 parent 45fcb39 commit cf44da7
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 1 deletion.
3 changes: 2 additions & 1 deletion piptools/__main__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import click

from piptools.scripts import compile, sync
from piptools.scripts import check, compile, sync


@click.group()
def cli():
pass


cli.add_command(check.cli, "check")
cli.add_command(compile.cli, "compile")
cli.add_command(sync.cli, "sync")

Expand Down
69 changes: 69 additions & 0 deletions piptools/scripts/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# coding: utf-8
from __future__ import absolute_import, print_function, unicode_literals

import os
import sys
import tempfile

from .. import click
from ..lock import check, read_locks
from ..logging import log

DEFAULT_REQUIREMENTS_FILE = "requirements.txt"


@click.command()
@click.version_option()
@click.pass_context
@click.option("-v", "--verbose", count=True, help="Show more output")
@click.option("-q", "--quiet", count=True, help="Give less output")
@click.argument("req_files", nargs=-1, type=click.Path(exists=True, allow_dash=True))
def cli(ctx, verbose, quiet, req_files):
"""Checks whether requirements.txt aligns with requirements.in."""
log.verbosity = verbose - quiet

for req_file in req_files:
if req_file.endswith(".in"):
raise click.BadParameter(
"req_file has the .in extensions, which is most likely an error "
"and will most likely fail the checks. You probably meant to use "
"the corresponding *.txt file?"
)

if len(req_files) == 0:
if os.path.exists(DEFAULT_REQUIREMENTS_FILE):
req_files = (DEFAULT_REQUIREMENTS_FILE,)
else:
raise click.BadParameter(
("If you do not specify an input file, " "the default is {}").format(
DEFAULT_REQUIREMENTS_FILE
)
)

if req_files == ("-",):
# pip requires filenames and not files. Since we want to support
# piping from stdin, we need to briefly save the input from stdin
# to a temporary file and have pip read that.
tmpfile = tempfile.NamedTemporaryFile(mode="wt", delete=False)
tmpfile.write(sys.stdin.read())
tmpfile.flush()

req_files = (tmpfile.name,)

errors = 0

for req_file in req_files:
locks = read_locks(req_file)
if not locks:
log.info("{}: no locks found".format(req_file))
errors += 1
is_okay = check(locks)
if not is_okay:
log.info("{}: lock(s) are out-of-date".format(req_file))
errors += 1

if errors:
log.info("{} errors found. Run pip-compile to fix".format(errors))
sys.exit(1)
else:
log.debug("locks are up-to-date")
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def read_file(filename):
zip_safe=False,
entry_points={
"console_scripts": [
"pip-check = piptools.scripts.check:cli",
"pip-compile = piptools.scripts.compile:cli",
"pip-sync = piptools.scripts.sync:cli",
]
Expand Down
174 changes: 174 additions & 0 deletions tests/test_cli_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import sys

import pytest

from .utils import invoke

from piptools.scripts.check import cli


def test_run_as_module_check():
"""piptools can be run as ``python -m piptools ...``."""

status, output = invoke([sys.executable, "-m", "piptools", "check", "--help"])

# Should have run pip-check successfully.
output = output.decode("utf-8")
assert output.startswith("Usage:")
assert "Checks whether requirements" in output
assert status == 0


def test_exits_successfully_if_lock_up_to_date(tmpdir_cwd, runner):
"""
If lock in requirements.txt is up-to-date pip-check exits with 0
"""
with open("requirements.in", "w") as req_in:
req_in.write("six")
with open("requirements.txt", "w") as req_txt:
req_txt.write(
"# sha256:44778d82365e4af681c40d5f0eef5cf6f5899d3f0ac335050a7ed6779cf3f674"
" requirements.in\n"
)
req_txt.write("six==1.10.0")

out = runner.invoke(cli, ["requirements.txt"])
assert out.exit_code == 0


def test_exits_unsuccessfully_if_lock_not_up_to_date(tmpdir_cwd, runner):
"""
If lock in requirements.txt is not up-to-date pip-check exits with 1
"""
with open("requirements.in", "w") as req_in:
req_in.write("six<1.10.0")
with open("requirements.txt", "w") as req_txt:
req_txt.write(
"# sha256:fe2547fe2604b445e70fc9d819062960552f9145bdb043b51986e478a4806a2b"
" requirements.in\n"
)
req_txt.write("six==1.10.0")

out = runner.invoke(cli, ["requirements.txt"])

assert out.exit_code == 1
assert "requirements.txt: lock(s) are out-of-date" in out.stderr


def test_exits_unsuccessfully_if_lock_not_present(tmpdir_cwd, runner):
"""
If lock not in requirements.txt pip-check exits with 1
"""
with open("requirements.in", "w") as req_in:
req_in.write("six")
with open("requirements.txt", "w") as req_txt:
req_txt.write("six==1.10.0")

out = runner.invoke(cli, ["requirements.txt"])
assert out.exit_code == 1
assert "no locks found" in out.stderr


@pytest.mark.parametrize(
"checksum, expected_exit_code",
(
["44778d82365e4af681c40d5f0eef5cf6f5899d3f0ac335050a7ed6779cf3f674", 0],
["4568992cc2c4736b0bee1179b5a3afe324a99aac2d24b0735ede273326b14220", 1],
["", 1],
),
)
def test_quiet_option(tmpdir_cwd, runner, checksum, expected_exit_code):
"""check command can be run with `--quiet` or `-q` flag."""
with open("requirements.in", "w") as req_in:
req_in.write("six")
with open("requirements.txt", "w") as req_txt:
if checksum:
req_txt.write("# sha256:{} requirements.in\n".format(checksum))
req_txt.write("six==1.10.0")

out = runner.invoke(cli, ["-q"])
assert not out.stderr_bytes
assert out.exit_code == expected_exit_code


def test_no_requirements_file(tmpdir_cwd, runner):
"""
It should raise an error if there are no input files
or a requirements.txt file does not exist.
"""
out = runner.invoke(cli)

assert "If you do not specify an input file" in out.stderr
assert out.exit_code == 2


def test_requirements_file_with_dot_in_extension(tmpdir_cwd, runner):
"""
It should raise an error if some of the input files have .in extension.
"""
with open("requirements.in", "w") as req_in:
req_in.write("six")

out = runner.invoke(cli, ["requirements.in"])

assert "req_file has the .in extension" in out.stderr
assert out.exit_code == 2


@pytest.mark.parametrize(
"checksum, expected_exit_code",
(
["44778d82365e4af681c40d5f0eef5cf6f5899d3f0ac335050a7ed6779cf3f674", 0],
["4568992cc2c4736b0bee1179b5a3afe324a99aac2d24b0735ede273326b14220", 1],
["", 1],
),
)
def test_stdin(tmpdir_cwd, runner, checksum, expected_exit_code):
"""
It can check requirements from stdin
"""
with open("requirements.in", "w") as req_in:
req_in.write("six")

req_txt = (
"# sha256:{} requirements.in\n".format(checksum)
if checksum
else "" "six==1.10.0"
)

out = runner.invoke(cli, ["-"], input=req_txt)

assert out.exit_code == expected_exit_code


@pytest.mark.parametrize(
"checksum, expected_exit_code",
(
["c0862201e27e10f591cc0e3fb8d1dd4f6a4af2559d04a71fb0cb142d59b2f6b7", 0],
["4568992cc2c4736b0bee1179b5a3afe324a99aac2d24b0735ede273326b14220", 1],
["", 1],
),
)
def test_multiple_requirement_files(tmpdir_cwd, runner, checksum, expected_exit_code):
"""
Can check multiple requirements files
"""
with open("req_file1.in", "w") as req_in:
req_in.write("Flask")
with open("req_file2.in", "w") as req_in:
req_in.write("Django")

with open("req_file1.txt", "w") as req_txt:
if checksum:
req_txt.write("# sha256:{} req_file1.in\n".format(checksum))
req_txt.write("Flask==1.1.1")
with open("req_file2.txt", "w") as req_txt:
req_txt.write(
"# sha256:4445d918dfcf1af804b749eeee4835dccfd27c06b6828533be827473ff63439f"
" req_file2.in\n"
)
req_txt.write("Django==2.2.1")

out = runner.invoke(cli, ["req_file1.txt", "req_file2.txt"])

assert out.exit_code == expected_exit_code

0 comments on commit cf44da7

Please sign in to comment.