@@ -1567,50 +1567,79 @@ def get_exe_prefixes(exe_filename):
15671567class PthDistributions (Environment ):
15681568 """A .pth file with Distribution paths in it"""
15691569
1570- dirty = False
1571-
15721570 def __init__ (self , filename , sitedirs = ()):
15731571 self .filename = filename
15741572 self .sitedirs = list (map (normalize_path , sitedirs ))
15751573 self .basedir = normalize_path (os .path .dirname (self .filename ))
1576- self ._load ()
1574+ self .paths , self .dirty = self ._load ()
1575+ # keep a copy if someone manually updates the paths attribute on the instance
1576+ self ._init_paths = self .paths [:]
15771577 super ().__init__ ([], None , None )
15781578 for path in yield_lines (self .paths ):
15791579 list (map (self .add , find_distributions (path , True )))
15801580
1581- def _load (self ):
1582- self . paths = []
1583- saw_import = False
1581+ def _load_raw (self ):
1582+ paths = []
1583+ dirty = saw_import = False
15841584 seen = dict .fromkeys (self .sitedirs )
1585- if os .path .isfile (self .filename ):
1586- f = open (self .filename , 'rt' )
1587- for line in f :
1588- if line .startswith ('import' ):
1589- saw_import = True
1590- continue
1591- path = line .rstrip ()
1592- self .paths .append (path )
1593- if not path .strip () or path .strip ().startswith ('#' ):
1594- continue
1595- # skip non-existent paths, in case somebody deleted a package
1596- # manually, and duplicate paths as well
1597- path = self .paths [- 1 ] = normalize_path (
1598- os .path .join (self .basedir , path )
1599- )
1600- if not os .path .exists (path ) or path in seen :
1601- self .paths .pop () # skip it
1602- self .dirty = True # we cleaned up, so we're dirty now :)
1603- continue
1604- seen [path ] = 1
1605- f .close ()
1585+ f = open (self .filename , 'rt' )
1586+ for line in f :
1587+ path = line .rstrip ()
1588+ # still keep imports and empty/commented lines for formatting
1589+ paths .append (path )
1590+ if line .startswith (('import ' , 'from ' )):
1591+ saw_import = True
1592+ continue
1593+ stripped_path = path .strip ()
1594+ if not stripped_path or stripped_path .startswith ('#' ):
1595+ continue
1596+ # skip non-existent paths, in case somebody deleted a package
1597+ # manually, and duplicate paths as well
1598+ normalized_path = normalize_path (os .path .join (self .basedir , path ))
1599+ if normalized_path in seen or not os .path .exists (normalized_path ):
1600+ log .debug ("cleaned up dirty or duplicated %r" , path )
1601+ dirty = True
1602+ paths .pop ()
1603+ continue
1604+ seen [normalized_path ] = 1
1605+ f .close ()
1606+ # remove any trailing empty/blank line
1607+ while paths and not paths [- 1 ].strip ():
1608+ paths .pop ()
1609+ dirty = True
1610+ return paths , dirty or (paths and saw_import )
16061611
1607- if self . paths and not saw_import :
1608- self .dirty = True # ensure anything we touch has import wrappers
1609- while self . paths and not self .paths [ - 1 ]. strip ():
1610- self . paths . pop ()
1612+ def _load ( self ) :
1613+ if os . path . isfile ( self .filename ):
1614+ return self ._load_raw ()
1615+ return [], False
16111616
16121617 def save (self ):
16131618 """Write changed .pth file back to disk"""
1619+ # first reload the file
1620+ last_paths , last_dirty = self ._load ()
1621+ # and check that there are no difference with what we have.
1622+ # there can be difference if someone else has written to the file
1623+ # since we first loaded it.
1624+ # we don't want to lose the eventual new paths added since then.
1625+ for path in last_paths [:]:
1626+ if path not in self .paths :
1627+ self .paths .append (path )
1628+ log .info ("detected new path %r" , path )
1629+ last_dirty = True
1630+ else :
1631+ last_paths .remove (path )
1632+ # also, re-check that all paths are still valid before saving them
1633+ for path in self .paths [:]:
1634+ if path not in last_paths \
1635+ and not path .startswith (('import ' , 'from ' , '#' )):
1636+ absolute_path = os .path .join (self .basedir , path )
1637+ if not os .path .exists (absolute_path ):
1638+ self .paths .remove (path )
1639+ log .info ("removing now non-existent path %r" , path )
1640+ last_dirty = True
1641+
1642+ self .dirty |= last_dirty or self .paths != self ._init_paths
16141643 if not self .dirty :
16151644 return
16161645
@@ -1619,17 +1648,16 @@ def save(self):
16191648 log .debug ("Saving %s" , self .filename )
16201649 lines = self ._wrap_lines (rel_paths )
16211650 data = '\n ' .join (lines ) + '\n '
1622-
16231651 if os .path .islink (self .filename ):
16241652 os .unlink (self .filename )
16251653 with open (self .filename , 'wt' ) as f :
16261654 f .write (data )
1627-
16281655 elif os .path .exists (self .filename ):
16291656 log .debug ("Deleting empty %s" , self .filename )
16301657 os .unlink (self .filename )
16311658
16321659 self .dirty = False
1660+ self ._init_paths [:] = self .paths [:]
16331661
16341662 @staticmethod
16351663 def _wrap_lines (lines ):
0 commit comments