| Viewing file:  req_uninstall.py (6.74 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
from __future__ import absolute_import
 import logging
 import os
 import tempfile
 
 from pip.compat import uses_pycache, WINDOWS, cache_from_source
 from pip.exceptions import UninstallationError
 from pip.utils import rmtree, ask, is_local, renames, normalize_path
 from pip.utils.logging import indent_log
 
 
 logger = logging.getLogger(__name__)
 
 
 class UninstallPathSet(object):
 """A set of file paths to be removed in the uninstallation of a
 requirement."""
 def __init__(self, dist):
 self.paths = set()
 self._refuse = set()
 self.pth = {}
 self.dist = dist
 self.save_dir = None
 self._moved_paths = []
 
 def _permitted(self, path):
 """
 Return True if the given path is one we are permitted to
 remove/modify, False otherwise.
 
 """
 return is_local(path)
 
 def add(self, path):
 head, tail = os.path.split(path)
 
 # we normalize the head to resolve parent directory symlinks, but not
 # the tail, since we only want to uninstall symlinks, not their targets
 path = os.path.join(normalize_path(head), os.path.normcase(tail))
 
 if not os.path.exists(path):
 return
 if self._permitted(path):
 self.paths.add(path)
 else:
 self._refuse.add(path)
 
 # __pycache__ files can show up after 'installed-files.txt' is created,
 # due to imports
 if os.path.splitext(path)[1] == '.py' and uses_pycache:
 self.add(cache_from_source(path))
 
 def add_pth(self, pth_file, entry):
 pth_file = normalize_path(pth_file)
 if self._permitted(pth_file):
 if pth_file not in self.pth:
 self.pth[pth_file] = UninstallPthEntries(pth_file)
 self.pth[pth_file].add(entry)
 else:
 self._refuse.add(pth_file)
 
 def compact(self, paths):
 """Compact a path set to contain the minimal number of paths
 necessary to contain all paths in the set. If /a/path/ and
 /a/path/to/a/file.txt are both in the set, leave only the
 shorter path."""
 short_paths = set()
 for path in sorted(paths, key=len):
 if not any([
 (path.startswith(shortpath) and
 path[len(shortpath.rstrip(os.path.sep))] == os.path.sep)
 for shortpath in short_paths]):
 short_paths.add(path)
 return short_paths
 
 def _stash(self, path):
 return os.path.join(
 self.save_dir, os.path.splitdrive(path)[1].lstrip(os.path.sep))
 
 def remove(self, auto_confirm=False):
 """Remove paths in ``self.paths`` with confirmation (unless
 ``auto_confirm`` is True)."""
 if not self.paths:
 logger.info(
 "Can't uninstall '%s'. No files were found to uninstall.",
 self.dist.project_name,
 )
 return
 logger.info(
 'Uninstalling %s-%s:',
 self.dist.project_name, self.dist.version
 )
 
 with indent_log():
 paths = sorted(self.compact(self.paths))
 
 if auto_confirm:
 response = 'y'
 else:
 for path in paths:
 logger.info(path)
 response = ask('Proceed (y/n)? ', ('y', 'n'))
 if self._refuse:
 logger.info('Not removing or modifying (outside of prefix):')
 for path in self.compact(self._refuse):
 logger.info(path)
 if response == 'y':
 self.save_dir = tempfile.mkdtemp(suffix='-uninstall',
 prefix='pip-')
 for path in paths:
 new_path = self._stash(path)
 logger.debug('Removing file or directory %s', path)
 self._moved_paths.append(path)
 renames(path, new_path)
 for pth in self.pth.values():
 pth.remove()
 logger.info(
 'Successfully uninstalled %s-%s',
 self.dist.project_name, self.dist.version
 )
 
 def rollback(self):
 """Rollback the changes previously made by remove()."""
 if self.save_dir is None:
 logger.error(
 "Can't roll back %s; was not uninstalled",
 self.dist.project_name,
 )
 return False
 logger.info('Rolling back uninstall of %s', self.dist.project_name)
 for path in self._moved_paths:
 tmp_path = self._stash(path)
 logger.debug('Replacing %s', path)
 renames(tmp_path, path)
 for pth in self.pth.values():
 pth.rollback()
 
 def commit(self):
 """Remove temporary save dir: rollback will no longer be possible."""
 if self.save_dir is not None:
 rmtree(self.save_dir)
 self.save_dir = None
 self._moved_paths = []
 
 
 class UninstallPthEntries(object):
 def __init__(self, pth_file):
 if not os.path.isfile(pth_file):
 raise UninstallationError(
 "Cannot remove entries from nonexistent file %s" % pth_file
 )
 self.file = pth_file
 self.entries = set()
 self._saved_lines = None
 
 def add(self, entry):
 entry = os.path.normcase(entry)
 # On Windows, os.path.normcase converts the entry to use
 # backslashes.  This is correct for entries that describe absolute
 # paths outside of site-packages, but all the others use forward
 # slashes.
 if WINDOWS and not os.path.splitdrive(entry)[0]:
 entry = entry.replace('\\', '/')
 self.entries.add(entry)
 
 def remove(self):
 logger.debug('Removing pth entries from %s:', self.file)
 with open(self.file, 'rb') as fh:
 # windows uses '\r\n' with py3k, but uses '\n' with py2.x
 lines = fh.readlines()
 self._saved_lines = lines
 if any(b'\r\n' in line for line in lines):
 endline = '\r\n'
 else:
 endline = '\n'
 for entry in self.entries:
 try:
 logger.debug('Removing entry: %s', entry)
 lines.remove((entry + endline).encode("utf-8"))
 except ValueError:
 pass
 with open(self.file, 'wb') as fh:
 fh.writelines(lines)
 
 def rollback(self):
 if self._saved_lines is None:
 logger.error(
 'Cannot roll back changes to %s, none were made', self.file
 )
 return False
 logger.debug('Rolling %s back to previous state', self.file)
 with open(self.file, 'wb') as fh:
 fh.writelines(self._saved_lines)
 return True
 
 |