1# -*- coding: utf-8 -*-
2from __future__ import absolute_import
3
4import os
5import sys
6
7from click import (
8    argument, echo, edit, group, option, pass_context, secho, version_option, Choice
9)
10
11from ..__version__ import __version__
12from .._compat import fix_utf8
13from ..exceptions import PipenvOptionsError
14from ..patched import crayons
15from ..vendor import click_completion, delegator
16from .options import (
17    CONTEXT_SETTINGS, PipenvGroup, code_option, common_options, deploy_option,
18    general_options, install_options, lock_options, pass_state,
19    pypi_mirror_option, python_option, site_packages_option, skip_lock_option,
20    sync_options, system_option, three_option, uninstall_options,
21    verbose_option
22)
23
24
25# Enable shell completion.
26click_completion.init()
27
28subcommand_context = CONTEXT_SETTINGS.copy()
29subcommand_context.update({
30    "ignore_unknown_options": True,
31    "allow_extra_args": True
32})
33subcommand_context_no_interspersion = subcommand_context.copy()
34subcommand_context_no_interspersion["allow_interspersed_args"] = False
35
36
37@group(cls=PipenvGroup, invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
38@option("--where", is_flag=True, default=False, help="Output project home information.")
39@option("--venv", is_flag=True, default=False, help="Output virtualenv information.")
40@option("--py", is_flag=True, default=False, help="Output Python interpreter information.")
41@option("--envs", is_flag=True, default=False, help="Output Environment Variable options.")
42@option("--rm", is_flag=True, default=False, help="Remove the virtualenv.")
43@option("--bare", is_flag=True, default=False, help="Minimal output.")
44@option(
45    "--completion",
46    is_flag=True,
47    default=False,
48    help="Output completion (to be executed by the shell).",
49)
50@option("--man", is_flag=True, default=False, help="Display manpage.")
51@option(
52    "--support",
53    is_flag=True,
54    help="Output diagnostic information for use in GitHub issues.",
55)
56@general_options
57@version_option(prog_name=crayons.normal("pipenv", bold=True), version=__version__)
58@pass_state
59@pass_context
60def cli(
61    ctx,
62    state,
63    where=False,
64    venv=False,
65    py=False,
66    envs=False,
67    rm=False,
68    bare=False,
69    completion=False,
70    man=False,
71    support=None,
72    help=False,
73    site_packages=None,
74    **kwargs
75):
76    # Handle this ASAP to make shell startup fast.
77    if completion:
78        from .. import shells
79
80        try:
81            shell = shells.detect_info()[0]
82        except shells.ShellDetectionFailure:
83            echo(
84                "Fail to detect shell. Please provide the {0} environment "
85                "variable.".format(crayons.normal("PIPENV_SHELL", bold=True)),
86                err=True,
87            )
88            ctx.abort()
89        print(click_completion.get_code(shell=shell, prog_name="pipenv"))
90        return 0
91
92    from ..core import (
93        system_which,
94        do_py,
95        warn_in_virtualenv,
96        do_where,
97        project,
98        cleanup_virtualenv,
99        ensure_project,
100        format_help,
101        do_clear,
102    )
103    from ..utils import create_spinner
104
105    if man:
106        if system_which("man"):
107            path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "pipenv.1")
108            os.execle(system_which("man"), "man", path, os.environ)
109            return 0
110        else:
111            secho("man does not appear to be available on your system.", fg="yellow", bold=True, err=True)
112            return 1
113    if envs:
114        echo("The following environment variables can be set, to do various things:\n")
115        from .. import environments
116        for key in environments.__dict__:
117            if key.startswith("PIPENV"):
118                echo("  - {0}".format(crayons.normal(key, bold=True)))
119        echo(
120            "\nYou can learn more at:\n   {0}".format(
121                crayons.green(
122                    "https://pipenv.pypa.io/en/latest/advanced/#configuration-with-environment-variables"
123                )
124            )
125        )
126        return 0
127    warn_in_virtualenv()
128    if ctx.invoked_subcommand is None:
129        # --where was passed...
130        if where:
131            do_where(bare=True)
132            return 0
133        elif py:
134            do_py()
135            return 0
136        # --support was passed...
137        elif support:
138            from ..help import get_pipenv_diagnostics
139
140            get_pipenv_diagnostics()
141            return 0
142        # --clear was passed...
143        elif state.clear:
144            do_clear()
145            return 0
146        # --venv was passed...
147        elif venv:
148            # There is no virtualenv yet.
149            if not project.virtualenv_exists:
150                echo(
151                    "{}({}){}".format(
152                        crayons.red("No virtualenv has been created for this project"),
153                        crayons.normal(project.project_directory, bold=True),
154                        crayons.red(" yet!")
155                    ),
156                    err=True,
157                )
158                ctx.abort()
159            else:
160                echo(project.virtualenv_location)
161                return 0
162        # --rm was passed...
163        elif rm:
164            # Abort if --system (or running in a virtualenv).
165            from ..environments import PIPENV_USE_SYSTEM
166            if PIPENV_USE_SYSTEM:
167                echo(
168                    crayons.red(
169                        "You are attempting to remove a virtualenv that "
170                        "Pipenv did not create. Aborting."
171                    )
172                )
173                ctx.abort()
174            if project.virtualenv_exists:
175                loc = project.virtualenv_location
176                echo(
177                    crayons.normal(
178                        u"{0} ({1})...".format(
179                            crayons.normal("Removing virtualenv", bold=True),
180                            crayons.green(loc),
181                        )
182                    )
183                )
184                with create_spinner(text="Running..."):
185                    # Remove the virtualenv.
186                    cleanup_virtualenv(bare=True)
187                return 0
188            else:
189                echo(
190                    crayons.red(
191                        "No virtualenv has been created for this project yet!",
192                        bold=True,
193                    ),
194                    err=True,
195                )
196                ctx.abort()
197    # --two / --three was passed...
198    if (state.python or state.three is not None) or state.site_packages:
199        ensure_project(
200            three=state.three,
201            python=state.python,
202            warn=True,
203            site_packages=state.site_packages,
204            pypi_mirror=state.pypi_mirror,
205            clear=state.clear,
206        )
207    # Check this again before exiting for empty ``pipenv`` command.
208    elif ctx.invoked_subcommand is None:
209        # Display help to user, if no commands were passed.
210        echo(format_help(ctx.get_help()))
211
212
213@cli.command(
214    short_help="Installs provided packages and adds them to Pipfile, or (if no packages are given), installs all packages from Pipfile.",
215    context_settings=subcommand_context,
216)
217@system_option
218@code_option
219@deploy_option
220@site_packages_option
221@skip_lock_option
222@install_options
223@pass_state
224@pass_context
225def install(
226    ctx,
227    state,
228    **kwargs
229):
230    """Installs provided packages and adds them to Pipfile, or (if no packages are given), installs all packages from Pipfile."""
231    from ..core import do_install
232
233    retcode = do_install(
234        dev=state.installstate.dev,
235        three=state.three,
236        python=state.python,
237        pypi_mirror=state.pypi_mirror,
238        system=state.system,
239        lock=not state.installstate.skip_lock,
240        ignore_pipfile=state.installstate.ignore_pipfile,
241        skip_lock=state.installstate.skip_lock,
242        requirementstxt=state.installstate.requirementstxt,
243        sequential=state.installstate.sequential,
244        pre=state.installstate.pre,
245        code=state.installstate.code,
246        deploy=state.installstate.deploy,
247        keep_outdated=state.installstate.keep_outdated,
248        selective_upgrade=state.installstate.selective_upgrade,
249        index_url=state.index,
250        extra_index_url=state.extra_index_urls,
251        packages=state.installstate.packages,
252        editable_packages=state.installstate.editables,
253        site_packages=state.site_packages
254    )
255    if retcode:
256        ctx.abort()
257
258
259@cli.command(
260    short_help="Uninstalls a provided package and removes it from Pipfile.",
261    context_settings=subcommand_context
262)
263@option(
264    "--all-dev",
265    is_flag=True,
266    default=False,
267    help="Uninstall all package from [dev-packages].",
268)
269@option(
270    "--all",
271    is_flag=True,
272    default=False,
273    help="Purge all package(s) from virtualenv. Does not edit Pipfile.",
274)
275@uninstall_options
276@pass_state
277@pass_context
278def uninstall(
279    ctx,
280    state,
281    all_dev=False,
282    all=False,
283    **kwargs
284):
285    """Uninstalls a provided package and removes it from Pipfile."""
286    from ..core import do_uninstall
287    retcode = do_uninstall(
288        packages=state.installstate.packages,
289        editable_packages=state.installstate.editables,
290        three=state.three,
291        python=state.python,
292        system=state.system,
293        lock=not state.installstate.skip_lock,
294        all_dev=all_dev,
295        all=all,
296        keep_outdated=state.installstate.keep_outdated,
297        pypi_mirror=state.pypi_mirror,
298        ctx=ctx
299    )
300    if retcode:
301        sys.exit(retcode)
302
303
304LOCK_HEADER = """\
305#
306# These requirements were autogenerated by pipenv
307# To regenerate from the project's Pipfile, run:
308#
309#    pipenv lock {options}
310#
311"""
312
313
314LOCK_DEV_NOTE = """\
315# Note: in pipenv 2020.x, "--dev" changed to emit both default and development
316# requirements. To emit only development requirements, pass "--dev-only".
317"""
318
319
320@cli.command(short_help="Generates Pipfile.lock.", context_settings=CONTEXT_SETTINGS)
321@lock_options
322@pass_state
323@pass_context
324def lock(
325    ctx,
326    state,
327    **kwargs
328):
329    """Generates Pipfile.lock."""
330    from ..core import ensure_project, do_init, do_lock
331    # Ensure that virtualenv is available.
332    # Note that we don't pass clear on to ensure_project as it is also
333    # handled in do_lock
334    ensure_project(
335        three=state.three, python=state.python, pypi_mirror=state.pypi_mirror,
336        warn=(not state.quiet), site_packages=state.site_packages,
337    )
338    emit_requirements = state.lockoptions.emit_requirements
339    dev = state.installstate.dev
340    dev_only = state.lockoptions.dev_only
341    pre = state.installstate.pre
342    if emit_requirements:
343        # Emit requirements file header (unless turned off with --no-header)
344        if state.lockoptions.emit_requirements_header:
345            header_options = ["--requirements"]
346            if dev_only:
347                header_options.append("--dev-only")
348            elif dev:
349                header_options.append("--dev")
350            echo(LOCK_HEADER.format(options=" ".join(header_options)))
351            # TODO: Emit pip-compile style header
352            if dev and not dev_only:
353                echo(LOCK_DEV_NOTE)
354        # Setting "emit_requirements=True" means do_init() just emits the
355        # install requirements file to stdout, it doesn't install anything
356        do_init(
357            dev=dev,
358            dev_only=dev_only,
359            emit_requirements=emit_requirements,
360            pypi_mirror=state.pypi_mirror,
361            pre=pre,
362        )
363    elif state.lockoptions.dev_only:
364        raise PipenvOptionsError(
365            "--dev-only",
366            "--dev-only is only permitted in combination with --requirements. "
367            "Aborting."
368        )
369    do_lock(
370        ctx=ctx,
371        clear=state.clear,
372        pre=pre,
373        keep_outdated=state.installstate.keep_outdated,
374        pypi_mirror=state.pypi_mirror,
375        write=not state.quiet,
376    )
377
378
379@cli.command(
380    short_help="Spawns a shell within the virtualenv.",
381    context_settings=subcommand_context,
382)
383@option(
384    "--fancy",
385    is_flag=True,
386    default=False,
387    help="Run in shell in fancy mode. Make sure the shell have no path manipulating"
388         " scripts. Run $pipenv shell for issues with compatibility mode.",
389)
390@option(
391    "--anyway",
392    is_flag=True,
393    default=False,
394    help="Always spawn a sub-shell, even if one is already spawned.",
395)
396@argument("shell_args", nargs=-1)
397@pypi_mirror_option
398@three_option
399@python_option
400@pass_state
401def shell(
402    state,
403    fancy=False,
404    shell_args=None,
405    anyway=False,
406):
407    """Spawns a shell within the virtualenv."""
408    from ..core import load_dot_env, do_shell
409
410    # Prevent user from activating nested environments.
411    if "PIPENV_ACTIVE" in os.environ:
412        # If PIPENV_ACTIVE is set, VIRTUAL_ENV should always be set too.
413        venv_name = os.environ.get("VIRTUAL_ENV", "UNKNOWN_VIRTUAL_ENVIRONMENT")
414        if not anyway:
415            echo(
416                "{0} {1} {2}\nNo action taken to avoid nested environments.".format(
417                    crayons.normal("Shell for"),
418                    crayons.green(venv_name, bold=True),
419                    crayons.normal("already activated.", bold=True),
420                ),
421                err=True,
422            )
423            sys.exit(1)
424    # Load .env file.
425    load_dot_env()
426    # Use fancy mode for Windows.
427    if os.name == "nt":
428        fancy = True
429    do_shell(
430        three=state.three,
431        python=state.python,
432        fancy=fancy,
433        shell_args=shell_args,
434        pypi_mirror=state.pypi_mirror,
435    )
436
437
438@cli.command(
439    short_help="Spawns a command installed into the virtualenv.",
440    context_settings=subcommand_context_no_interspersion,
441)
442@common_options
443@argument("command")
444@argument("args", nargs=-1)
445@pass_state
446def run(state, command, args):
447    """Spawns a command installed into the virtualenv."""
448    from ..core import do_run
449    do_run(
450        command=command, args=args, three=state.three, python=state.python, pypi_mirror=state.pypi_mirror
451    )
452
453
454@cli.command(
455    short_help="Checks for PyUp Safety security vulnerabilities and against"
456               " PEP 508 markers provided in Pipfile.",
457    context_settings=subcommand_context
458)
459@option(
460    "--unused",
461    nargs=1,
462    default=False,
463    help="Given a code path, show potentially unused dependencies.",
464)
465@option(
466    "--db",
467    nargs=1,
468    default=lambda: os.environ.get('PIPENV_SAFETY_DB', False),
469    help="Path to a local PyUp Safety vulnerabilities database."
470         " Default: ENV PIPENV_SAFETY_DB or None.",
471)
472@option(
473    "--ignore",
474    "-i",
475    multiple=True,
476    help="Ignore specified vulnerability during PyUp Safety checks.",
477)
478@option(
479    "--output",
480    type=Choice(["default", "json", "full-report", "bare"]),
481    default="default",
482    help="Translates to --json, --full-report or --bare from PyUp Safety check",
483)
484@option(
485    "--key",
486    help="Safety API key from PyUp.io for scanning dependencies against a live"
487         " vulnerabilities database. Leave blank for scanning against a"
488         " database that only updates once a month.",
489)
490@option(
491    "--quiet",
492    is_flag=True,
493    help="Quiet standard output, except vulnerability report."
494)
495@common_options
496@system_option
497@argument("args", nargs=-1)
498@pass_state
499def check(
500    state,
501    unused=False,
502    db=False,
503    style=False,
504    ignore=None,
505    output="default",
506    key=None,
507    quiet=False,
508    args=None,
509    **kwargs
510):
511    """Checks for PyUp Safety security vulnerabilities and against PEP 508 markers provided in Pipfile."""
512    from ..core import do_check
513
514    do_check(
515        three=state.three,
516        python=state.python,
517        system=state.system,
518        unused=unused,
519        db=db,
520        ignore=ignore,
521        output=output,
522        key=key,
523        quiet=quiet,
524        args=args,
525        pypi_mirror=state.pypi_mirror,
526    )
527
528
529@cli.command(short_help="Runs lock, then sync.", context_settings=CONTEXT_SETTINGS)
530@option("--bare", is_flag=True, default=False, help="Minimal output.")
531@option(
532    "--outdated", is_flag=True, default=False, help=u"List out-of-date dependencies."
533)
534@option("--dry-run", is_flag=True, default=None, help=u"List out-of-date dependencies.")
535@install_options
536@pass_state
537@pass_context
538def update(
539    ctx,
540    state,
541    bare=False,
542    dry_run=None,
543    outdated=False,
544    **kwargs
545):
546    """Runs lock, then sync."""
547    from ..core import (
548        ensure_project,
549        do_outdated,
550        do_lock,
551        do_sync,
552        project,
553    )
554    ensure_project(
555        three=state.three, python=state.python, pypi_mirror=state.pypi_mirror,
556        warn=(not state.quiet), site_packages=state.site_packages, clear=state.clear
557    )
558    if not outdated:
559        outdated = bool(dry_run)
560    if outdated:
561        do_outdated(clear=state.clear, pre=state.installstate.pre, pypi_mirror=state.pypi_mirror)
562    packages = [p for p in state.installstate.packages if p]
563    editable = [p for p in state.installstate.editables if p]
564    if not packages:
565        echo(
566            "{0} {1} {2} {3}{4}".format(
567                crayons.normal("Running", bold=True),
568                crayons.yellow("$ pipenv lock", bold=True),
569                crayons.normal("then", bold=True),
570                crayons.yellow("$ pipenv sync", bold=True),
571                crayons.normal(".", bold=True),
572            )
573        )
574    else:
575        for package in packages + editable:
576            if package not in project.all_packages:
577                echo(
578                    "{0}: {1} was not found in your Pipfile! Aborting."
579                    "".format(
580                        crayons.red("Warning", bold=True),
581                        crayons.green(package, bold=True),
582                    ),
583                    err=True,
584                )
585                ctx.abort()
586    do_lock(
587        ctx=ctx,
588        clear=state.clear,
589        pre=state.installstate.pre,
590        keep_outdated=state.installstate.keep_outdated,
591        pypi_mirror=state.pypi_mirror,
592        write=not state.quiet,
593    )
594    do_sync(
595        ctx=ctx,
596        dev=state.installstate.dev,
597        three=state.three,
598        python=state.python,
599        bare=bare,
600        dont_upgrade=not state.installstate.keep_outdated,
601        user=False,
602        clear=state.clear,
603        unused=False,
604        sequential=state.installstate.sequential,
605        pypi_mirror=state.pypi_mirror,
606    )
607
608
609@cli.command(
610    short_help=u"Displays currently-installed dependency graph information.",
611    context_settings=CONTEXT_SETTINGS
612)
613@option("--bare", is_flag=True, default=False, help="Minimal output.")
614@option("--json", is_flag=True, default=False, help="Output JSON.")
615@option("--json-tree", is_flag=True, default=False, help="Output JSON in nested tree.")
616@option("--reverse", is_flag=True, default=False, help="Reversed dependency graph.")
617def graph(bare=False, json=False, json_tree=False, reverse=False):
618    """Displays currently-installed dependency graph information."""
619    from ..core import do_graph
620
621    do_graph(bare=bare, json=json, json_tree=json_tree, reverse=reverse)
622
623
624@cli.command(
625    short_help="View a given module in your editor.", name="open",
626    context_settings=CONTEXT_SETTINGS
627)
628@common_options
629@argument("module", nargs=1)
630@pass_state
631def run_open(state, module, *args, **kwargs):
632    """View a given module in your editor.
633
634    This uses the EDITOR environment variable. You can temporarily override it,
635    for example:
636
637        EDITOR=atom pipenv open requests
638    """
639    from ..core import which, ensure_project, inline_activate_virtual_environment
640
641    # Ensure that virtualenv is available.
642    ensure_project(
643        three=state.three, python=state.python,
644        validate=False, pypi_mirror=state.pypi_mirror,
645    )
646    c = delegator.run(
647        '{0} -c "import {1}; print({1}.__file__);"'.format(which("python"), module)
648    )
649    try:
650        assert c.return_code == 0
651    except AssertionError:
652        echo(crayons.red("Module not found!"))
653        sys.exit(1)
654    if "__init__.py" in c.out:
655        p = os.path.dirname(c.out.strip().rstrip("cdo"))
656    else:
657        p = c.out.strip().rstrip("cdo")
658    echo(crayons.normal("Opening {0!r} in your EDITOR.".format(p), bold=True))
659    inline_activate_virtual_environment()
660    edit(filename=p)
661    return 0
662
663
664@cli.command(
665    short_help="Installs all packages specified in Pipfile.lock.",
666    context_settings=CONTEXT_SETTINGS
667)
668@system_option
669@option("--bare", is_flag=True, default=False, help="Minimal output.")
670@sync_options
671@pass_state
672@pass_context
673def sync(
674    ctx,
675    state,
676    bare=False,
677    user=False,
678    unused=False,
679    **kwargs
680):
681    """Installs all packages specified in Pipfile.lock."""
682    from ..core import do_sync
683
684    retcode = do_sync(
685        ctx=ctx,
686        dev=state.installstate.dev,
687        three=state.three,
688        python=state.python,
689        bare=bare,
690        dont_upgrade=(not state.installstate.keep_outdated),
691        user=user,
692        clear=state.clear,
693        unused=unused,
694        sequential=state.installstate.sequential,
695        pypi_mirror=state.pypi_mirror,
696        system=state.system
697    )
698    if retcode:
699        ctx.abort()
700
701
702@cli.command(
703    short_help="Uninstalls all packages not specified in Pipfile.lock.",
704    context_settings=CONTEXT_SETTINGS
705)
706@option("--bare", is_flag=True, default=False, help="Minimal output.")
707@option("--dry-run", is_flag=True, default=False, help="Just output unneeded packages.")
708@verbose_option
709@three_option
710@python_option
711@pass_state
712@pass_context
713def clean(ctx, state, dry_run=False, bare=False, user=False):
714    """Uninstalls all packages not specified in Pipfile.lock."""
715    from ..core import do_clean
716    do_clean(ctx=ctx, three=state.three, python=state.python, dry_run=dry_run,
717             system=state.system)
718
719
720@cli.command(
721    short_help="Lists scripts in current environment config.",
722    context_settings=subcommand_context_no_interspersion,
723)
724@common_options
725def scripts():
726    """Lists scripts in current environment config."""
727    from ..core import project
728
729    if not project.pipfile_exists:
730        echo("No Pipfile present at project home.", err=True)
731        sys.exit(1)
732    scripts = project.parsed_pipfile.get('scripts', {})
733    first_column_width = max(len(word) for word in ["Command"] + list(scripts))
734    second_column_width = max(len(word) for word in ["Script"] + list(scripts.values()))
735    lines = ["{0:<{width}}  Script".format("Command", width=first_column_width)]
736    lines.append("{}  {}".format("-" * first_column_width, "-" * second_column_width))
737    lines.extend(
738        "{0:<{width}}  {1}".format(name, script, width=first_column_width)
739        for name, script in scripts.items()
740    )
741    echo("\n".join(fix_utf8(line) for line in lines))
742
743
744if __name__ == "__main__":
745    cli()
746