diff --git a/docs/advanced.rst b/docs/advanced.rst index f1297388..be2d8905 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -38,6 +38,23 @@ If you'd like a specific package to be installed with a specific package index, Very fancy. +☤ Injecting credentials into Pipfiles via environment variables +----------------------------------------------------------------- + + +Pipenv will expand environment variables (if defined) in your Pipfile. Quite +useful if you need to authenticate to a private PyPI:: + + [[source]] + url = "https://$USERNAME:${PASSWORD}@mypypi.example.com/simple" + verify_ssl = true + name = "pypi" + +Luckily - pipenv will hash your Pipfile *before* expanding environment +variables (and, helpfully, will substitute the environment variables again when +you install from the lock file - so no need to commit any secrets! Woo!) + + ☤ Specifying Basically Anything ------------------------------- diff --git a/pipenv/core.py b/pipenv/core.py index d26356ca..3e7c570d 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import contextlib -import codecs import logging import os import sys @@ -1312,20 +1311,14 @@ def do_init( ) # Write out the lockfile if it doesn't exist, but not if the Pipfile is being ignored if (project.lockfile_exists and not ignore_pipfile) and not skip_lock: - # Open the lockfile. - with codecs.open(project.lockfile_location, 'r') as f: - lockfile = simplejson.load(f) - # Update the lockfile if it is out-of-date. - p = pipfile.load(project.pipfile_location) - # Check that the hash of the Lockfile matches the lockfile's hash. - if not lockfile['_meta'].get('hash', {}).get('sha256') == p.hash: - old_hash = lockfile['_meta'].get('hash', {}).get('sha256')[-6:] - new_hash = p.hash[-6:] + old_hash = project.get_lockfile_hash() + new_hash = project.calculate_pipfile_hash() + if new_hash != old_hash: if deploy: click.echo( crayons.red( 'Your Pipfile.lock ({0}) is out of date. Expected: ({1}).'.format( - old_hash, new_hash + old_hash[-6:], new_hash[-6:] ) ) ) @@ -1338,7 +1331,7 @@ def do_init( click.echo( crayons.red( u'Pipfile.lock ({0}) out of date, updating to ({1})…'.format( - old_hash, new_hash + old_hash[-6:], new_hash[-6:] ), bold=True, ), @@ -1655,19 +1648,13 @@ def ensure_lockfile(keep_outdated=False): keep_outdated = project.settings.get('keep_outdated') # Write out the lockfile if it doesn't exist, but not if the Pipfile is being ignored if project.lockfile_exists: - # Open the lockfile. - with codecs.open(project.lockfile_location, 'r') as f: - lockfile = simplejson.load(f) - # Update the lockfile if it is out-of-date. - p = pipfile.load(project.pipfile_location) - # Check that the hash of the Lockfile matches the lockfile's hash. - if not lockfile['_meta'].get('hash', {}).get('sha256') == p.hash: - old_hash = lockfile['_meta'].get('hash', {}).get('sha256')[-6:] - new_hash = p.hash[-6:] + old_hash = project.get_lockfile_hash() + new_hash = project.calculate_pipfile_hash() + if new_hash != old_hash: click.echo( crayons.red( u'Pipfile.lock ({0}) out of date, updating to ({1})…'.format( - old_hash, new_hash + old_hash[-6:], new_hash[-6:] ), bold=True, ), diff --git a/pipenv/project.py b/pipenv/project.py index 9e5aeda4..747c6ac7 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import codecs import json import os import re @@ -640,3 +641,16 @@ class Project(object): def recase_pipfile(self): self.write_toml(recase_file(self._pipfile)) + + def get_lockfile_hash(self): + if not os.path.exists(self.lockfile_location): + return + # Open the lockfile. + with codecs.open(self.lockfile_location, 'r') as f: + lockfile = json.load(f) + return lockfile['_meta'].get('hash', {}).get('sha256') + + def calculate_pipfile_hash(self): + # Update the lockfile if it is out-of-date. + p = pipfile.load(self.pipfile_location, inject_env=False) + return p.hash diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py index 3a80e30e..db1575d1 100644 --- a/tests/test_pipenv.py +++ b/tests/test_pipenv.py @@ -12,6 +12,7 @@ from pipenv.utils import ( ) from pipenv.vendor import toml from pipenv.vendor import delegator +from pipenv.patched import pipfile from pipenv.project import Project from pipenv.vendor.six import PY2 if PY2: @@ -1118,3 +1119,37 @@ requests = "==2.14.0" with PipenvInstance(pypi=pypi) as p: c = p.pipenv('clean') assert c.return_code == 0 + + + @pytest.mark.install + def test_environment_variable_value_does_not_change_hash(self, pypi, monkeypatch): + with PipenvInstance(chdir=True, pypi=pypi) as p: + with open(p.pipfile_path, 'w') as f: + f.write(""" +[[source]] +url = 'https://${PYPI_USERNAME}:${PYPI_PASSWORD}@pypi.python.org/simple' +verify_ssl = true +name = 'pypi' + +[requires] +python_version = '2.7' + +[packages] +flask = "==0.12.2" +""") + monkeypatch.setitem(os.environ, 'PYPI_USERNAME', 'whatever') + monkeypatch.setitem(os.environ, 'PYPI_PASSWORD', 'pass') + assert Project().get_lockfile_hash() is None + c = p.pipenv('install') + lock_hash = Project().get_lockfile_hash() + assert lock_hash is not None + assert lock_hash == Project().calculate_pipfile_hash() + # sanity check on pytest + assert 'PYPI_USERNAME' not in str(pipfile.load(p.pipfile_path)) + assert c.return_code == 0 + assert Project().get_lockfile_hash() == Project.calculate_pipfile_hash() + monkeypatch.setitem(os.environ, 'PYPI_PASSWORD', 'pass2') + assert Project().get_lockfile_hash() == Project.calculate_pipfile_hash() + with open(p.pipfile_path, 'a') as f: + f.write('requests = "==2.14.0"\n') + assert Project().get_lockfile_hash() != Project.calculate_pipfile_hash()