From beeef0e33a471bdb006e7531417b235c09c2fda9 Mon Sep 17 00:00:00 2001 From: kylecribbs <31107878+kylecribbs@users.noreply.github.com> Date: Tue, 22 Jan 2019 21:21:41 -0700 Subject: [PATCH 001/342] Added --db, --output, and --squelch flags. Cleaned up duplicate code. --db allows users to specify a local source to the json databse for offline safety check. --output allows users to specify json, full-report, or bare from safety check --squelch hides all stdout from pipenv. Removed some unneeded duplicate code. --- pipenv/cli/command.py | 25 ++++++++- pipenv/core.py | 117 +++++++++++++++++++++++++----------------- 2 files changed, 94 insertions(+), 48 deletions(-) diff --git a/pipenv/cli/command.py b/pipenv/cli/command.py index ec1bef6138..c37c55603a 100644 --- a/pipenv/cli/command.py +++ b/pipenv/cli/command.py @@ -5,7 +5,7 @@ import sys from click import ( - argument, echo, edit, group, option, pass_context, secho, version_option + argument, echo, edit, group, option, pass_context, secho, version_option, Choice ) import click_completion @@ -416,12 +416,29 @@ def run(state, command, args): default=False, help="Given a code path, show potentially unused dependencies.", ) +@option( + "--db", + nargs=1, + default=False, + help="Path to a local vulnerability database. Default: empty", +) @option( "--ignore", "-i", multiple=True, help="Ignore specified vulnerability during safety checks.", ) +@option( + "--output", + type=Choice(["default", "json", "full-report", "bare"]), + default="default", + help="Translates to --json, --ful-report or --bare from safety check", +) +@option( + "--squelch", + is_flag=True, + help="Squelch stdout except vulnerability report." +) @common_options @system_option @argument("args", nargs=-1) @@ -429,8 +446,11 @@ def run(state, command, args): def check( state, unused=False, + db=False, style=False, ignore=None, + output="default", + squelch=False, args=None, **kwargs ): @@ -442,7 +462,10 @@ def check( python=state.python, system=state.system, unused=unused, + db=db, ignore=ignore, + output=output, + squelch=squelch, args=args, pypi_mirror=state.pypi_mirror, ) diff --git a/pipenv/core.py b/pipenv/core.py index 0cbb956b8b..db39a56f77 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -2139,11 +2139,6 @@ def do_shell(three=None, python=False, fancy=False, shell_args=None, pypi_mirror three=three, python=python, validate=False, pypi_mirror=pypi_mirror, ) - # Set an environment variable, so we know we're in the environment. - os.environ["PIPENV_ACTIVE"] = vistir.misc.fs_str("1") - - os.environ.pop("PIP_SHIMS_BASE_MODULE", None) - # Support shell compatibility mode. if PIPENV_SHELL_FANCY: fancy = True @@ -2159,6 +2154,13 @@ def do_shell(three=None, python=False, fancy=False, shell_args=None, pypi_mirror shell_args, ) + # Set an environment variable, so we know we're in the environment. + # Only set PIPENV_ACTIVE after finishing reading virtualenv_location + # otherwise its value will be changed + os.environ["PIPENV_ACTIVE"] = vistir.misc.fs_str("1") + + os.environ.pop("PIP_SHIMS_BASE_MODULE", None) + if fancy: shell.fork(*fork_args) return @@ -2293,14 +2295,19 @@ def do_run(command, args, three=None, python=False, pypi_mirror=None): three=three, python=python, validate=False, pypi_mirror=pypi_mirror, ) + load_dot_env() + + # Activate virtualenv under the current interpreter's environment + inline_activate_virtual_environment() + # Set an environment variable, so we know we're in the environment. + # Only set PIPENV_ACTIVE after finishing reading virtualenv_location + # such as in inline_activate_virtual_environment + # otherwise its value will be changed os.environ["PIPENV_ACTIVE"] = vistir.misc.fs_str("1") os.environ.pop("PIP_SHIMS_BASE_MODULE", None) - load_dot_env() - # Activate virtualenv under the current interpreter's environment - inline_activate_virtual_environment() try: script = project.build_script(command, args) cmd_string = ' '.join([script.command] + script.args) @@ -2319,9 +2326,12 @@ def do_check( python=False, system=False, unused=False, + db=False, ignore=None, + output="default", + squelch=False, args=None, - pypi_mirror=None, + pypi_mirror=None ): if not system: # Ensure that virtualenv is available. @@ -2343,17 +2353,19 @@ def do_check( except ValueError: pass if deps_required: - click.echo( - crayons.normal( - "The following dependencies appear unused, and may be safe for removal:" + if not squelch: + click.echo( + crayons.normal( + "The following dependencies appear unused, and may be safe for removal:" + ) ) - ) - for dep in deps_required: - click.echo(" - {0}".format(crayons.green(dep))) - sys.exit(1) + for dep in deps_required: + click.echo(" - {0}".format(crayons.green(dep))) + sys.exit(1) else: sys.exit(0) - click.echo(crayons.normal(fix_utf8("Checking PEP 508 requirements…"), bold=True)) + if not squelch: + click.echo(crayons.normal(fix_utf8("Checking PEP 508 requirements…"), bold=True)) if system: python = system_which("python") else: @@ -2388,14 +2400,13 @@ def do_check( click.echo(crayons.red("Failed!"), err=True) sys.exit(1) else: - click.echo(crayons.green("Passed!")) - click.echo(crayons.normal(fix_utf8("Checking installed package safety…"), bold=True)) + if not squelch: + click.echo(crayons.green("Passed!")) + if not squelch: + click.echo(crayons.normal(fix_utf8("Checking installed package safety…"), bold=True)) path = pep508checker.__file__.rstrip("cdo") path = os.sep.join(__file__.split(os.sep)[:-1] + ["patched", "safety.zip"]) - if not system: - python = which("python") - else: - python = system_which("python") + ignored = "" if ignore: ignored = "--ignore {0}".format(" --ignore ".join(ignore)) click.echo( @@ -2404,33 +2415,45 @@ def do_check( ), err=True, ) - else: - ignored = "" - c = delegator.run( - '"{0}" {1} check --json --key={2} {3}'.format( - python, escape_grouped_arguments(path), PIPENV_PYUP_API_KEY, ignored - ) + + key = "--key={0}".format(PIPENV_PYUP_API_KEY) + if db: + key = "--db {0}".format(db) + + switch = output + if output == "default": + switch = "json" + + check_string = '"{0}" {1} check --{2} {3} {4}'.format( + python, escape_grouped_arguments(path), switch, ignored, key ) - try: - results = simplejson.loads(c.out) - except ValueError: - click.echo("An error occurred:", err=True) - click.echo(c.err if len(c.err) > 0 else c.out, err=True) - sys.exit(1) - for (package, resolved, installed, description, vuln) in results: - click.echo( - "{0}: {1} {2} resolved ({3} installed)!".format( - crayons.normal(vuln, bold=True), - crayons.green(package), - crayons.red(resolved, bold=False), - crayons.red(installed, bold=True), + + c = delegator.run(check_string) + + if output == "default": + try: + results = simplejson.loads(c.out) + except ValueError: + click.echo("An error occurred:", err=True) + click.echo(c.err if len(c.err) > 0 else c.out, err=True) + sys.exit(1) + for (package, resolved, installed, description, vuln) in results: + click.echo( + "{0}: {1} {2} resolved ({3} installed)!".format( + crayons.normal(vuln, bold=True), + crayons.green(package), + crayons.red(resolved, bold=False), + crayons.red(installed, bold=True), + ) ) - ) - click.echo("{0}".format(description)) - click.echo() - if not results: - click.echo(crayons.green("All good!")) + click.echo("{0}".format(description)) + click.echo() + if not results: + click.echo(crayons.green("All good!")) + else: + sys.exit(1) else: + click.echo(c.out) sys.exit(1) From 1c25b660e066afcde4bf9e909a0d966f118b9ad4 Mon Sep 17 00:00:00 2001 From: kylecribbs <31107878+kylecribbs@users.noreply.github.com> Date: Tue, 22 Jan 2019 21:29:19 -0700 Subject: [PATCH 002/342] Add PEEP --- peeps/PEEP-044.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 peeps/PEEP-044.md diff --git a/peeps/PEEP-044.md b/peeps/PEEP-044.md new file mode 100644 index 0000000000..73044826f2 --- /dev/null +++ b/peeps/PEEP-044.md @@ -0,0 +1,54 @@ +# PEEP-044: safety-db integration, squelch, and output. + +pipenv check needs offline, ci, and other output capabilities. + +☤ + +Not everyone can utilize pipenv check and access the internet. Safety check knew this +and that is why they created safety-db. This repository contains a json database that +is updated monthly. Safety check allows you to pass a --db flag that is a local directory +containing that database. Safety check also allows you to pass --json, --bare, and +--full-report. Pipenv check has their own way of displaying the results that is why I +believe there should be a --output flag that allows users to specify json, bare, +and full-report from safety check and default for the current pipenv check output. +Currently, pipenv check has a lot of stdout messages and makes it harder to pipe +the results into something to be checked (especially for continuous integration +pipelines). That is why adding a --squelch switch is also important. This will be +default False (display all stdout); however, the user has the option to add the +--squelch switch to make the output only come from safety check. + +## Current implementation: +### Example 1 +``` bash +pipenv check +Checking PEP 508 requirements… +Passed! +Checking installed package safety… +25853: insecure-package <0.2.0 resolved (0.1.0 installed)! +This is an insecure package with lots of exploitable security vulnerabilities. +``` +### Example 2 +``` bash +pipenv check | jq length +parse error: Invalid numeric literal at line 1, column 9 +``` + +## Future implementation: +### Example 1 +``` bash +pipenv check --db /Users/macbookpro/workspace/test/safety-db/data/ --output json --squelch +[ + [ + "insecure-package", + "<0.2.0", + "0.1.0", + "This is an insecure package with lots of exploitable security vulnerabilities.", + "25853" + ] +] +``` +### Example 2 +``` bash +pipenv check --db /Users/macbookpro/workspace/test/safety-db/data/ --output json --squelch | jq length +1 +``` From 966997c64d9ed012cb3125648fcde5c2ecfe981f Mon Sep 17 00:00:00 2001 From: frostming Date: Fri, 2 Aug 2019 10:01:47 +0800 Subject: [PATCH 003/342] PEEP-006: new behavior of `pipenv lock -r -d` --- peeps/PEEP-006.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 peeps/PEEP-006.md diff --git a/peeps/PEEP-006.md b/peeps/PEEP-006.md new file mode 100644 index 0000000000..87304bba46 --- /dev/null +++ b/peeps/PEEP-006.md @@ -0,0 +1,30 @@ +# PEEP-006: Change the behavior of `-d` flag when generating requirement.txt + +Make the behavior of `pipenv lock -r -d` consistent with those in other commands: convert all dependencies. + +☤ + +If you type `pipenv lock --help` the help document says: + +```bash +-d, --dev Install both develop and default packages. [env var:PIPENV_DEV] +``` + +That is not accurate and confusing for `pipenv lock -r`, which only produces the develop requirments. + +This PEEP proposes to change the behavior of `pipenv lock -r -d` to produce **all** requirements, both develop +and default. Also, change the help string of `-d/--dev` to **"Generate both develop and default requirements"**. + +Introduce a new flag `--only` to restrict to develop requirements only. The flag does nothing when not combined with +`-d/--dev` flag. + +Display a warning message to remind users of the new `--only` flag and the behavior change, for the next several releases. + +## Impact + +The users relying on the old behavior will get more requirements listed in the ``dev-requirements.txt`` file, +which in most cases is harmless. They can just add `--only` flag to achieve the same thing before. + +## Related issues: + +- #3316 From 711403305a543355292db291906178257cf373ad Mon Sep 17 00:00:00 2001 From: Kyle Cribbs Date: Thu, 10 Oct 2019 14:21:33 -0600 Subject: [PATCH 004/342] Updated with quiet, verbose, and environmental var Users can pass PIPENV_SAFETY_DB path for local db or --db changed squelch to quiet. added logging stating its using local db. --- pipenv/cli/command.py | 12 ++++++------ pipenv/core.py | 12 +++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pipenv/cli/command.py b/pipenv/cli/command.py index c37c55603a..a0f721ab95 100644 --- a/pipenv/cli/command.py +++ b/pipenv/cli/command.py @@ -419,8 +419,8 @@ def run(state, command, args): @option( "--db", nargs=1, - default=False, - help="Path to a local vulnerability database. Default: empty", + default=lambda: os.environ.get('PIPENV_SAFETY_DB', False), + help="Path to a local vulnerability database. Default: ENV PIPENV_SAFETY_DB or None", ) @option( "--ignore", @@ -435,9 +435,9 @@ def run(state, command, args): help="Translates to --json, --ful-report or --bare from safety check", ) @option( - "--squelch", + "--quiet", is_flag=True, - help="Squelch stdout except vulnerability report." + help="Quiet stdout except vulnerability report." ) @common_options @system_option @@ -450,7 +450,7 @@ def check( style=False, ignore=None, output="default", - squelch=False, + quiet=False, args=None, **kwargs ): @@ -465,7 +465,7 @@ def check( db=db, ignore=ignore, output=output, - squelch=squelch, + quiet=quiet, args=args, pypi_mirror=state.pypi_mirror, ) diff --git a/pipenv/core.py b/pipenv/core.py index db39a56f77..981d0a4c3b 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -2329,7 +2329,7 @@ def do_check( db=False, ignore=None, output="default", - squelch=False, + quiet=False, args=None, pypi_mirror=None ): @@ -2353,7 +2353,7 @@ def do_check( except ValueError: pass if deps_required: - if not squelch: + if not quiet and not environments.is_quiet(): click.echo( crayons.normal( "The following dependencies appear unused, and may be safe for removal:" @@ -2364,7 +2364,7 @@ def do_check( sys.exit(1) else: sys.exit(0) - if not squelch: + if not quiet and not environments.is_quiet(): click.echo(crayons.normal(fix_utf8("Checking PEP 508 requirements…"), bold=True)) if system: python = system_which("python") @@ -2400,9 +2400,9 @@ def do_check( click.echo(crayons.red("Failed!"), err=True) sys.exit(1) else: - if not squelch: + if not quiet and not environments.is_quiet(): click.echo(crayons.green("Passed!")) - if not squelch: + if not quiet and not environments.is_quiet(): click.echo(crayons.normal(fix_utf8("Checking installed package safety…"), bold=True)) path = pep508checker.__file__.rstrip("cdo") path = os.sep.join(__file__.split(os.sep)[:-1] + ["patched", "safety.zip"]) @@ -2418,6 +2418,8 @@ def do_check( key = "--key={0}".format(PIPENV_PYUP_API_KEY) if db: + if not quiet and not environments.is_quiet(): + click.echo(crayons.normal("Using local database {}".format(db))) key = "--db {0}".format(db) switch = output From 1fcb1b5bc195922602ef77a6e3fba2cb5c1c08c7 Mon Sep 17 00:00:00 2001 From: Tim Hughes Date: Wed, 15 Jan 2020 10:40:01 +0000 Subject: [PATCH 005/342] [docs] mention variable expansion in `.env` files fix #3610 --- docs/advanced.rst | 17 +++++++++++++++++ news/4100.doc.rst | 1 + 2 files changed, 18 insertions(+) create mode 100644 news/4100.doc.rst diff --git a/docs/advanced.rst b/docs/advanced.rst index 9efb798eb1..014bd62d27 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -344,6 +344,21 @@ If a ``.env`` file is present in your project, ``$ pipenv shell`` and ``$ pipenv >>> os.environ['HELLO'] 'WORLD' +Shell like variable expansion is available in ``.env`` files using `${VARNAME}` syntax.:: + + $ cat .env + CONFIG_PATH=${HOME}/.config/foo + + $ pipenv run python + Loading .env environment variables… + Python 3.7.6 (default, Dec 19 2019, 22:52:49) + [GCC 9.2.1 20190827 (Red Hat 9.2.1-1)] on linux + Type "help", "copyright", "credits" or "license" for more information. + >>> import os + >>> os.environ['CONFIG_PATH'] + '/home/kennethreitz/.config/foo' + + This is very useful for keeping production credentials out of your codebase. We do not recommend committing ``.env`` files into source control! @@ -355,6 +370,8 @@ To prevent pipenv from loading the ``.env`` file, set the ``PIPENV_DONT_LOAD_ENV $ PIPENV_DONT_LOAD_ENV=1 pipenv shell +See `theskumar/python-dotenv `_ for more information on ``.env`` files. + ☤ Custom Script Shortcuts ------------------------- diff --git a/news/4100.doc.rst b/news/4100.doc.rst new file mode 100644 index 0000000000..050bdcca96 --- /dev/null +++ b/news/4100.doc.rst @@ -0,0 +1 @@ +More documentation for ``.env`` files From 9ca7543a3255e7f5c4adb1178670fa7a7ddfffc9 Mon Sep 17 00:00:00 2001 From: Denis Otkidach Date: Tue, 11 Feb 2020 19:54:07 +0300 Subject: [PATCH 006/342] Fix setting of PIPENV_RESOLVE_VCS from environ (instroduced in f0e3bbaa) --- pipenv/environments.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pipenv/environments.py b/pipenv/environments.py index 622b76ffce..0ca64834fc 100644 --- a/pipenv/environments.py +++ b/pipenv/environments.py @@ -247,7 +247,11 @@ def _is_env_truthy(name): Defaullts to (w)ipe """ -PIPENV_RESOLVE_VCS = _is_env_truthy(os.environ.get("PIPENV_RESOLVE_VCS", 'true')) +PIPENV_RESOLVE_VCS = ( + os.environ.get("PIPENV_RESOLVE_VCS") is None + or _is_env_truthy("PIPENV_RESOLVE_VCS") +) + """Tells Pipenv whether to resolve all VCS dependencies in full. As of Pipenv 2018.11.26, only editable VCS dependencies were resolved in full. From 603a20bf453cfe798286316467d073799f3a157e Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Thu, 5 Mar 2020 20:48:40 -0500 Subject: [PATCH 007/342] Update links in documentation The canonical Pipenv documentation is now at pipenv.pypa.io. Also, the canonical GitHub repositories for Pipenv and Requests have changed, and some other communications links (Twitter, mailing list, "thank you") were no longer operational. This commit updates those and clarifies that Pipenv is a project maintained by the PyPA. Fixes #4137. Signed-off-by: Sumana Harihareswara --- .github/ISSUE_TEMPLATE/Bug_report.md | 2 +- .gitmodules | 4 ++-- LICENSE | 2 +- README.md | 5 ++--- docs/_templates/sidebarintro.html | 7 +++---- docs/_templates/sidebarlogo.html | 13 ++++++------- docs/advanced.rst | 4 ++-- docs/conf.py | 4 ++-- docs/dev/philosophy.rst | 2 ++ docs/index.rst | 3 --- docs/install.rst | 2 +- news/4137.doc | 1 + pipenv/pipenv.1 | 4 ++-- setup.py | 4 ++-- tests/integration/test_lock.py | 16 ++++++++-------- tests/unit/test_utils.py | 4 ++-- 16 files changed, 37 insertions(+), 40 deletions(-) create mode 100644 news/4137.doc diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 30b70bf344..e516787f61 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -5,7 +5,7 @@ about: Create a report to help us improve Be sure to check the existing issues (both open and closed!), and make sure you are running the latest version of Pipenv. -Check the [diagnose documentation](https://pipenv.kennethreitz.org/en/latest/diagnose/) for common issues before posting! We may close your issue if it is very similar to one of them. Please be considerate, or be on your way. +Check the [diagnose documentation](https://pipenv.pypa.io/en/latest/diagnose/) for common issues before posting! We may close your issue if it is very similar to one of them. Please be considerate, or be on your way. Make sure to mention your debugging experience if the documented solution failed. diff --git a/.gitmodules b/.gitmodules index e2f779afa6..4f0f9fa28d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,7 +6,7 @@ url = https://github.com/pinax/pinax.git [submodule "tests/test_artifacts/git/requests"] path = tests/test_artifacts/git/requests - url = https://github.com/kennethreitz/requests.git + url = https://github.com/psf/requests.git [submodule "tests/test_artifacts/git/six"] path = tests/test_artifacts/git/six url = https://github.com/benjaminp/six.git @@ -24,7 +24,7 @@ url = https://github.com/pallets/flask.git [submodule "tests/test_artifacts/git/requests-2.18.4"] path = tests/test_artifacts/git/requests-2.18.4 - url = https://github.com/kennethreitz/requests + url = https://github.com/psf/requests [submodule "tests/pypi"] path = tests/pypi url = https://github.com/sarugaku/pipenv-test-artifacts.git diff --git a/LICENSE b/LICENSE index f5639bb0e9..ea28562ec2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright 2017 Kenneth Reitz +Copyright 2020 Python Packaging Authority Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index def7281fb4..a8f251db4b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ Pipenv: Python Development Workflow for Humans [![image](https://img.shields.io/pypi/l/pipenv.svg)](https://python.org/pypi/pipenv) [![Azure Pipelines Build Status](https://dev.azure.com/pypa/pipenv/_apis/build/status/Pipenv%20CI?branchName=master)](https://dev.azure.com/pypa/pipenv/_build/latest?definitionId=16&branchName=master) [![image](https://img.shields.io/pypi/pyversions/pipenv.svg)](https://python.org/pypi/pipenv) -[![image](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/kennethreitz) ------------------------------------------------------------------------ @@ -56,7 +55,7 @@ Or, if you\'re using FreeBSD: # pkg install py36-pipenv -Otherwise, refer to the [documentation](https://pipenv.kennethreitz.org/en/latest/#install-pipenv-today) for instructions. +Otherwise, refer to the [documentation](https://pipenv.pypa.io/en/latest/#install-pipenv-today) for instructions. ✨🍰✨ @@ -297,4 +296,4 @@ Use the shell: ☤ Documentation --------------- -Documentation resides over at [pipenv.org](https://pipenv.kennethreitz.org/en/latest/). +Documentation resides over at [pipenv.pypa.io](https://pipenv.pypa.io/en/latest/). diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 67c83d0e49..cbf7e5f85b 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -37,9 +37,8 @@

Stay Informed

Receive updates on new releases and upcoming projects.

-

-

Say Thanks!

-

Join Mailing List.

+

+

Join Mailing List.

Other Projects

@@ -47,7 +46,7 @@

Other Projects

  • Pipenv-Pipes
  • -

    More Kenneth Reitz projects:

    +

    More projects founded by Kenneth Reitz:

    • pep8.org
    • httpbin.org
    • diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html index d574633f5f..59e8641e8e 100644 --- a/docs/_templates/sidebarlogo.html +++ b/docs/_templates/sidebarlogo.html @@ -38,12 +38,11 @@

      Stay Informed

      Receive updates on new releases and upcoming projects.

      -

      -

      -

      Say Thanks!

      -

      Join Mailing List.

      +

      +

      Join Mailing List.

      Other Projects

      @@ -51,7 +50,7 @@

      Other Projects

    • Pipenv-Pipes
    -

    More Kenneth Reitz projects:

    +

    More projects founded by Kenneth Reitz: