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