Repository: jazzband/pip-tools

Issue:
Add support for `pythonX.Y -m piptools ...`.
I work with both Python 2.7 and Python 3.5 regularly.  The dependency resolution process uses the Python version in multiple different ways and I'm afraid that running with both might not give the same result depending on the Python version.

For this reason, I don't have a global alias to `pip-compile` _et al._.  I could of course use a virtualenv just for running piptools, but that's quite inconvenient (especially for projects that don't use Tox).   What I do have is a global alias to `pythonX.Y` (Linux) and `py -X.Y` (Windows).

The most convenient way to use pip-tools in this context is to support the `python -m piptools ...` CLI syntax, which requires a `piptools.__main__` module.

That way, when I work on a Python 2.7 project:

```
py -2.7 -m piptools compile requirements.txt  # compiles for Python 2.7
```

And when I switch to a Python 3.5 project:

```
py -3.5 -m piptools compile requirements.txt  # compiles for Python 3.5
```

Are you open to such a change if I'm willing to put some time into it?

Thanks!



Discussion:
This would be great. If anyone wants to contribute this, I'd encourage that.


Resolving patch:
diff --git a/piptools/__main__.py b/piptools/__main__.py
new file mode 100644
--- /dev/null
+++ b/piptools/__main__.py
@@ -0,0 +1,16 @@
+import click
+from piptools.scripts import compile, sync
+
+
+@click.group()
+def cli():
+    pass
+
+
+cli.add_command(compile.cli, 'compile')
+cli.add_command(sync.cli, 'sync')
+
+
+# Enable ``python -m piptools ...``.
+if __name__ == '__main__':  # pragma: no cover
+    cli()
diff --git a/piptools/scripts/sync.py b/piptools/scripts/sync.py
--- a/piptools/scripts/sync.py
+++ b/piptools/scripts/sync.py
@@ -28,6 +28,7 @@
 @click.option('--no-index', is_flag=True, help="Ignore package index (only looking at --find-links URLs instead)")
 @click.argument('src_files', required=False, type=click.Path(exists=True), nargs=-1)
 def cli(dry_run, force, find_links, index_url, extra_index_url, no_index, src_files):
+    """Synchronize virtual environment with requirements.txt."""
     if not src_files:
         if os.path.exists(DEFAULT_REQUIREMENTS_FILE):
             src_files = (DEFAULT_REQUIREMENTS_FILE,)


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,6 +1,7 @@
 import os
 from textwrap import dedent
 import subprocess
+import sys
 
 from click.testing import CliRunner
 
@@ -125,3 +126,46 @@ def test_realistic_complex_sub_dependencies(tmpdir):
 
         print(out.output)
         assert out.exit_code == 0
+
+
+def invoke(command):
+    """Invoke sub-process."""
+    try:
+        output = subprocess.check_output(
+            command,
+            stderr=subprocess.STDOUT,
+        )
+        status = 0
+    except subprocess.CalledProcessError as error:
+        output = error.output
+        status = error.returncode
+
+    return status, output
+
+
+def test_run_as_module_compile(tmpdir):
+    """piptools can be run as ``python -m piptools ...``."""
+
+    status, output = invoke([
+        sys.executable, '-m', 'piptools', 'compile', '--help',
+    ])
+
+    # Should have run pip-compile successfully.
+    output = output.decode('utf-8')
+    assert output.startswith('Usage:')
+    assert 'Compiles requirements.txt from requirements.in specs.' in output
+    assert status == 0
+
+
+def test_run_as_module_sync():
+    """piptools can be run as ``python -m piptools ...``."""
+
+    status, output = invoke([
+        sys.executable, '-m', 'piptools', 'sync', '--help',
+    ])
+
+    # Should have run pip-compile successfully.
+    output = output.decode('utf-8')
+    assert output.startswith('Usage:')
+    assert 'Synchronize virtual environment with requirements.txt.' in output
+    assert status == 0