@@ -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