1# coding: utf-8
2from __future__ import absolute_import, division, print_function, unicode_literals
3
4import os
5import shlex
6import sys
7import tempfile
8import warnings
9
10from click import Command
11from click.utils import safecall
12from pip._internal.commands import create_command
13from pip._internal.req.constructors import install_req_from_line
14from pip._internal.utils.misc import redact_auth_from_url
15
16from .. import click
17from .._compat import parse_requirements
18from ..cache import DependencyCache
19from ..exceptions import PipToolsError
20from ..locations import CACHE_DIR
21from ..logging import log
22from ..repositories import LocalRequirementsRepository, PyPIRepository
23from ..resolver import Resolver
24from ..utils import UNSAFE_PACKAGES, dedup, is_pinned_requirement, key_from_ireq
25from ..writer import OutputWriter
26
27DEFAULT_REQUIREMENTS_FILE = "requirements.in"
28DEFAULT_REQUIREMENTS_OUTPUT_FILE = "requirements.txt"
29
30
31def _get_default_option(option_name):
32    """
33    Get default value of the pip's option (including option from pip.conf)
34    by a given option name.
35    """
36    install_command = create_command("install")
37    default_values = install_command.parser.get_default_values()
38    return getattr(default_values, option_name)
39
40
41class BaseCommand(Command):
42    _os_args = None
43
44    def parse_args(self, ctx, args):
45        """
46        Override base `parse_args` to store the argument part of `sys.argv`.
47        """
48        self._os_args = set(args)
49        return super(BaseCommand, self).parse_args(ctx, args)
50
51    def has_arg(self, arg_name):
52        """
53        Detect whether a given arg name (including negative counterparts
54        to the arg, e.g. --no-arg) is present in the argument part of `sys.argv`.
55        """
56        command_options = {option.name: option for option in self.params}
57        option = command_options[arg_name]
58        args = set(option.opts + option.secondary_opts)
59        return bool(self._os_args & args)
60
61
62@click.command(
63    cls=BaseCommand, context_settings={"help_option_names": ("-h", "--help")}
64)
65@click.version_option()
66@click.pass_context
67@click.option("-v", "--verbose", count=True, help="Show more output")
68@click.option("-q", "--quiet", count=True, help="Give less output")
69@click.option(
70    "-n",
71    "--dry-run",
72    is_flag=True,
73    help="Only show what would happen, don't change anything",
74)
75@click.option(
76    "-p",
77    "--pre",
78    is_flag=True,
79    default=None,
80    help="Allow resolving to prereleases (default is not)",
81)
82@click.option(
83    "-r",
84    "--rebuild",
85    is_flag=True,
86    help="Clear any caches upfront, rebuild from scratch",
87)
88@click.option(
89    "-f",
90    "--find-links",
91    multiple=True,
92    help="Look for archives in this directory or on this HTML page",
93)
94@click.option(
95    "-i",
96    "--index-url",
97    help="Change index URL (defaults to {index_url})".format(
98        index_url=redact_auth_from_url(_get_default_option("index_url"))
99    ),
100)
101@click.option(
102    "--extra-index-url", multiple=True, help="Add additional index URL to search"
103)
104@click.option("--cert", help="Path to alternate CA bundle.")
105@click.option(
106    "--client-cert",
107    help="Path to SSL client certificate, a single file containing "
108    "the private key and the certificate in PEM format.",
109)
110@click.option(
111    "--trusted-host",
112    multiple=True,
113    help="Mark this host as trusted, even though it does not have "
114    "valid or any HTTPS.",
115)
116@click.option(
117    "--header/--no-header",
118    is_flag=True,
119    default=True,
120    help="Add header to generated file",
121)
122@click.option(
123    "--index/--no-index",
124    is_flag=True,
125    default=True,
126    help="DEPRECATED: Add index URL to generated file",
127)
128@click.option(
129    "--emit-trusted-host/--no-emit-trusted-host",
130    is_flag=True,
131    default=True,
132    help="Add trusted host option to generated file",
133)
134@click.option(
135    "--annotate/--no-annotate",
136    is_flag=True,
137    default=True,
138    help="Annotate results, indicating where dependencies come from",
139)
140@click.option(
141    "-U",
142    "--upgrade",
143    is_flag=True,
144    default=False,
145    help="Try to upgrade all dependencies to their latest versions",
146)
147@click.option(
148    "-P",
149    "--upgrade-package",
150    "upgrade_packages",
151    nargs=1,
152    multiple=True,
153    help="Specify particular packages to upgrade.",
154)
155@click.option(
156    "-o",
157    "--output-file",
158    nargs=1,
159    default=None,
160    type=click.File("w+b", atomic=True, lazy=True),
161    help=(
162        "Output file name. Required if more than one input file is given. "
163        "Will be derived from input file otherwise."
164    ),
165)
166@click.option(
167    "--allow-unsafe/--no-allow-unsafe",
168    is_flag=True,
169    default=False,
170    help=(
171        "Pin packages considered unsafe: {}.\n\n"
172        "WARNING: Future versions of pip-tools will enable this behavior by default. "
173        "Use --no-allow-unsafe to keep the old behavior. It is recommended to pass the "
174        "--allow-unsafe now to adapt to the upcoming change.".format(
175            ", ".join(sorted(UNSAFE_PACKAGES))
176        )
177    ),
178)
179@click.option(
180    "--generate-hashes",
181    is_flag=True,
182    default=False,
183    help="Generate pip 8 style hashes in the resulting requirements file.",
184)
185@click.option(
186    "--reuse-hashes/--no-reuse-hashes",
187    is_flag=True,
188    default=True,
189    help=(
190        "Improve the speed of --generate-hashes by reusing the hashes from an "
191        "existing output file."
192    ),
193)
194@click.option(
195    "--max-rounds",
196    default=10,
197    help="Maximum number of rounds before resolving the requirements aborts.",
198)
199@click.argument("src_files", nargs=-1, type=click.Path(exists=True, allow_dash=True))
200@click.option(
201    "--build-isolation/--no-build-isolation",
202    is_flag=True,
203    default=True,
204    help="Enable isolation when building a modern source distribution. "
205    "Build dependencies specified by PEP 518 must be already installed "
206    "if build isolation is disabled.",
207)
208@click.option(
209    "--emit-find-links/--no-emit-find-links",
210    is_flag=True,
211    default=True,
212    help="Add the find-links option to generated file",
213)
214@click.option(
215    "--cache-dir",
216    help="Store the cache data in DIRECTORY.",
217    default=CACHE_DIR,
218    show_default=True,
219    type=click.Path(file_okay=False, writable=True),
220)
221@click.option("--pip-args", help="Arguments to pass directly to the pip command.")
222@click.option(
223    "--emit-index-url/--no-emit-index-url",
224    is_flag=True,
225    default=True,
226    help="Add index URL to generated file",
227)
228def cli(
229    ctx,
230    verbose,
231    quiet,
232    dry_run,
233    pre,
234    rebuild,
235    find_links,
236    index_url,
237    extra_index_url,
238    cert,
239    client_cert,
240    trusted_host,
241    header,
242    index,
243    emit_trusted_host,
244    annotate,
245    upgrade,
246    upgrade_packages,
247    output_file,
248    allow_unsafe,
249    generate_hashes,
250    reuse_hashes,
251    src_files,
252    max_rounds,
253    build_isolation,
254    emit_find_links,
255    cache_dir,
256    pip_args,
257    emit_index_url,
258):
259    """Compiles requirements.txt from requirements.in specs."""
260    log.verbosity = verbose - quiet
261
262    if len(src_files) == 0:
263        if os.path.exists(DEFAULT_REQUIREMENTS_FILE):
264            src_files = (DEFAULT_REQUIREMENTS_FILE,)
265        elif os.path.exists("setup.py"):
266            src_files = ("setup.py",)
267        else:
268            raise click.BadParameter(
269                (
270                    "If you do not specify an input file, "
271                    "the default is {} or setup.py"
272                ).format(DEFAULT_REQUIREMENTS_FILE)
273            )
274
275    if not output_file:
276        # An output file must be provided for stdin
277        if src_files == ("-",):
278            raise click.BadParameter("--output-file is required if input is from stdin")
279        # Use default requirements output file if there is a setup.py the source file
280        elif src_files == ("setup.py",):
281            file_name = DEFAULT_REQUIREMENTS_OUTPUT_FILE
282        # An output file must be provided if there are multiple source files
283        elif len(src_files) > 1:
284            raise click.BadParameter(
285                "--output-file is required if two or more input files are given."
286            )
287        # Otherwise derive the output file from the source file
288        else:
289            base_name = src_files[0].rsplit(".", 1)[0]
290            file_name = base_name + ".txt"
291
292        output_file = click.open_file(file_name, "w+b", atomic=True, lazy=True)
293
294        # Close the file at the end of the context execution
295        ctx.call_on_close(safecall(output_file.close_intelligently))
296
297    if cli.has_arg("index") and cli.has_arg("emit_index_url"):
298        raise click.BadParameter(
299            "--index/--no-index and --emit-index-url/--no-emit-index-url "
300            "are mutually exclusive."
301        )
302    elif cli.has_arg("index"):
303        warnings.warn(
304            "--index and --no-index are deprecated and will be removed "
305            "in future versions. Use --emit-index-url/--no-emit-index-url instead.",
306            category=FutureWarning,
307        )
308        emit_index_url = index
309
310    ###
311    # Setup
312    ###
313
314    right_args = shlex.split(pip_args or "")
315    pip_args = []
316    for link in find_links:
317        pip_args.extend(["-f", link])
318    if index_url:
319        pip_args.extend(["-i", index_url])
320    for extra_index in extra_index_url:
321        pip_args.extend(["--extra-index-url", extra_index])
322    if cert:
323        pip_args.extend(["--cert", cert])
324    if client_cert:
325        pip_args.extend(["--client-cert", client_cert])
326    if pre:
327        pip_args.extend(["--pre"])
328    for host in trusted_host:
329        pip_args.extend(["--trusted-host", host])
330
331    if not build_isolation:
332        pip_args.append("--no-build-isolation")
333    pip_args.extend(right_args)
334
335    repository = PyPIRepository(pip_args, cache_dir=cache_dir)
336
337    # Parse all constraints coming from --upgrade-package/-P
338    upgrade_reqs_gen = (install_req_from_line(pkg) for pkg in upgrade_packages)
339    upgrade_install_reqs = {
340        key_from_ireq(install_req): install_req for install_req in upgrade_reqs_gen
341    }
342
343    existing_pins_to_upgrade = set()
344
345    # Proxy with a LocalRequirementsRepository if --upgrade is not specified
346    # (= default invocation)
347    if not upgrade and os.path.exists(output_file.name):
348        # Use a temporary repository to ensure outdated(removed) options from
349        # existing requirements.txt wouldn't get into the current repository.
350        tmp_repository = PyPIRepository(pip_args, cache_dir=cache_dir)
351        ireqs = parse_requirements(
352            output_file.name,
353            finder=tmp_repository.finder,
354            session=tmp_repository.session,
355            options=tmp_repository.options,
356        )
357
358        # Exclude packages from --upgrade-package/-P from the existing
359        # constraints, and separately gather pins to be upgraded
360        existing_pins = {}
361        for ireq in filter(is_pinned_requirement, ireqs):
362            key = key_from_ireq(ireq)
363            if key in upgrade_install_reqs:
364                existing_pins_to_upgrade.add(key)
365            else:
366                existing_pins[key] = ireq
367        repository = LocalRequirementsRepository(
368            existing_pins, repository, reuse_hashes=reuse_hashes
369        )
370
371    ###
372    # Parsing/collecting initial requirements
373    ###
374
375    constraints = []
376    for src_file in src_files:
377        is_setup_file = os.path.basename(src_file) == "setup.py"
378        if is_setup_file or src_file == "-":
379            # pip requires filenames and not files. Since we want to support
380            # piping from stdin, we need to briefly save the input from stdin
381            # to a temporary file and have pip read that.  also used for
382            # reading requirements from install_requires in setup.py.
383            tmpfile = tempfile.NamedTemporaryFile(mode="wt", delete=False)
384            if is_setup_file:
385                from distutils.core import run_setup
386
387                dist = run_setup(src_file)
388                tmpfile.write("\n".join(dist.install_requires))
389                comes_from = "{name} ({filename})".format(
390                    name=dist.get_name(), filename=src_file
391                )
392            else:
393                tmpfile.write(sys.stdin.read())
394                comes_from = "-r -"
395            tmpfile.flush()
396            reqs = list(
397                parse_requirements(
398                    tmpfile.name,
399                    finder=repository.finder,
400                    session=repository.session,
401                    options=repository.options,
402                )
403            )
404            for req in reqs:
405                req.comes_from = comes_from
406            constraints.extend(reqs)
407        else:
408            constraints.extend(
409                parse_requirements(
410                    src_file,
411                    finder=repository.finder,
412                    session=repository.session,
413                    options=repository.options,
414                )
415            )
416
417    primary_packages = {
418        key_from_ireq(ireq) for ireq in constraints if not ireq.constraint
419    }
420
421    allowed_upgrades = primary_packages | existing_pins_to_upgrade
422    constraints.extend(
423        ireq for key, ireq in upgrade_install_reqs.items() if key in allowed_upgrades
424    )
425
426    # Filter out pip environment markers which do not match (PEP496)
427    constraints = [
428        req for req in constraints if req.markers is None or req.markers.evaluate()
429    ]
430
431    log.debug("Using indexes:")
432    with log.indentation():
433        for index_url in dedup(repository.finder.index_urls):
434            log.debug(redact_auth_from_url(index_url))
435
436    if repository.finder.find_links:
437        log.debug("")
438        log.debug("Using links:")
439        with log.indentation():
440            for find_link in dedup(repository.finder.find_links):
441                log.debug(redact_auth_from_url(find_link))
442
443    try:
444        resolver = Resolver(
445            constraints,
446            repository,
447            prereleases=repository.finder.allow_all_prereleases or pre,
448            cache=DependencyCache(cache_dir),
449            clear_caches=rebuild,
450            allow_unsafe=allow_unsafe,
451        )
452        results = resolver.resolve(max_rounds=max_rounds)
453        if generate_hashes:
454            hashes = resolver.resolve_hashes(results)
455        else:
456            hashes = None
457    except PipToolsError as e:
458        log.error(str(e))
459        sys.exit(2)
460
461    log.debug("")
462
463    ##
464    # Output
465    ##
466
467    writer = OutputWriter(
468        src_files,
469        output_file,
470        click_ctx=ctx,
471        dry_run=dry_run,
472        emit_header=header,
473        emit_index_url=emit_index_url,
474        emit_trusted_host=emit_trusted_host,
475        annotate=annotate,
476        generate_hashes=generate_hashes,
477        default_index_url=repository.DEFAULT_INDEX_URL,
478        index_urls=repository.finder.index_urls,
479        trusted_hosts=repository.finder.trusted_hosts,
480        format_control=repository.finder.format_control,
481        allow_unsafe=allow_unsafe,
482        find_links=repository.finder.find_links,
483        emit_find_links=emit_find_links,
484    )
485    writer.write(
486        results=results,
487        unsafe_requirements=resolver.unsafe_constraints,
488        markers={
489            key_from_ireq(ireq): ireq.markers for ireq in constraints if ireq.markers
490        },
491        hashes=hashes,
492    )
493
494    if dry_run:
495        log.info("Dry-run, so nothing updated.")
496