6from typing
import Any, Callable, Dict, Generator, Iterable, List, Optional, Set, Tuple
18logger = getLogger(__name__)
22 bin_dir: str, script_name: str, is_gui: bool
23) -> Generator[str,
None,
None]:
24 """Create the fully qualified name of the files created by
25 {console,gui}_scripts for the given ``dist``.
26 Returns the list of file names
32 yield f
"{exe_name}.exe"
33 yield f
"{exe_name}.exe.manifest"
35 yield f
"{exe_name}-script.pyw"
37 yield f
"{exe_name}-script.py"
41 fn: Callable[..., Generator[Any,
None,
None]]
42) -> Callable[..., Generator[Any,
None,
None]]:
44 def unique(*args: Any, **kw: Any) -> Generator[Any,
None,
None]:
45 seen: Set[Any] = set()
46 for item
in fn(*args, **kw):
57 Yield all the uninstallation paths for dist based on RECORD-without-.py[co]
59 Yield paths to all the files in RECORD. For each .py file in RECORD, add
60 the .pyc and .pyo in the same directory.
62 UninstallPathSet.add() takes care of the __pycache__ .py[co].
64 If RECORD is not found, raises UninstallationError,
65 with possible information from the INSTALLER file.
67 https://packaging.python.org/specifications/recording-installed-packages/
70 assert location
is not None,
"not installed"
74 msg =
"Cannot uninstall {dist}, RECORD file not found.".format(dist=dist)
76 if not installer
or installer ==
"pip":
79 " You might be able to recover from this via: "
80 "'pip install --force-reinstall --no-deps {}'.".format(dep)
83 msg +=
" Hint: The package was installed by {}.".format(installer)
98def compact(paths: Iterable[str]) -> Set[str]:
99 """Compact a path set to contain the minimal number of paths
100 necessary to contain all paths in the set. If /a/path/ and
101 /a/path/to/a/file.txt are both in the set, leave only the
105 short_paths: Set[str] = set()
106 for path
in sorted(paths, key=len):
110 for shortpath
in short_paths
118 """Returns a set containing the paths that need to be renamed.
120 This set may include directories when the original sequence of paths
121 included every file on disk.
124 remaining = set(case_map)
126 wildcards: Set[str] = set()
131 for root
in unchecked:
136 all_files: Set[str] = set()
137 all_subdirs: Set[str] = set()
138 for dirname, subdirs, files
in os.walk(root):
144 if not (all_files - remaining):
152 """Returns a tuple of 2 sets of which paths to display to user
154 The first set contains paths that would be deleted. Files of a package
155 are not added and the top-level directory of the package has a '*' added
156 at the end - to signify that all it's contents are removed.
158 The second set contains files that would have been skipped in the above
162 will_remove = set(paths)
168 for path
in will_remove:
182 for folder
in folders:
183 for dirpath, _, dirfiles
in os.walk(folder):
184 for fname
in dirfiles:
196 will_remove = files | {
os.path.join(folder,
"*")
for folder
in folders}
198 return will_remove, will_skip
202 """A set of file rename operations to stash files while
203 tentatively uninstalling them."""
208 self.
_save_dirs: Dict[str, TempDirectory] = {}
211 self.
_moves: List[Tuple[str, str]] = []
214 """Stashes a directory.
216 Directories are stashed adjacent to their original location if
217 possible, or else moved/copied into the user's temp dir."""
230 If no root has been provided, one will be created for the directory
231 in the user's temp directory."""
236 while head != old_head:
255 """Stashes the directory or file and returns its new location.
256 Handle symlinks as files to avoid modifying the symlink targets.
264 self.
_moves.append((path, new_path))
272 renames(path, new_path)
276 """Commits the uninstall by removing stashed files."""
283 """Undoes the uninstall by moving stashed files back."""
287 for new_path, path
in self.
_moves:
294 renames(path, new_path)
295 except OSError
as ex:
307 """A set of file paths to be removed in the uninstallation of a
310 def __init__(self, dist: BaseDistribution) ->
None:
311 self.
_paths: Set[str] = set()
313 self._pth: Dict[str, UninstallPthEntries] = {}
323 Return True if the given path is one we are permitted to
324 remove/modify, False otherwise.
328 if not running_under_virtualenv():
332 def add(self, path: str) ->
None:
349 self.
add(cache_from_source(path))
351 def add_pth(self, pth_file: str, entry: str) ->
None:
354 if pth_file
not in self._pth:
356 self._pth[pth_file].
add(entry)
360 def remove(self, auto_confirm: bool =
False, verbose: bool =
False) ->
None:
361 """Remove paths in ``self._paths`` with confirmation (unless
362 ``auto_confirm`` is True)."""
366 "Can't uninstall '%s'. No files were found to uninstall.",
371 dist_name_version = f
"{self._dist.raw_name}-{self._dist.version}"
380 for path
in sorted(
compact(for_rename)):
384 for pth
in self._pth.values():
387 logger.info(
"Successfully uninstalled %s", dist_name_version)
390 """Display which files would be deleted and prompt for confirmation"""
392 def _display(msg: str, paths: Iterable[str]) ->
None:
398 for path
in sorted(
compact(paths)):
406 will_remove = set(self.
_paths)
409 _display(
"Would remove:", will_remove)
410 _display(
"Would not remove (might be manually added):", will_skip)
415 return ask(
"Proceed (Y/n)? ", (
"y",
"n",
"")) !=
"n"
418 """Rollback the changes previously made by remove()."""
421 "Can't roll back %s; was not uninstalled",
427 for pth
in self._pth.values():
431 """Remove temporary save dir: rollback will no longer be possible."""
435 def from_dist(cls, dist: BaseDistribution) ->
"UninstallPathSet":
438 if dist_location
is None:
440 "Not uninstalling %s since it is not installed",
445 normalized_dist_location = normalize_path(dist_location)
448 "Not uninstalling %s at %s, outside environment %s",
450 normalized_dist_location,
455 if normalized_dist_location
in {
461 "Not uninstalling %s at %s, as it is in the standard library.",
463 normalized_dist_location,
467 paths_to_remove =
cls(dist)
468 develop_egg_link = egg_link_path_from_location(
dist.raw_name)
473 setuptools_flat_installation = (
475 and info_location
is not None
484 if setuptools_flat_installation:
485 if info_location
is not None:
488 if installed_files
is not None:
489 for installed_file
in installed_files:
497 except FileNotFoundError:
501 for top_level_pkg
in [
504 if p
and p
not in namespaces
514 "Cannot uninstall {!r}. It is a distutils installed project "
515 "and thus we cannot accurately determine which files belong "
516 "to it which would lead to only a partial uninstall.".format(
537 elif develop_egg_link:
540 with open(develop_egg_link)
as fh:
546 normalized_link_pointer, normalized_dist_location
548 f
"Egg-link {develop_egg_link} (to {link_pointer}) does not match "
549 f
"installed location of {dist.raw_name} (at {dist_location})"
559 "Not sure how to uninstall: %s - Check: %s",
565 bin_dir = get_bin_user()
567 bin_dir = get_bin_prefix()
575 except (FileNotFoundError, NotADirectoryError):
580 dist: BaseDistribution,
582 ) -> Generator[str,
None,
None]:
592 return paths_to_remove
598 self.entries: Set[str] = set()
601 def add(self, entry: str) ->
None:
614 self.entries.
add(entry)
623 with open(self.
file,
"rb")
as fh:
627 if any(b
"\r\n" in line
for line
in lines):
634 for entry
in self.entries:
640 with open(self.
file,
"wb")
as fh:
648 with open(self.
file,
"wb")
as fh:
str _get_file_stash(self, str path)
str _get_directory_stash(self, str path)
str stash(self, str path)
None __init__(self, BaseDistribution dist)
None remove(self, bool auto_confirm=False, bool verbose=False)
bool _allowed_to_proceed(self, bool verbose)
"UninstallPathSet" from_dist(cls, BaseDistribution dist)
None add_pth(self, str pth_file, str entry)
bool _permitted(self, str path)
None add(self, str entry)
None __init__(self, str pth_file)
Set[str] compress_for_rename(Iterable[str] paths)
Generator[str, None, None] uninstallation_paths(BaseDistribution dist)
Tuple[Set[str], Set[str]] compress_for_output_listing(Iterable[str] paths)
Set[str] compact(Iterable[str] paths)
Callable[..., Generator[Any, None, None]] _unique(Callable[..., Generator[Any, None, None]] fn)
Generator[str, None, None] _script_names(str bin_dir, str script_name, bool is_gui)