17
17
FakeFile , ask , dist_in_usersite , dist_is_local , egg_link_path , is_local ,
18
18
normalize_path , renames ,
19
19
)
20
- from pip ._internal .utils .temp_dir import TempDirectory
20
+ from pip ._internal .utils .temp_dir import TempDirectory , AdjacentTempDirectory
21
21
22
22
logger = logging .getLogger (__name__ )
23
23
@@ -86,16 +86,49 @@ def compact(paths):
86
86
sep = os .path .sep
87
87
short_paths = set ()
88
88
for path in sorted (paths , key = len ):
89
- should_add = any (
89
+ should_skip = any (
90
90
path .startswith (shortpath .rstrip ("*" )) and
91
91
path [len (shortpath .rstrip ("*" ).rstrip (sep ))] == sep
92
92
for shortpath in short_paths
93
93
)
94
- if not should_add :
94
+ if not should_skip :
95
95
short_paths .add (path )
96
96
return short_paths
97
97
98
98
99
+ def compress_for_rename (paths ):
100
+ """Returns a set containing the paths that need to be renamed.
101
+
102
+ This set may include directories when the original sequence of paths
103
+ included every file on disk.
104
+ """
105
+ remaining = set (paths )
106
+ unchecked = sorted (set (os .path .split (p )[0 ] for p in remaining ), key = len )
107
+ wildcards = set ()
108
+
109
+ def norm_join (* a ):
110
+ return os .path .normcase (os .path .join (* a ))
111
+
112
+ for root in unchecked :
113
+ if any (root .startswith (w ) for w in wildcards ):
114
+ # This directory has already been handled.
115
+ continue
116
+
117
+ all_files = set ()
118
+ all_subdirs = set ()
119
+ for dirname , subdirs , files in os .walk (root ):
120
+ all_subdirs .update (norm_join (root , dirname , d ) for d in subdirs )
121
+ all_files .update (norm_join (root , dirname , f ) for f in files )
122
+ # If all the files we found are in our remaining set of files to
123
+ # remove, then remove them from the latter set and add a wildcard
124
+ # for the directory.
125
+ if len (all_files - remaining ) == 0 :
126
+ remaining .difference_update (all_files )
127
+ wildcards .add (root + os .sep )
128
+
129
+ return remaining | wildcards
130
+
131
+
99
132
def compress_for_output_listing (paths ):
100
133
"""Returns a tuple of 2 sets of which paths to display to user
101
134
@@ -153,7 +186,7 @@ def __init__(self, dist):
153
186
self ._refuse = set ()
154
187
self .pth = {}
155
188
self .dist = dist
156
- self .save_dir = TempDirectory ( kind = "uninstall" )
189
+ self ._save_dirs = []
157
190
self ._moved_paths = []
158
191
159
192
def _permitted (self , path ):
@@ -193,9 +226,17 @@ def add_pth(self, pth_file, entry):
193
226
self ._refuse .add (pth_file )
194
227
195
228
def _stash (self , path ):
196
- return os .path .join (
197
- self .save_dir .path , os .path .splitdrive (path )[1 ].lstrip (os .path .sep )
198
- )
229
+ best = None
230
+ for save_dir in self ._save_dirs :
231
+ if not path .startswith (save_dir .original + os .sep ):
232
+ continue
233
+ if not best or len (save_dir .original ) > len (best .original ):
234
+ best = save_dir
235
+ if best is None :
236
+ best = AdjacentTempDirectory (os .path .dirname (path ))
237
+ best .create ()
238
+ self ._save_dirs .append (best )
239
+ return os .path .join (best .path , os .path .relpath (path , best .original ))
199
240
200
241
def remove (self , auto_confirm = False , verbose = False ):
201
242
"""Remove paths in ``self.paths`` with confirmation (unless
@@ -215,12 +256,10 @@ def remove(self, auto_confirm=False, verbose=False):
215
256
216
257
with indent_log ():
217
258
if auto_confirm or self ._allowed_to_proceed (verbose ):
218
- self .save_dir .create ()
219
-
220
- for path in sorted (compact (self .paths )):
259
+ for path in sorted (compact (compress_for_rename (self .paths ))):
221
260
new_path = self ._stash (path )
222
261
logger .debug ('Removing file or directory %s' , path )
223
- self ._moved_paths .append (path )
262
+ self ._moved_paths .append (( path , new_path ) )
224
263
renames (path , new_path )
225
264
for pth in self .pth .values ():
226
265
pth .remove ()
@@ -251,28 +290,30 @@ def _display(msg, paths):
251
290
_display ('Would remove:' , will_remove )
252
291
_display ('Would not remove (might be manually added):' , will_skip )
253
292
_display ('Would not remove (outside of prefix):' , self ._refuse )
293
+ if verbose :
294
+ _display ('Will actually move:' , compress_for_rename (self .paths ))
254
295
255
296
return ask ('Proceed (y/n)? ' , ('y' , 'n' )) == 'y'
256
297
257
298
def rollback (self ):
258
299
"""Rollback the changes previously made by remove()."""
259
- if self .save_dir . path is None :
300
+ if not self ._save_dirs :
260
301
logger .error (
261
302
"Can't roll back %s; was not uninstalled" ,
262
303
self .dist .project_name ,
263
304
)
264
305
return False
265
306
logger .info ('Rolling back uninstall of %s' , self .dist .project_name )
266
- for path in self ._moved_paths :
267
- tmp_path = self ._stash (path )
307
+ for path , tmp_path in self ._moved_paths :
268
308
logger .debug ('Replacing %s' , path )
269
309
renames (tmp_path , path )
270
310
for pth in self .pth .values ():
271
311
pth .rollback ()
272
312
273
313
def commit (self ):
274
314
"""Remove temporary save dir: rollback will no longer be possible."""
275
- self .save_dir .cleanup ()
315
+ for save_dir in self ._save_dirs :
316
+ save_dir .cleanup ()
276
317
self ._moved_paths = []
277
318
278
319
@classmethod
0 commit comments