Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

req_uninstall.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. from __future__ import absolute_import
  2. import csv
  3. import functools
  4. import logging
  5. import os
  6. import sys
  7. import sysconfig
  8. from pip._vendor import pkg_resources
  9. from pip._internal.exceptions import UninstallationError
  10. from pip._internal.locations import bin_py, bin_user
  11. from pip._internal.utils.compat import WINDOWS, cache_from_source, uses_pycache
  12. from pip._internal.utils.logging import indent_log
  13. from pip._internal.utils.misc import (
  14. FakeFile, ask, dist_in_usersite, dist_is_local, egg_link_path, is_local,
  15. normalize_path, renames,
  16. )
  17. from pip._internal.utils.temp_dir import TempDirectory
  18. logger = logging.getLogger(__name__)
  19. def _script_names(dist, script_name, is_gui):
  20. """Create the fully qualified name of the files created by
  21. {console,gui}_scripts for the given ``dist``.
  22. Returns the list of file names
  23. """
  24. if dist_in_usersite(dist):
  25. bin_dir = bin_user
  26. else:
  27. bin_dir = bin_py
  28. exe_name = os.path.join(bin_dir, script_name)
  29. paths_to_remove = [exe_name]
  30. if WINDOWS:
  31. paths_to_remove.append(exe_name + '.exe')
  32. paths_to_remove.append(exe_name + '.exe.manifest')
  33. if is_gui:
  34. paths_to_remove.append(exe_name + '-script.pyw')
  35. else:
  36. paths_to_remove.append(exe_name + '-script.py')
  37. return paths_to_remove
  38. def _unique(fn):
  39. @functools.wraps(fn)
  40. def unique(*args, **kw):
  41. seen = set()
  42. for item in fn(*args, **kw):
  43. if item not in seen:
  44. seen.add(item)
  45. yield item
  46. return unique
  47. @_unique
  48. def uninstallation_paths(dist):
  49. """
  50. Yield all the uninstallation paths for dist based on RECORD-without-.py[co]
  51. Yield paths to all the files in RECORD. For each .py file in RECORD, add
  52. the .pyc and .pyo in the same directory.
  53. UninstallPathSet.add() takes care of the __pycache__ .py[co].
  54. """
  55. r = csv.reader(FakeFile(dist.get_metadata_lines('RECORD')))
  56. for row in r:
  57. path = os.path.join(dist.location, row[0])
  58. yield path
  59. if path.endswith('.py'):
  60. dn, fn = os.path.split(path)
  61. base = fn[:-3]
  62. path = os.path.join(dn, base + '.pyc')
  63. yield path
  64. path = os.path.join(dn, base + '.pyo')
  65. yield path
  66. def compact(paths):
  67. """Compact a path set to contain the minimal number of paths
  68. necessary to contain all paths in the set. If /a/path/ and
  69. /a/path/to/a/file.txt are both in the set, leave only the
  70. shorter path."""
  71. sep = os.path.sep
  72. short_paths = set()
  73. for path in sorted(paths, key=len):
  74. should_add = any(
  75. path.startswith(shortpath.rstrip("*")) and
  76. path[len(shortpath.rstrip("*").rstrip(sep))] == sep
  77. for shortpath in short_paths
  78. )
  79. if not should_add:
  80. short_paths.add(path)
  81. return short_paths
  82. def compress_for_output_listing(paths):
  83. """Returns a tuple of 2 sets of which paths to display to user
  84. The first set contains paths that would be deleted. Files of a package
  85. are not added and the top-level directory of the package has a '*' added
  86. at the end - to signify that all it's contents are removed.
  87. The second set contains files that would have been skipped in the above
  88. folders.
  89. """
  90. will_remove = list(paths)
  91. will_skip = set()
  92. # Determine folders and files
  93. folders = set()
  94. files = set()
  95. for path in will_remove:
  96. if path.endswith(".pyc"):
  97. continue
  98. if path.endswith("__init__.py") or ".dist-info" in path:
  99. folders.add(os.path.dirname(path))
  100. files.add(path)
  101. _normcased_files = set(map(os.path.normcase, files))
  102. folders = compact(folders)
  103. # This walks the tree using os.walk to not miss extra folders
  104. # that might get added.
  105. for folder in folders:
  106. for dirpath, _, dirfiles in os.walk(folder):
  107. for fname in dirfiles:
  108. if fname.endswith(".pyc"):
  109. continue
  110. file_ = os.path.join(dirpath, fname)
  111. if (os.path.isfile(file_) and
  112. os.path.normcase(file_) not in _normcased_files):
  113. # We are skipping this file. Add it to the set.
  114. will_skip.add(file_)
  115. will_remove = files | {
  116. os.path.join(folder, "*") for folder in folders
  117. }
  118. return will_remove, will_skip
  119. class UninstallPathSet(object):
  120. """A set of file paths to be removed in the uninstallation of a
  121. requirement."""
  122. def __init__(self, dist):
  123. self.paths = set()
  124. self._refuse = set()
  125. self.pth = {}
  126. self.dist = dist
  127. self.save_dir = TempDirectory(kind="uninstall")
  128. self._moved_paths = []
  129. def _permitted(self, path):
  130. """
  131. Return True if the given path is one we are permitted to
  132. remove/modify, False otherwise.
  133. """
  134. return is_local(path)
  135. def add(self, path):
  136. head, tail = os.path.split(path)
  137. # we normalize the head to resolve parent directory symlinks, but not
  138. # the tail, since we only want to uninstall symlinks, not their targets
  139. path = os.path.join(normalize_path(head), os.path.normcase(tail))
  140. if not os.path.exists(path):
  141. return
  142. if self._permitted(path):
  143. self.paths.add(path)
  144. else:
  145. self._refuse.add(path)
  146. # __pycache__ files can show up after 'installed-files.txt' is created,
  147. # due to imports
  148. if os.path.splitext(path)[1] == '.py' and uses_pycache:
  149. self.add(cache_from_source(path))
  150. def add_pth(self, pth_file, entry):
  151. pth_file = normalize_path(pth_file)
  152. if self._permitted(pth_file):
  153. if pth_file not in self.pth:
  154. self.pth[pth_file] = UninstallPthEntries(pth_file)
  155. self.pth[pth_file].add(entry)
  156. else:
  157. self._refuse.add(pth_file)
  158. def _stash(self, path):
  159. return os.path.join(
  160. self.save_dir.path, os.path.splitdrive(path)[1].lstrip(os.path.sep)
  161. )
  162. def remove(self, auto_confirm=False, verbose=False):
  163. """Remove paths in ``self.paths`` with confirmation (unless
  164. ``auto_confirm`` is True)."""
  165. if not self.paths:
  166. logger.info(
  167. "Can't uninstall '%s'. No files were found to uninstall.",
  168. self.dist.project_name,
  169. )
  170. return
  171. dist_name_version = (
  172. self.dist.project_name + "-" + self.dist.version
  173. )
  174. logger.info('Uninstalling %s:', dist_name_version)
  175. with indent_log():
  176. if auto_confirm or self._allowed_to_proceed(verbose):
  177. self.save_dir.create()
  178. for path in sorted(compact(self.paths)):
  179. new_path = self._stash(path)
  180. logger.debug('Removing file or directory %s', path)
  181. self._moved_paths.append(path)
  182. renames(path, new_path)
  183. for pth in self.pth.values():
  184. pth.remove()
  185. logger.info('Successfully uninstalled %s', dist_name_version)
  186. def _allowed_to_proceed(self, verbose):
  187. """Display which files would be deleted and prompt for confirmation
  188. """
  189. def _display(msg, paths):
  190. if not paths:
  191. return
  192. logger.info(msg)
  193. with indent_log():
  194. for path in sorted(compact(paths)):
  195. logger.info(path)
  196. if not verbose:
  197. will_remove, will_skip = compress_for_output_listing(self.paths)
  198. else:
  199. # In verbose mode, display all the files that are going to be
  200. # deleted.
  201. will_remove = list(self.paths)
  202. will_skip = set()
  203. _display('Would remove:', will_remove)
  204. _display('Would not remove (might be manually added):', will_skip)
  205. _display('Would not remove (outside of prefix):', self._refuse)
  206. return ask('Proceed (y/n)? ', ('y', 'n')) == 'y'
  207. def rollback(self):
  208. """Rollback the changes previously made by remove()."""
  209. if self.save_dir.path is None:
  210. logger.error(
  211. "Can't roll back %s; was not uninstalled",
  212. self.dist.project_name,
  213. )
  214. return False
  215. logger.info('Rolling back uninstall of %s', self.dist.project_name)
  216. for path in self._moved_paths:
  217. tmp_path = self._stash(path)
  218. logger.debug('Replacing %s', path)
  219. renames(tmp_path, path)
  220. for pth in self.pth.values():
  221. pth.rollback()
  222. def commit(self):
  223. """Remove temporary save dir: rollback will no longer be possible."""
  224. self.save_dir.cleanup()
  225. self._moved_paths = []
  226. @classmethod
  227. def from_dist(cls, dist):
  228. dist_path = normalize_path(dist.location)
  229. if not dist_is_local(dist):
  230. logger.info(
  231. "Not uninstalling %s at %s, outside environment %s",
  232. dist.key,
  233. dist_path,
  234. sys.prefix,
  235. )
  236. return cls(dist)
  237. if dist_path in {p for p in {sysconfig.get_path("stdlib"),
  238. sysconfig.get_path("platstdlib")}
  239. if p}:
  240. logger.info(
  241. "Not uninstalling %s at %s, as it is in the standard library.",
  242. dist.key,
  243. dist_path,
  244. )
  245. return cls(dist)
  246. paths_to_remove = cls(dist)
  247. develop_egg_link = egg_link_path(dist)
  248. develop_egg_link_egg_info = '{}.egg-info'.format(
  249. pkg_resources.to_filename(dist.project_name))
  250. egg_info_exists = dist.egg_info and os.path.exists(dist.egg_info)
  251. # Special case for distutils installed package
  252. distutils_egg_info = getattr(dist._provider, 'path', None)
  253. # Uninstall cases order do matter as in the case of 2 installs of the
  254. # same package, pip needs to uninstall the currently detected version
  255. if (egg_info_exists and dist.egg_info.endswith('.egg-info') and
  256. not dist.egg_info.endswith(develop_egg_link_egg_info)):
  257. # if dist.egg_info.endswith(develop_egg_link_egg_info), we
  258. # are in fact in the develop_egg_link case
  259. paths_to_remove.add(dist.egg_info)
  260. if dist.has_metadata('installed-files.txt'):
  261. for installed_file in dist.get_metadata(
  262. 'installed-files.txt').splitlines():
  263. path = os.path.normpath(
  264. os.path.join(dist.egg_info, installed_file)
  265. )
  266. paths_to_remove.add(path)
  267. # FIXME: need a test for this elif block
  268. # occurs with --single-version-externally-managed/--record outside
  269. # of pip
  270. elif dist.has_metadata('top_level.txt'):
  271. if dist.has_metadata('namespace_packages.txt'):
  272. namespaces = dist.get_metadata('namespace_packages.txt')
  273. else:
  274. namespaces = []
  275. for top_level_pkg in [
  276. p for p
  277. in dist.get_metadata('top_level.txt').splitlines()
  278. if p and p not in namespaces]:
  279. path = os.path.join(dist.location, top_level_pkg)
  280. paths_to_remove.add(path)
  281. paths_to_remove.add(path + '.py')
  282. paths_to_remove.add(path + '.pyc')
  283. paths_to_remove.add(path + '.pyo')
  284. elif distutils_egg_info:
  285. raise UninstallationError(
  286. "Cannot uninstall {!r}. It is a distutils installed project "
  287. "and thus we cannot accurately determine which files belong "
  288. "to it which would lead to only a partial uninstall.".format(
  289. dist.project_name,
  290. )
  291. )
  292. elif dist.location.endswith('.egg'):
  293. # package installed by easy_install
  294. # We cannot match on dist.egg_name because it can slightly vary
  295. # i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
  296. paths_to_remove.add(dist.location)
  297. easy_install_egg = os.path.split(dist.location)[1]
  298. easy_install_pth = os.path.join(os.path.dirname(dist.location),
  299. 'easy-install.pth')
  300. paths_to_remove.add_pth(easy_install_pth, './' + easy_install_egg)
  301. elif egg_info_exists and dist.egg_info.endswith('.dist-info'):
  302. for path in uninstallation_paths(dist):
  303. paths_to_remove.add(path)
  304. elif develop_egg_link:
  305. # develop egg
  306. with open(develop_egg_link, 'r') as fh:
  307. link_pointer = os.path.normcase(fh.readline().strip())
  308. assert (link_pointer == dist.location), (
  309. 'Egg-link %s does not match installed location of %s '
  310. '(at %s)' % (link_pointer, dist.project_name, dist.location)
  311. )
  312. paths_to_remove.add(develop_egg_link)
  313. easy_install_pth = os.path.join(os.path.dirname(develop_egg_link),
  314. 'easy-install.pth')
  315. paths_to_remove.add_pth(easy_install_pth, dist.location)
  316. else:
  317. logger.debug(
  318. 'Not sure how to uninstall: %s - Check: %s',
  319. dist, dist.location,
  320. )
  321. # find distutils scripts= scripts
  322. if dist.has_metadata('scripts') and dist.metadata_isdir('scripts'):
  323. for script in dist.metadata_listdir('scripts'):
  324. if dist_in_usersite(dist):
  325. bin_dir = bin_user
  326. else:
  327. bin_dir = bin_py
  328. paths_to_remove.add(os.path.join(bin_dir, script))
  329. if WINDOWS:
  330. paths_to_remove.add(os.path.join(bin_dir, script) + '.bat')
  331. # find console_scripts
  332. _scripts_to_remove = []
  333. console_scripts = dist.get_entry_map(group='console_scripts')
  334. for name in console_scripts.keys():
  335. _scripts_to_remove.extend(_script_names(dist, name, False))
  336. # find gui_scripts
  337. gui_scripts = dist.get_entry_map(group='gui_scripts')
  338. for name in gui_scripts.keys():
  339. _scripts_to_remove.extend(_script_names(dist, name, True))
  340. for s in _scripts_to_remove:
  341. paths_to_remove.add(s)
  342. return paths_to_remove
  343. class UninstallPthEntries(object):
  344. def __init__(self, pth_file):
  345. if not os.path.isfile(pth_file):
  346. raise UninstallationError(
  347. "Cannot remove entries from nonexistent file %s" % pth_file
  348. )
  349. self.file = pth_file
  350. self.entries = set()
  351. self._saved_lines = None
  352. def add(self, entry):
  353. entry = os.path.normcase(entry)
  354. # On Windows, os.path.normcase converts the entry to use
  355. # backslashes. This is correct for entries that describe absolute
  356. # paths outside of site-packages, but all the others use forward
  357. # slashes.
  358. if WINDOWS and not os.path.splitdrive(entry)[0]:
  359. entry = entry.replace('\\', '/')
  360. self.entries.add(entry)
  361. def remove(self):
  362. logger.debug('Removing pth entries from %s:', self.file)
  363. with open(self.file, 'rb') as fh:
  364. # windows uses '\r\n' with py3k, but uses '\n' with py2.x
  365. lines = fh.readlines()
  366. self._saved_lines = lines
  367. if any(b'\r\n' in line for line in lines):
  368. endline = '\r\n'
  369. else:
  370. endline = '\n'
  371. # handle missing trailing newline
  372. if lines and not lines[-1].endswith(endline.encode("utf-8")):
  373. lines[-1] = lines[-1] + endline.encode("utf-8")
  374. for entry in self.entries:
  375. try:
  376. logger.debug('Removing entry: %s', entry)
  377. lines.remove((entry + endline).encode("utf-8"))
  378. except ValueError:
  379. pass
  380. with open(self.file, 'wb') as fh:
  381. fh.writelines(lines)
  382. def rollback(self):
  383. if self._saved_lines is None:
  384. logger.error(
  385. 'Cannot roll back changes to %s, none were made', self.file
  386. )
  387. return False
  388. logger.debug('Rolling %s back to previous state', self.file)
  389. with open(self.file, 'wb') as fh:
  390. fh.writelines(self._saved_lines)
  391. return True