Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements requirements command as per #4959 #5013

Merged
merged 15 commits into from
Apr 5, 2022
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ sphinxcontrib-spelling = "<4.3.0"

[scripts]
tests = "bash ./run-tests.sh"
test = "pytest -vvs"

[pipenv]
allow_prereleases = true
32 changes: 18 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
37 changes: 35 additions & 2 deletions docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -199,19 +199,45 @@ 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::

$ pipenv lock -r --dev-only
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.

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
Expand All @@ -221,7 +247,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
---------------------------------------
Expand Down
1 change: 1 addition & 0 deletions news/4959.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implements a `pipenv requirements` command which generates a requirements.txt compatible output without locking.
24 changes: 24 additions & 0 deletions pipenv/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,5 +709,29 @@ 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.")
ImreC marked this conversation as resolved.
Show resolved Hide resolved
@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']])))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should output the index verbatim as specified in the Pipfile, without expanding variables. It is fairly common to specify credentials for a private PyPI this way, and writing those into the resulting requirements.txt file is counter-productive.

I'm not sure whether your existing code is already doing this or not, just wanted to mention this need.

if not dev_only:
for req_name, value in lockfile['default'].items():
hashes = [f' \\\n --hash={h}' for h in value['hashes'] if hash]
echo(crayons.normal(''.join([req_name, value['version'], *hashes])))
if dev or dev_only:
for req_name, value in lockfile['develop'].items():
hashes = [f' \\\n --hash={h}' for h in value['hashes'] if hash]
ImreC marked this conversation as resolved.
Show resolved Hide resolved
echo(crayons.normal(''.join([req_name, value['version'], *hashes])))
sys.exit(0)


if __name__ == "__main__":
cli()
68 changes: 68 additions & 0 deletions tests/integration/test_requirements.py
Original file line number Diff line number Diff line change
@@ -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://https://$USERNAME:${{PASSWORD}}@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://$USERNAME:${PASSWORD}@some_other_source.org' in c.stdout