Repository: jazzband/pip-tools

Issue:
Support for stdin / stdout
It would be useful if pip-compile would accept input from stdin and allowed to write to stdout.

`pip-compile` might detect if stdin/stdout is used, but an explicit option should work, too:

```
git show :requirements.in | pip-compile > requirements.txt
git show :requirements.in | pip-compile - -o > requirements.txt
```

This would allow to easily compile only what's in Git's index, for example.



Discussion:
+1 to this idea.

Seconded. It would also enable modular dependencies in a simple way:

``` bash
$ cat requirements-{base,dev}.in | pip-compile - -o >requirements-dev.txt
$ cat requirements-{base,production}.in | pip-compile - -o >requirements-production.txt
```

Great way to share a common versioned package list between different environments. The proper way to allow this would require quite a bit of reworking of .in files, allowing for some kind of "include" directive, but I think it's overkill at the moment.


Resolving patch:
diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py
--- a/piptools/scripts/compile.py
+++ b/piptools/scripts/compile.py
@@ -4,7 +4,10 @@
                         unicode_literals)
 
 import optparse
+import os
 import sys
+import tempfile
+
 import pip
 
 # Make sure we're using a reasonably modern version of pip
@@ -45,15 +48,27 @@ class PipCommand(pip.basecommand.Command):
 @click.option('--header/--no-header', is_flag=True, default=True, help="Add header to generated file")
 @click.option('--annotate/--no-annotate', is_flag=True, default=True,
               help="Annotate results, indicating where dependencies come from")
-@click.argument('src_file', required=False, type=click.Path(exists=True), default=DEFAULT_REQUIREMENTS_FILE)
+@click.option('-o', '--output-file', nargs=1, type=str, default=None,
+              help=("Where should pip-compile's output be stored? If unspecified and there is one input file, "
+                    "will be determined from the input file's name. Else, must be specified."))
+@click.argument('src_files', nargs=-1, type=click.Path(exists=True, allow_dash=True))
 def cli(verbose, dry_run, pre, rebuild, find_links, index_url, extra_index_url,
-        client_cert, trusted_host, header, annotate, src_file):
+        client_cert, trusted_host, header, annotate, output_file, src_files):
     """Compiles requirements.txt from requirements.in specs."""
     log.verbose = verbose
 
-    if not src_file:
-        log.warning('No input files to process')
-        sys.exit(2)
+    if len(src_files) == 0:
+        if not os.path.exists(DEFAULT_REQUIREMENTS_FILE):
+            raise click.BadParameter(("If you do not specify an input file, "
+                                      "the default is {}").format(DEFAULT_REQUIREMENTS_FILE))
+        src_files = (DEFAULT_REQUIREMENTS_FILE,)
+
+    if len(src_files) == 1 and src_files[0] == '-':
+        if not output_file:
+            raise click.BadParameter("If you specify '-' as your input file, then you must specify an output file")
+
+    if len(src_files) > 1 and not output_file:
+        raise click.BadParameter("If you specify more than one input file, then you must specify an output file")
 
     ###
     # Setup
@@ -104,12 +119,25 @@ def cli(verbose, dry_run, pre, rebuild, find_links, index_url, extra_index_url,
     ###
     # Parsing/collecting initial requirements
     ###
+
     constraints = []
-    for line in parse_requirements(src_file, finder=repository.finder, session=repository.session, options=pip_options):
-        constraints.append(line)
+    for src_file in src_files:
+        if src_file == '-':
+            # 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.
+            with tempfile.NamedTemporaryFile() as tmpfile:
+                tmpfile.write(sys.stdin.read())
+                tmpfile.flush()
+                constraints.extend(parse_requirements(
+                    tmpfile.name, finder=repository.finder, session=repository.session, options=pip_options))
+        else:
+            constraints.extend(parse_requirements(
+                src_file, finder=repository.finder, session=repository.session, options=pip_options))
 
     try:
-        resolver = Resolver(constraints, repository, prereleases=pre, clear_caches=rebuild)
+        resolver = Resolver(constraints, repository, prereleases=pre,
+                            clear_caches=rebuild)
         results = resolver.resolve()
     except PipToolsError as e:
         log.error(str(e))
@@ -146,7 +174,7 @@ def cli(verbose, dry_run, pre, rebuild, find_links, index_url, extra_index_url,
     if annotate:
         reverse_dependencies = resolver.reverse_dependencies(results)
 
-    writer = OutputWriter(src_file, dry_run=dry_run, header=header,
+    writer = OutputWriter(src_file, output_file=output_file, dry_run=dry_run, header=header,
                           annotate=annotate,
                           default_index_url=repository.DEFAULT_INDEX_URL,
                           index_urls=repository.finder.index_urls)
diff --git a/piptools/writer.py b/piptools/writer.py
--- a/piptools/writer.py
+++ b/piptools/writer.py
@@ -10,8 +10,9 @@
 
 class OutputWriter(object):
     def __init__(self, src_file, dry_run, header, annotate, default_index_url,
-                 index_urls):
+                 index_urls, output_file=None):
         self.src_file = src_file
+        self.output_file = output_file
         self.dry_run = dry_run
         self.header = header
         self.annotate = annotate
@@ -20,6 +21,8 @@ def __init__(self, src_file, dry_run, header, annotate, default_index_url,
 
     @property
     def dst_file(self):
+        if self.output_file:
+            return self.output_file
         base_name, _, _ = self.src_file.rpartition('.')
         return base_name + '.txt'
 
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@
     description=__doc__,
     packages=find_packages(exclude=['tests']),
     install_requires=[
-        'click',
+        'click>=6',
         'first',
         'six',
     ],


Test patch: