Repository: jazzband/pip-tools

Issue:
--index/--no-index flag means different things in pip-compile and pip-sync
`--no-index` is an existing `pip` internal flag. It does the same thing on `pip-sync` (ignore default pypi url), and something different on `pip-compile` (don't annotate index url into `requirements.txt`).

**Proposal: Change `--index/--no-index` in `pip-compile` to `--annotate-index/--no-annotate-index`.**

Achieves:
- Consistency (between `pip-sync` and `pip-compile`)
- Consistency (with `pip --no-index`)
- Consistency (with other flag `--annotate/--no-annotate`)



Discussion:
My initial thought on this was to make the change if we were to make a major release.
Now that it's happening, while I would also prefer it to be consistent, I feel it isn't worth it to make an additional breaking change for aesthetic reasons.

If others feels like this is really irritating and should really be changed, speak up. I just don't want to make this change if there's no one else that want it changed.

@davidovich @jdufresne @suutari / @suutari-ai If you would like to chime-in as common contributors
`--index` & `--no-index` could continue to exist as aliases that also issue a warning when used. That way, existing code won't break. The hope is people will notice a need to change before an eventual backwards compatibility breakage.
Yeah, I agree with @jdufresne. Introducing the new options and deprecating the old ones would be the nicest first step.
Good idea, thanks for the input!
I'd prefer `--emit-index-url / --no-emit-index-url` to be consistent with `--emit-trusted-host / --no-emit-trusted-host`.
There is a PR #1130 which resolves the issue, any feedback is appreciated!

Resolving patch:
diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py
--- a/piptools/scripts/compile.py
+++ b/piptools/scripts/compile.py
@@ -5,7 +5,9 @@
 import shlex
 import sys
 import tempfile
+import warnings
 
+from click import Command
 from click.utils import safecall
 from pip._internal.commands import create_command
 from pip._internal.req.constructors import install_req_from_line
@@ -36,7 +38,28 @@ def _get_default_option(option_name):
     return getattr(default_values, option_name)
 
 
-@click.command()
+class BaseCommand(Command):
+    _os_args = None
+
+    def parse_args(self, ctx, args):
+        """
+        Override base `parse_args` to store the argument part of `sys.argv`.
+        """
+        self._os_args = set(args)
+        return super(BaseCommand, self).parse_args(ctx, args)
+
+    def has_arg(self, arg_name):
+        """
+        Detect whether a given arg name (including negative counterparts
+        to the arg, e.g. --no-arg) is present in the argument part of `sys.argv`.
+        """
+        command_options = {option.name: option for option in self.params}
+        option = command_options[arg_name]
+        args = set(option.opts + option.secondary_opts)
+        return bool(self._os_args & args)
+
+
+@click.command(cls=BaseCommand)
 @click.version_option()
 @click.pass_context
 @click.option("-v", "--verbose", count=True, help="Show more output")
@@ -104,7 +127,7 @@ def _get_default_option(option_name):
     "--index/--no-index",
     is_flag=True,
     default=True,
-    help="Add index URL to generated file",
+    help="DEPRECATED: Add index URL to generated file",
 )
 @click.option(
     "--emit-trusted-host/--no-emit-trusted-host",
@@ -188,6 +211,12 @@ def _get_default_option(option_name):
     type=click.Path(file_okay=False, writable=True),
 )
 @click.option("--pip-args", help="Arguments to pass directly to the pip command.")
+@click.option(
+    "--emit-index-url/--no-emit-index-url",
+    is_flag=True,
+    default=True,
+    help="Add index URL to generated file",
+)
 def cli(
     ctx,
     verbose,
@@ -216,6 +245,7 @@ def cli(
     emit_find_links,
     cache_dir,
     pip_args,
+    emit_index_url,
 ):
     """Compiles requirements.txt from requirements.in specs."""
     log.verbosity = verbose - quiet
@@ -255,6 +285,19 @@ def cli(
         # Close the file at the end of the context execution
         ctx.call_on_close(safecall(output_file.close_intelligently))
 
+    if cli.has_arg("index") and cli.has_arg("emit_index_url"):
+        raise click.BadParameter(
+            "--index/--no-index and --emit-index-url/--no-emit-index-url "
+            "are mutually exclusive."
+        )
+    elif cli.has_arg("index"):
+        warnings.warn(
+            "--index and --no-index are deprecated and will be removed "
+            "in future versions. Use --emit-index-url/--no-emit-index-url instead.",
+            category=FutureWarning,
+        )
+        emit_index_url = index
+
     ###
     # Setup
     ###
@@ -419,7 +462,7 @@ def cli(
         click_ctx=ctx,
         dry_run=dry_run,
         emit_header=header,
-        emit_index=index,
+        emit_index_url=emit_index_url,
         emit_trusted_host=emit_trusted_host,
         annotate=annotate,
         generate_hashes=generate_hashes,
diff --git a/piptools/writer.py b/piptools/writer.py
--- a/piptools/writer.py
+++ b/piptools/writer.py
@@ -56,7 +56,7 @@ def __init__(
         click_ctx,
         dry_run,
         emit_header,
-        emit_index,
+        emit_index_url,
         emit_trusted_host,
         annotate,
         generate_hashes,
@@ -73,7 +73,7 @@ def __init__(
         self.click_ctx = click_ctx
         self.dry_run = dry_run
         self.emit_header = emit_header
-        self.emit_index = emit_index
+        self.emit_index_url = emit_index_url
         self.emit_trusted_host = emit_trusted_host
         self.annotate = annotate
         self.generate_hashes = generate_hashes
@@ -101,7 +101,7 @@ def write_header(self):
             yield comment("#")
 
     def write_index_options(self):
-        if self.emit_index:
+        if self.emit_index_url:
             for index, index_url in enumerate(dedup(self.index_urls)):
                 if index_url.rstrip("/") == self.default_index_url:
                     continue


Test patch:
diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py
--- a/tests/test_cli_compile.py
+++ b/tests/test_cli_compile.py
@@ -1,3 +1,4 @@
+import itertools
 import os
 import subprocess
 import sys
@@ -176,20 +177,64 @@ def test_trusted_host(pip_conf, runner):
     assert "--trusted-host example.com\n" "--trusted-host example2.com\n" in out.stderr
 
 
-def test_trusted_host_no_emit(pip_conf, runner):
+@pytest.mark.parametrize(
+    "options",
+    (
+        pytest.param(
+            ["--trusted-host", "example.com", "--no-emit-trusted-host"],
+            id="trusted host",
+        ),
+        pytest.param(
+            ["--find-links", "wheels", "--no-emit-find-links"], id="find links"
+        ),
+        pytest.param(
+            ["--index-url", "https://index-url", "--no-emit-index-url"], id="index url"
+        ),
+    ),
+)
+def test_all_no_emit_options(runner, options):
+    with open("requirements.in", "w"):
+        pass
+    out = runner.invoke(cli, ["--no-header"] + options)
+    assert out.stderr.strip().splitlines() == []
+
+
+@pytest.mark.parametrize(
+    ("option", "expected_output"),
+    (
+        pytest.param("--index", ["--index-url https://index-url"], id="index url"),
+        pytest.param("--no-index", [], id="no index"),
+    ),
+)
+def test_index_option(runner, option, expected_output):
     with open("requirements.in", "w"):
         pass
-    out = runner.invoke(
-        cli, ["-v", "--trusted-host", "example.com", "--no-emit-trusted-host"]
-    )
-    assert "--trusted-host example.com" not in out.stderr
 
+    with pytest.warns(FutureWarning, match="--index and --no-index are deprecated"):
+        out = runner.invoke(
+            cli, ["--no-header", "--index-url", "https://index-url", option]
+        )
+
+    assert out.stderr.strip().splitlines() == expected_output
 
-def test_find_links_no_emit(pip_conf, runner):
+
+@pytest.mark.parametrize(
+    "options",
+    itertools.product(
+        ("--index", "--no-index"), ("--emit-index-url", "--no-emit-index-url")
+    ),
+)
+def test_mutual_exclusive_index_options(runner, options):
     with open("requirements.in", "w"):
         pass
-    out = runner.invoke(cli, ["-v", "--no-emit-find-links"])
-    assert "--find-links" not in out.stderr
+
+    out = runner.invoke(cli, options)
+
+    assert out.exit_code == 2
+    assert (
+        "--index/--no-index and --emit-index-url/--no-emit-index-url "
+        "are mutually exclusive"
+    ) in out.stderr
 
 
 @pytest.mark.network
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -247,12 +247,14 @@ def test_force_text(value, expected_text):
         (["--allow-unsafe"], "pip-compile --allow-unsafe"),
         # Check negative flags
         (["--no-index"], "pip-compile --no-index"),
+        (["--no-emit-index-url"], "pip-compile --no-emit-index-url"),
         (["--no-emit-trusted-host"], "pip-compile --no-emit-trusted-host"),
         (["--no-annotate"], "pip-compile --no-annotate"),
         # Check that default values will be removed from the command
         (["--emit-trusted-host"], "pip-compile"),
         (["--annotate"], "pip-compile"),
         (["--index"], "pip-compile"),
+        (["--emit-index-url"], "pip-compile"),
         (["--max-rounds=10"], "pip-compile"),
         (["--build-isolation"], "pip-compile"),
         # Check options with multiple values
@@ -337,7 +339,7 @@ def test_get_compile_command_sort_args(tmpdir_cwd):
         pass
 
     args = [
-        "--no-index",
+        "--no-emit-index-url",
         "--no-emit-trusted-host",
         "--no-annotate",
         "setup.py",
@@ -350,6 +352,6 @@ def test_get_compile_command_sort_args(tmpdir_cwd):
     with compile_cli.make_context("pip-compile", args) as ctx:
         assert get_compile_command(ctx) == (
             "pip-compile --find-links=bar --find-links=foo "
-            "--no-annotate --no-emit-trusted-host --no-index "
+            "--no-annotate --no-emit-index-url --no-emit-trusted-host "
             "requirements.in setup.py"
         )
diff --git a/tests/test_writer.py b/tests/test_writer.py
--- a/tests/test_writer.py
+++ b/tests/test_writer.py
@@ -31,7 +31,7 @@ def writer(tmpdir_cwd):
             click_ctx=ctx,
             dry_run=True,
             emit_header=True,
-            emit_index=True,
+            emit_index_url=True,
             emit_trusted_host=True,
             annotate=True,
             generate_hashes=False,
@@ -318,9 +318,10 @@ def test_write_index_options(writer, index_urls, expected_lines):
 
 def test_write_index_options_no_emit_index(writer):
     """
-    There should not be --index-url/--extra-index-url options if emit_index is False.
+    There should not be --index-url/--extra-index-url options
+    if emit_index_url is False.
     """
-    writer.emit_index = False
+    writer.emit_index_url = False
     with pytest.raises(StopIteration):
         next(writer.write_index_options())