Skip to content

Commit

Permalink
Merge pull request #1477 from AndydeCleyre/bugfix/1306
Browse files Browse the repository at this point in the history
Enable single-line annotations with `pip-compile --annotation-style=line`
  • Loading branch information
atugushev committed Sep 20, 2021
2 parents f1ee721 + b6162c6 commit e548bdf
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 23 deletions.
8 changes: 8 additions & 0 deletions piptools/scripts/compile.py
Expand Up @@ -123,6 +123,12 @@ def _get_default_option(option_name: str) -> Any:
default=True,
help="Annotate results, indicating where dependencies come from",
)
@click.option(
"--annotation-style",
type=click.Choice(("line", "split")),
default="split",
help="Choose the format of annotation comments",
)
@click.option(
"-U",
"--upgrade/--no-upgrade",
Expand Down Expand Up @@ -244,6 +250,7 @@ def cli(
header: bool,
emit_trusted_host: bool,
annotate: bool,
annotation_style: str,
upgrade: bool,
upgrade_packages: Tuple[str, ...],
output_file: Union[LazyFile, IO[Any], None],
Expand Down Expand Up @@ -471,6 +478,7 @@ def cli(
emit_index_url=emit_index_url,
emit_trusted_host=emit_trusted_host,
annotate=annotate,
annotation_style=annotation_style,
strip_extras=strip_extras,
generate_hashes=generate_hashes,
default_index_url=repository.DEFAULT_INDEX_URL,
Expand Down
40 changes: 30 additions & 10 deletions piptools/writer.py
Expand Up @@ -51,6 +51,23 @@ def _comes_from_as_string(ireq: InstallRequirement) -> str:
return key_from_ireq(ireq.comes_from)


def annotation_style_split(required_by: Set[str]) -> str:
sorted_required_by = sorted(required_by)
if len(sorted_required_by) == 1:
source = sorted_required_by[0]
annotation = "# via " + source
else:
annotation_lines = ["# via"]
for source in sorted_required_by:
annotation_lines.append(" # " + source)
annotation = "\n".join(annotation_lines)
return annotation


def annotation_style_line(required_by: Set[str]) -> str:
return f"# via {', '.join(sorted(required_by))}"


class OutputWriter:
def __init__(
self,
Expand All @@ -61,6 +78,7 @@ def __init__(
emit_index_url: bool,
emit_trusted_host: bool,
annotate: bool,
annotation_style: str,
strip_extras: bool,
generate_hashes: bool,
default_index_url: str,
Expand All @@ -79,6 +97,7 @@ def __init__(
self.emit_index_url = emit_index_url
self.emit_trusted_host = emit_trusted_host
self.annotate = annotate
self.annotation_style = annotation_style
self.strip_extras = strip_extras
self.generate_hashes = generate_hashes
self.default_index_url = default_index_url
Expand Down Expand Up @@ -258,15 +277,16 @@ def _format_requirement(
required_by.add(_comes_from_as_string(ireq))

if required_by:
sorted_required_by = sorted(required_by)
if len(sorted_required_by) == 1:
source = sorted_required_by[0]
annotation = " # via " + source
else:
annotation_lines = [" # via"]
for source in sorted_required_by:
annotation_lines.append(" # " + source)
annotation = "\n".join(annotation_lines)
line = f"{line}\n{comment(annotation)}"
if self.annotation_style == "split":
annotation = annotation_style_split(required_by)
sep = "\n "
elif self.annotation_style == "line":
annotation = annotation_style_line(required_by)
sep = "\n " if ireq_hashes else " "
else: # pragma: no cover
raise ValueError("Invalid value for annotation style")
# 24 is one reasonable column size to use here, that we've used in the past
lines = f"{line:24}{sep}{comment(annotation)}".splitlines()
line = "\n".join(ln.rstrip() for ln in lines)

return line
80 changes: 72 additions & 8 deletions tests/test_cli_compile.py
Expand Up @@ -891,7 +891,7 @@ def test_generate_hashes_with_annotations(runner):


@pytest.mark.network
def test_generate_hashes_with_long_annotations(runner):
def test_generate_hashes_with_split_style_annotations(runner):
with open("requirements.in", "w") as fp:
fp.write("Django==1.11.29\n")
fp.write("django-debug-toolbar==1.11\n")
Expand All @@ -900,7 +900,7 @@ def test_generate_hashes_with_long_annotations(runner):
fp.write("pytz==2020.4\n")
fp.write("sqlparse==0.3.1\n")

out = runner.invoke(cli, ["--generate-hashes"])
out = runner.invoke(cli, ["--generate-hashes", "--annotation-style", "split"])
assert out.stderr == dedent(
f"""\
#
Expand Down Expand Up @@ -946,6 +946,54 @@ def test_generate_hashes_with_long_annotations(runner):
)


@pytest.mark.network
def test_generate_hashes_with_line_style_annotations(runner):
with open("requirements.in", "w") as fp:
fp.write("Django==1.11.29\n")
fp.write("django-debug-toolbar==1.11\n")
fp.write("django-storages==1.9.1\n")
fp.write("django-taggit==0.24.0\n")
fp.write("pytz==2020.4\n")
fp.write("sqlparse==0.3.1\n")

out = runner.invoke(cli, ["--generate-hashes", "--annotation-style", "line"])
assert out.stderr == dedent(
f"""\
#
# This file is autogenerated by pip-compile with python \
{sys.version_info.major}.{sys.version_info.minor}
# To update, run:
#
# pip-compile --annotation-style=line --generate-hashes
#
django==1.11.29 \\
--hash=sha256:014e3392058d94f40569206a24523ce254d55ad2f9f46c6550b0fe2e4f94cf3f \\
--hash=sha256:4200aefb6678019a0acf0005cd14cfce3a5e6b9b90d06145fcdd2e474ad4329c
# via -r requirements.in, django-debug-toolbar, django-storages, django-taggit
django-debug-toolbar==1.11 \\
--hash=sha256:89d75b60c65db363fb24688d977e5fbf0e73386c67acf562d278402a10fc3736 \\
--hash=sha256:c2b0134119a624f4ac9398b44f8e28a01c7686ac350a12a74793f3dd57a9eea0
# via -r requirements.in
django-storages==1.9.1 \\
--hash=sha256:3103991c2ee8cef8a2ff096709973ffe7106183d211a79f22cf855f33533d924 \\
--hash=sha256:a59e9923cbce7068792f75344ed7727021ee4ac20f227cf17297d0d03d141e91
# via -r requirements.in
django-taggit==0.24.0 \\
--hash=sha256:710b4d15ec1996550cc68a0abbc41903ca7d832540e52b1336e6858737e410d8 \\
--hash=sha256:bb8f27684814cd1414b2af75b857b5e26a40912631904038a7ecacd2bfafc3ac
# via -r requirements.in
pytz==2020.4 \\
--hash=sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268 \\
--hash=sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd
# via -r requirements.in, django
sqlparse==0.3.1 \\
--hash=sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e \\
--hash=sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548
# via -r requirements.in, django-debug-toolbar
"""
)


def test_filter_pip_markers(pip_conf, runner):
"""
Check that pip-compile works with pip environment markers (PEP496)
Expand Down Expand Up @@ -1078,10 +1126,10 @@ def test_multiple_input_files_without_output_file(runner):


@pytest.mark.parametrize(
("option", "expected"),
("options", "expected"),
(
pytest.param(
"--annotate",
("--annotate",),
f"""\
#
# This file is autogenerated by pip-compile with python \
Expand All @@ -1101,7 +1149,23 @@ def test_multiple_input_files_without_output_file(runner):
id="annotate",
),
pytest.param(
"--no-annotate",
("--annotate", "--annotation-style", "line"),
f"""\
#
# This file is autogenerated by pip-compile with python \
{sys.version_info.major}.{sys.version_info.minor}
# To update, run:
#
# pip-compile --annotation-style=line --no-emit-find-links
#
small-fake-a==0.1 # via -c constraints.txt, small-fake-with-deps
small-fake-with-deps==0.1 # via -r requirements.in
Dry-run, so nothing updated.
""",
id="annotate line style",
),
pytest.param(
("--no-annotate",),
f"""\
#
# This file is autogenerated by pip-compile with python \
Expand All @@ -1118,17 +1182,17 @@ def test_multiple_input_files_without_output_file(runner):
),
),
)
def test_annotate_option(pip_conf, runner, option, expected):
def test_annotate_option(pip_conf, runner, options, expected):
"""
The output lines has have annotations if option is turned on.
The output lines have annotations if the option is turned on.
"""
with open("constraints.txt", "w") as constraints_in:
constraints_in.write("small-fake-a==0.1")
with open("requirements.in", "w") as req_in:
req_in.write("-c constraints.txt\n")
req_in.write("small_fake_with_deps")

out = runner.invoke(cli, [option, "-n", "--no-emit-find-links"])
out = runner.invoke(cli, [*options, "-n", "--no-emit-find-links"])

assert out.stderr == dedent(expected)
assert out.exit_code == 0
Expand Down
11 changes: 6 additions & 5 deletions tests/test_writer.py
Expand Up @@ -36,6 +36,7 @@ def writer(tmpdir_cwd):
emit_index_url=True,
emit_trusted_host=True,
annotate=True,
annotation_style="split",
generate_hashes=False,
default_index_url=None,
index_urls=[],
Expand All @@ -57,37 +58,37 @@ def test_format_requirement_annotation_editable(from_editable, writer):

assert writer._format_requirement(
ireq
) == "-e git+git://fake.org/x/y.git#egg=y\n" + comment(" # via xyz")
) == "-e git+git://fake.org/x/y.git#egg=y\n " + comment("# via xyz")


def test_format_requirement_annotation(from_line, writer):
ireq = from_line("test==1.2")
ireq.comes_from = "xyz"

assert writer._format_requirement(ireq) == "test==1.2\n" + comment(" # via xyz")
assert writer._format_requirement(ireq) == "test==1.2\n " + comment("# via xyz")


def test_format_requirement_annotation_lower_case(from_line, writer):
ireq = from_line("Test==1.2")
ireq.comes_from = "xyz"

assert writer._format_requirement(ireq) == "test==1.2\n" + comment(" # via xyz")
assert writer._format_requirement(ireq) == "test==1.2\n " + comment("# via xyz")


def test_format_requirement_for_primary(from_line, writer):
"Primary packages should get annotated."
ireq = from_line("test==1.2")
ireq.comes_from = "xyz"

assert writer._format_requirement(ireq) == "test==1.2\n" + comment(" # via xyz")
assert writer._format_requirement(ireq) == "test==1.2\n " + comment("# via xyz")


def test_format_requirement_for_primary_lower_case(from_line, writer):
"Primary packages should get annotated."
ireq = from_line("Test==1.2")
ireq.comes_from = "xyz"

assert writer._format_requirement(ireq) == "test==1.2\n" + comment(" # via xyz")
assert writer._format_requirement(ireq) == "test==1.2\n " + comment("# via xyz")


def test_format_requirement_environment_marker(from_line, writer):
Expand Down

0 comments on commit e548bdf

Please sign in to comment.