From 63ac0d03a928962f29420ff33c369f77f9132501 Mon Sep 17 00:00:00 2001 From: ImreC Date: Tue, 5 Apr 2022 14:13:33 +0200 Subject: [PATCH] Implements requirements command as per #4959 (#5013) * Implements reqs command * Add news document * Process comments * Rename newsfile * Adds --dev-only and --hash args * Linting fixes Co-authored-by: Imre Persoonlijk --- Pipfile | 1 + README.md | 32 ++++++------ docs/advanced.rst | 39 ++++++++++++++- news/4959.feature.rst | 1 + pipenv/cli/command.py | 32 ++++++++++++ tests/integration/test_requirements.py | 68 ++++++++++++++++++++++++++ 6 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 news/4959.feature.rst create mode 100644 tests/integration/test_requirements.py diff --git a/Pipfile b/Pipfile index 4e46706b62..461925224f 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,7 @@ atomicwrites = {version = "*", markers="sys_platform == 'win32'"} [scripts] tests = "bash ./run-tests.sh" +test = "pytest -vvs" [pipenv] allow_prereleases = true diff --git a/README.md b/README.md index b0baf4f361..d467892dc3 100644 --- a/README.md +++ b/README.md @@ -185,21 +185,25 @@ Magic shell completions are now enabled! Use a lower-level pip command: $ pipenv run pip freeze + Generate a requirements.txt file (including dev): + $ pipenv requirements --dev > requirements.txt + Commands: - check Checks for security vulnerabilities and against PEP 508 markers - provided in Pipfile. - clean Uninstalls all packages not specified in Pipfile.lock. - graph Displays currently–installed dependency graph information. - install Installs provided packages and adds them to Pipfile, or (if no - packages are given), installs all packages from Pipfile. - lock Generates Pipfile.lock. - open View a given module in your editor. - run Spawns a command installed into the virtualenv. - scripts Displays the shortcuts in the (optional) [scripts] section of - Pipfile. - shell Spawns a shell within the virtualenv. - sync Installs all packages specified in Pipfile.lock. - uninstall Un-installs a provided package and removes it from Pipfile. + check Checks for security vulnerabilities and against PEP 508 markers + provided in Pipfile. + clean Uninstalls all packages not specified in Pipfile.lock. + graph Displays currently–installed dependency graph information. + install Installs provided packages and adds them to Pipfile, or (if no + packages are given), installs all packages from Pipfile. + lock Generates Pipfile.lock. + open View a given module in your editor. + run Spawns a command installed into the virtualenv. + scripts Displays the shortcuts in the (optional) [scripts] section of + Pipfile. + shell Spawns a shell within the virtualenv. + sync Installs all packages specified in Pipfile.lock. + requirements Generates a requirements.txt compatible output directly from Pipfile.lock + uninstall Un-installs a provided package and removes it from Pipfile. Locate the project: diff --git a/docs/advanced.rst b/docs/advanced.rst index fe52751e0f..f7b2da3c1d 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -199,7 +199,7 @@ development dependencies:: py==1.4.34 pytest==3.2.3 -Finally, if you wish to generate a requirements file with only the +If you wish to generate a requirements file with only the development requirements you can do that too, using the ``--dev-only`` flag:: @@ -207,11 +207,39 @@ flag:: py==1.4.34 pytest==3.2.3 +Sometimes, you would want to generate a requirements file based on your current +environment. However, using pipenv lock -r will still do the locking process which +could update package versions. To keep the packages as is, use the ``--keep-outdated`` +flag:: + + $ pipenv lock -r --keep-outdated + chardet==3.0.4 + requests==2.18.4 + certifi==2017.7.27.1 + idna==2.6 + urllib3==1.22 + +Note that using this approach, packages newly added to the Pipfile will still be +included in requirements.txt. If you really want to use Pipfile.lock and +Pipfile.lock only, you can generate the requirements using:: + $ pipenv requirements + chardet==3.0.4 + requests==2.18.4 + certifi==2017.7.27.1 + idna==2.6 + urllib3==1.22 + +This will bypass the locking process completely. As with other commands, +passing ``--dev`` will include both the default and development dependencies. +Passing ``--dev-only`` will include only development dependencies and ``--hash`` will +add package hashes to the output for extra security. + The locked requirements are written to stdout, with shell output redirection used to write them to a file:: $ pipenv lock -r > requirements.txt $ pipenv lock -r --dev-only > dev-requirements.txt + $ pipenv requirements --dev > all-requirements.txt $ cat requirements.txt chardet==3.0.4 requests==2.18.4 @@ -221,7 +249,14 @@ used to write them to a file:: $ cat dev-requirements.txt py==1.4.34 pytest==3.2.3 - + $ cat all-requirements.txt + chardet==3.0.4 + requests==2.18.4 + certifi==2017.7.27.1 + idna==2.6 + urllib3==1.22 + py==1.4.34 + pytest==3.2.3 ☤ Detection of Security Vulnerabilities --------------------------------------- diff --git a/news/4959.feature.rst b/news/4959.feature.rst new file mode 100644 index 0000000000..70911ac38a --- /dev/null +++ b/news/4959.feature.rst @@ -0,0 +1 @@ +Implements a ``pipenv requirements`` command which generates a requirements.txt compatible output without locking. diff --git a/pipenv/cli/command.py b/pipenv/cli/command.py index 99dd30a159..de90881cec 100644 --- a/pipenv/cli/command.py +++ b/pipenv/cli/command.py @@ -746,5 +746,37 @@ def verify(state): sys.exit(0) +@cli.command( + short_help="Generate a requirements.txt from Pipfile.lock.", + context_settings=CONTEXT_SETTINGS, +) +@option("--dev", is_flag=True, default=False, help="Also add development requirements.") +@option( + "--dev-only", is_flag=True, default=False, help="Only add development requirements." +) +@option("--hash", is_flag=True, default=False, help="Add package hashes.") +@pass_state +def requirements(state, dev=False, dev_only=False, hash=False): + lockfile = state.project.lockfile_content + for i, package_index in enumerate(lockfile["_meta"]["sources"]): + prefix = "-i" if i == 0 else "--extra-index-url" + echo(crayons.normal(" ".join([prefix, package_index["url"]]))) + if not dev_only: + for req_name, value in lockfile["default"].items(): + if hash: + hashes = [f" \\\n --hash={h}" for h in value.get("hashes", [])] + else: + hashes = [] + echo(crayons.normal("".join([req_name, value["version"], *hashes]))) + if dev or dev_only: + for req_name, value in lockfile["develop"].items(): + if hash: + hashes = [f" \\\n --hash={h}" for h in value.get("hashes", [])] + else: + hashes = [] + echo(crayons.normal("".join([req_name, value["version"], *hashes]))) + sys.exit(0) + + if __name__ == "__main__": cli() diff --git a/tests/integration/test_requirements.py b/tests/integration/test_requirements.py new file mode 100644 index 0000000000..7eb077f57f --- /dev/null +++ b/tests/integration/test_requirements.py @@ -0,0 +1,68 @@ +import pytest + + +@pytest.mark.requirements +def test_requirements_generates_requirements_from_lockfile(PipenvInstance): + with PipenvInstance(chdir=True) as p: + packages = ('requests', '2.14.0') + dev_packages = ('flask', '0.12.2') + with open(p.pipfile_path, 'w') as f: + contents = f""" + [packages] + {packages[0]}= "=={packages[1]}" + [dev-packages] + {dev_packages[0]}= "=={dev_packages[1]}" + """.strip() + f.write(contents) + p.pipenv('lock') + c = p.pipenv('requirements') + assert c.returncode == 0 + assert f'{packages[0]}=={packages[1]}' in c.stdout + assert f'{dev_packages[0]}=={dev_packages[1]}' not in c.stdout + + d = p.pipenv('requirements --dev') + assert d.returncode == 0 + assert f'{packages[0]}=={packages[1]}' in d.stdout + assert f'{dev_packages[0]}=={dev_packages[1]}' in d.stdout + + e = p.pipenv('requirements --dev-only') + assert e.returncode == 0 + assert f'{packages[0]}=={packages[1]}' not in e.stdout + assert f'{dev_packages[0]}=={dev_packages[1]}' in e.stdout + + e = p.pipenv('requirements --hash') + assert e.returncode == 0 + assert f'{packages[0]}=={packages[1]}' in e.stdout + for value in p.lockfile['default'].values(): + for hash in value['hashes']: + assert f' --hash={hash}' in e.stdout + + +@pytest.mark.requirements +def test_requirements_generates_requirements_from_lockfile_multiple_sources(PipenvInstance): + with PipenvInstance(chdir=True) as p: + packages = ('requests', '2.14.0') + dev_packages = ('flask', '0.12.2') + with open(p.pipfile_path, 'w') as f: + contents = f""" + [[source]] + name = "pypi" + url = "https://pypi.org/simple" + verify_ssl = true + [[source]] + name = "other_source" + url = "https://some_other_source.org" + verify_ssl = true + [packages] + {packages[0]}= "=={packages[1]}" + [dev-packages] + {dev_packages[0]}= "=={dev_packages[1]}" + """.strip() + f.write(contents) + l = p.pipenv('lock') + assert l.returncode == 0 + c = p.pipenv('requirements') + assert c.returncode == 0 + + assert '-i https://pypi.org/simple' in c.stdout + assert '--extra-index-url https://some_other_source.org' in c.stdout