@@ -458,6 +458,87 @@ def _download_one(cmd, source, install, download_dir, *, must_copy=False):
458458 return package
459459
460460
461+ def _preserve_site (cmd , root ):
462+ if not root .is_dir ():
463+ return None
464+ if not cmd .preserve_site_on_upgrade :
465+ LOGGER .verbose ("Not preserving site directory because of config" )
466+ return None
467+ if cmd .force :
468+ LOGGER .verbose ("Not preserving site directory because of --force" )
469+ return None
470+ if cmd .repair :
471+ LOGGER .verbose ("Not preserving site directory because of --repair" )
472+ return None
473+ state = []
474+ i = 0
475+ dirs = [root ]
476+ root = root .with_name (f"_{ root .name } " )
477+ root .mkdir (parents = True , exist_ok = True )
478+ while dirs :
479+ if dirs [0 ].match ("site-packages" ):
480+ while True :
481+ target = root / str (i )
482+ i += 1
483+ try :
484+ unlink (target )
485+ break
486+ except FileNotFoundError :
487+ break
488+ except OSError :
489+ LOGGER .verbose ("Failed to remove %s." , target )
490+ LOGGER .info ("Preserving %s during update as %s." , dirs [0 ], target )
491+ try :
492+ dirs [0 ].rename (target )
493+ except OSError :
494+ LOGGER .warn ("Failed to preserve %s during update." , dirs [0 ])
495+ LOGGER .verbose ("TRACEBACK" , exc_info = True )
496+ else :
497+ state .append ((dirs [0 ], target ))
498+ else :
499+ dirs .extend (d for d in dirs [0 ].iterdir () if d .is_dir ())
500+ dirs .pop (0 )
501+ # Append None, root last so that root gets cleaned up after restore is done
502+ state .append ((None , root ))
503+ return state
504+
505+
506+ def _restore_site (cmd , state ):
507+ if not state :
508+ return
509+ for dest , src in state :
510+ if not dest :
511+ LOGGER .verbose ("Removing preserved directory at %s" , src )
512+ try :
513+ rmtree (
514+ src ,
515+ "Removing temporary files is taking some time. " +
516+ "You can continue to wait or press Ctrl+C to abort. " +
517+ "Python has been installed, but some harmless temporary " +
518+ "files may remain on disk."
519+ )
520+ except KeyboardInterrupt :
521+ break
522+ continue
523+ LOGGER .info ("Restoring %s from %s after update." , dest , src )
524+ try :
525+ for i in src .iterdir ():
526+ if not i .is_dir () and not i .is_file ():
527+ LOGGER .verbose ("Not restoring %s because it is not a " +
528+ "normal file or directory." , i )
529+ continue
530+ d = dest / i .name
531+ if d .exists ():
532+ LOGGER .verbose ("Not restoring %s because %s exists" , i , d )
533+ continue
534+ LOGGER .verbose ("Restoring %s to %s" , i , d )
535+ d .parent .mkdir (parents = True , exist_ok = True )
536+ i .rename (d )
537+ except OSError :
538+ LOGGER .warn ("Failed to restore %s during update." , dest )
539+ LOGGER .verbose ("TRACEBACK" , exc_info = True )
540+
541+
461542def _install_one (cmd , source , install , * , target = None ):
462543 if cmd .repair :
463544 LOGGER .info ("Repairing %s." , install ['display-name' ])
@@ -475,6 +556,8 @@ def _install_one(cmd, source, install, *, target=None):
475556
476557 dest = target or (cmd .install_dir / install ["id" ])
477558
559+ preserved_site = _preserve_site (cmd , dest )
560+
478561 LOGGER .verbose ("Extracting %s to %s" , package , dest )
479562 if not cmd .repair :
480563 try :
@@ -544,6 +627,8 @@ def _install_one(cmd, source, install, *, target=None):
544627 with open (dest / "__install__.json" , "w" , encoding = "utf-8" ) as f :
545628 json .dump (install , f , default = str )
546629
630+ _restore_site (cmd , preserved_site )
631+
547632 LOGGER .verbose ("Install complete" )
548633
549634
@@ -560,7 +645,6 @@ def _merge_existing_index(versions, index_json):
560645 else :
561646 LOGGER .debug ("Merging into existing %s" , index_json )
562647 current = {i ["url" ].casefold () for i in versions }
563- added = []
564648 for install in existing_index ["versions" ]:
565649 if install .get ("url" , "" ).casefold () not in current :
566650 LOGGER .debug ("Merging %s" , install .get ("url" , "<unspecified>" ))
0 commit comments