@@ -458,6 +458,86 @@ 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+ d = dest / i .name
530+ if d .exists ():
531+ LOGGER .verbose ("Not restoring %s because %s exists" , i , d )
532+ continue
533+ LOGGER .verbose ("Restoring %s to %s" , i , d )
534+ d .parent .mkdir (parents = True , exist_ok = True )
535+ i .rename (d )
536+ except OSError :
537+ LOGGER .warn ("Failed to restore %s during update." , dest )
538+ LOGGER .verbose ("TRACEBACK" , exc_info = True )
539+
540+
461541def _install_one (cmd , source , install , * , target = None ):
462542 if cmd .repair :
463543 LOGGER .info ("Repairing %s." , install ['display-name' ])
@@ -475,6 +555,8 @@ def _install_one(cmd, source, install, *, target=None):
475555
476556 dest = target or (cmd .install_dir / install ["id" ])
477557
558+ preserved_site = _preserve_site (cmd , dest )
559+
478560 LOGGER .verbose ("Extracting %s to %s" , package , dest )
479561 if not cmd .repair :
480562 try :
@@ -544,6 +626,8 @@ def _install_one(cmd, source, install, *, target=None):
544626 with open (dest / "__install__.json" , "w" , encoding = "utf-8" ) as f :
545627 json .dump (install , f , default = str )
546628
629+ _restore_site (cmd , preserved_site )
630+
547631 LOGGER .verbose ("Install complete" )
548632
549633
0 commit comments