17
17
FakeFile , ask , dist_in_usersite , dist_is_local , egg_link_path , is_local ,
18
18
normalize_path , renames , rmtree ,
19
19
)
20
- from pip ._internal .utils .temp_dir import AdjacentTempDirectory
20
+ from pip ._internal .utils .temp_dir import AdjacentTempDirectory , TempDirectory
21
21
22
22
logger = logging .getLogger (__name__ )
23
23
@@ -183,6 +183,112 @@ def compress_for_output_listing(paths):
183
183
return will_remove , will_skip
184
184
185
185
186
+ class StashedUninstallPathSet (object ):
187
+ """A set of file rename operations to stash files while
188
+ tentatively uninstalling them."""
189
+ def __init__ (self ):
190
+ # Mapping from source file root to [Adjacent]TempDirectory
191
+ # for files under that directory.
192
+ self ._save_dirs = {}
193
+ # (old path, new path) tuples for each move that may need
194
+ # to be undone.
195
+ self ._moves = []
196
+
197
+ def _add_root (self , path ):
198
+ """Create or return the stash directory for a given path.
199
+
200
+ For internal use only. External users should call add_root()"""
201
+ key = os .path .normcase (path ).rstrip ("/\\ " )
202
+
203
+ if key in self ._save_dirs :
204
+ return key , self ._save_dirs [key ]
205
+
206
+ save_dir = AdjacentTempDirectory (path )
207
+ try :
208
+ save_dir .create ()
209
+ except OSError :
210
+ save_dir = TempDirectory (kind = 'uninstall' )
211
+ save_dir .create ()
212
+
213
+ self ._save_dirs [key ] = save_dir
214
+ return key , save_dir
215
+
216
+ def add_root (self , path ):
217
+ """Adds a root directory that we will be moving files from."""
218
+ if not os .path .isdir (path ):
219
+ raise ValueError ("Roots must be directories" )
220
+
221
+ # Keep return values internal
222
+ self ._add_root (path )
223
+
224
+ def _get_stash_path (self , path ):
225
+ """Finds a place to stash the path
226
+
227
+ If no root has been provided, one will be created for the directory
228
+ passed."""
229
+ path = os .path .normcase (path )
230
+ head , old_head = os .path .dirname (path ), None
231
+ best_head , best_save_dir = None , None
232
+
233
+ while head != old_head :
234
+ save_dir = self ._save_dirs .get (head )
235
+ if save_dir :
236
+ break
237
+ head , old_head = os .path .dirname (head ), head
238
+
239
+ if not best_save_dir :
240
+ head = path if os .path .isdir (path ) else os .path .dirname (path )
241
+ best_head , best_save_dir = self ._add_root (head )
242
+
243
+ relpath = os .path .relpath (path , best_head )
244
+ if not relpath or relpath == os .path .curdir :
245
+ return best_save_dir .path
246
+ return os .path .join (best_save_dir .path , relpath )
247
+
248
+ def stash (self , path ):
249
+ """Stashes a file somewhere out of the way."""
250
+ new_path = self ._get_stash_path (path )
251
+ self ._moves .append ((path , new_path ))
252
+ if os .path .isdir (path ) and os .path .isdir (new_path ):
253
+ # If we're moving a directory, we need to
254
+ # remove the destination first or else it will be
255
+ # moved to inside the existing directory.
256
+ # We just created new_path ourselves, so it will
257
+ # be removable.
258
+ os .rmdir (new_path )
259
+ renames (path , new_path )
260
+
261
+ def commit (self ):
262
+ """Commits the uninstall by removing stashed files."""
263
+ for _ , save_dir in self ._save_dirs .items ():
264
+ save_dir .cleanup ()
265
+ self ._moves = []
266
+ self ._save_dirs = {}
267
+
268
+ def rollback (self ):
269
+ """Undoes the uninstall by moving stashed files back."""
270
+ for p in self ._moves :
271
+ logging .info ("Moving to %s\n from %s" , * p )
272
+
273
+ for new_path , path in self ._moves :
274
+ try :
275
+ logger .debug ('Replacing %s from %s' , new_path , path )
276
+ if os .path .isfile (new_path ):
277
+ os .unlink (new_path )
278
+ elif os .path .isdir (new_path ):
279
+ rmtree (new_path )
280
+ renames (path , new_path )
281
+ except OSError as ex :
282
+ logger .error ("Failed to restore %s" , new_path )
283
+ logger .debug ("Exception: %s" , ex )
284
+
285
+ self .commit ()
286
+
287
+ @property
288
+ def can_rollback (self ):
289
+ return bool (self ._moves )
290
+
291
+
186
292
class UninstallPathSet (object ):
187
293
"""A set of file paths to be removed in the uninstallation of a
188
294
requirement."""
@@ -191,8 +297,7 @@ def __init__(self, dist):
191
297
self ._refuse = set ()
192
298
self .pth = {}
193
299
self .dist = dist
194
- self ._save_dirs = []
195
- self ._moved_paths = []
300
+ self ._moved_paths = StashedUninstallPathSet ()
196
301
197
302
def _permitted (self , path ):
198
303
"""
@@ -230,22 +335,6 @@ def add_pth(self, pth_file, entry):
230
335
else :
231
336
self ._refuse .add (pth_file )
232
337
233
- def _stash (self , path ):
234
- best = None
235
- for save_dir in self ._save_dirs :
236
- if not path .startswith (save_dir .original + os .sep ):
237
- continue
238
- if not best or len (save_dir .original ) > len (best .original ):
239
- best = save_dir
240
- if best is None :
241
- best = AdjacentTempDirectory (os .path .dirname (path ))
242
- best .create ()
243
- self ._save_dirs .append (best )
244
- relpath = os .path .relpath (path , best .original )
245
- if not relpath or relpath == os .path .curdir :
246
- return best .path
247
- return os .path .join (best .path , relpath )
248
-
249
338
def remove (self , auto_confirm = False , verbose = False ):
250
339
"""Remove paths in ``self.paths`` with confirmation (unless
251
340
``auto_confirm`` is True)."""
@@ -264,18 +353,18 @@ def remove(self, auto_confirm=False, verbose=False):
264
353
265
354
with indent_log ():
266
355
if auto_confirm or self ._allowed_to_proceed (verbose ):
267
- for path in sorted (compact (compress_for_rename (self .paths ))):
268
- new_path = self ._stash (path )
356
+ moved = self ._moved_paths
357
+
358
+ for_rename = compress_for_rename (self .paths )
359
+
360
+ for path in for_rename :
361
+ if os .path .isdir (path ):
362
+ moved .add_root (path )
363
+
364
+ for path in sorted (compact (for_rename )):
365
+ moved .stash (path )
269
366
logger .debug ('Removing file or directory %s' , path )
270
- self ._moved_paths .append ((path , new_path ))
271
- if os .path .isdir (path ) and os .path .isdir (new_path ):
272
- # If we're moving a directory, we need to
273
- # remove the destination first or else it will be
274
- # moved to inside the existing directory.
275
- # We just created new_path ourselves, so it will
276
- # be removable.
277
- os .rmdir (new_path )
278
- renames (path , new_path )
367
+
279
368
for pth in self .pth .values ():
280
369
pth .remove ()
281
370
@@ -312,28 +401,22 @@ def _display(msg, paths):
312
401
313
402
def rollback (self ):
314
403
"""Rollback the changes previously made by remove()."""
315
- if not self ._save_dirs :
404
+ if not self ._moved_paths . can_rollback :
316
405
logger .error (
317
406
"Can't roll back %s; was not uninstalled" ,
318
407
self .dist .project_name ,
319
408
)
320
409
return False
321
410
logger .info ('Rolling back uninstall of %s' , self .dist .project_name )
322
- for path , tmp_path in self ._moved_paths :
323
- logger .debug ('Replacing %s' , path )
324
- if os .path .isdir (tmp_path ) and os .path .isdir (path ):
325
- rmtree (path )
326
- renames (tmp_path , path )
411
+ self ._moved_paths .rollback ()
327
412
for pth in self .pth .values ():
328
413
pth .rollback ()
329
414
for save_dir in self ._save_dirs :
330
415
save_dir .cleanup ()
331
416
332
417
def commit (self ):
333
418
"""Remove temporary save dir: rollback will no longer be possible."""
334
- for save_dir in self ._save_dirs :
335
- save_dir .cleanup ()
336
- self ._moved_paths = []
419
+ self ._moved_paths .commit ()
337
420
338
421
@classmethod
339
422
def from_dist (cls , dist ):
0 commit comments