diff --git a/pipenv/core.py b/pipenv/core.py index 8965668d..f6bebbcb 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -25,6 +25,7 @@ import six from .cmdparse import ScriptEmptyError from .project import Project, SourceNotFound from .utils import ( + atomic_open_for_write, convert_deps_from_pip, convert_deps_to_pip, is_required_version, @@ -1005,8 +1006,6 @@ def do_lock( ) sys.exit(1) cached_lockfile = project.lockfile_content - if write: - project.destroy_lockfile() if write: # Alert the user of progress. click.echo( @@ -1167,7 +1166,7 @@ def do_lock( ] if write: # Write out the lockfile. - with open(project.lockfile_location, 'w') as f: + with atomic_open_for_write(project.lockfile_location) as f: simplejson.dump( lockfile, f, indent=4, separators=(',', ': '), sort_keys=True ) diff --git a/pipenv/project.py b/pipenv/project.py index 91e8c38a..be489a10 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -673,14 +673,6 @@ class Project(object): return found_source raise SourceNotFound(name or url) - def destroy_lockfile(self): - """Deletes the lockfile.""" - try: - return os.remove(self.lockfile_location) - - except OSError: - pass - def get_package_name_in_pipfile(self, package_name, dev=False): """Get the equivalent package name in pipfile""" key = 'dev-packages' if dev else 'packages' diff --git a/pipenv/utils.py b/pipenv/utils.py index 10e3ae75..2c815b6f 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -1323,3 +1323,44 @@ def split_argument(req, short=None, long_=None): index, more_req = remaining_line[0], ' '.join(remaining_line[1:]) req = '{0} {1}'.format(req, more_req) return req, index + + +@contextmanager +def atomic_open_for_write(target, binary=False): + """Atomically open `target` for writing. + + This is based on Lektor's `atomic_open()` utility, but simplified a lot + to handle only writing, and skip many multi-process/thread edge cases + handled by Werkzeug. + + How this works: + + * Create a temp file (in the same directory of the actual target), and + yield for surrounding code to write to it. + * If some thing goes wrong, try to remove the temp file. The actual target + is not touched whatsoever. + * If everything goes well, close the temp file, and replace the actual + target with this new file. + """ + fd, tmp = tempfile.mkstemp( + dir=os.path.dirname(target), + prefix='.__atomic-write', + ) + os.chmod(tmp, 0o644) + f = os.fdopen(fd, 'wb' if binary else 'w') + try: + yield f + except BaseException: + f.close() + try: + os.remove(tmp) + except OSError: + pass + raise + else: + f.close() + try: + os.remove(target) # This is needed on Windows. + except OSError: + pass + os.rename(tmp, target) # No os.replace() on Python 2.