# Released under the MIT License. See LICENSE for details.#"""Provides a system for caching linting/formatting operations."""from__future__importannotationsimportjsonimportosfromtypingimportTYPE_CHECKING# Pylint's preferred import order here seems non-deterministic (as of 2.10.1).# pylint: disable=useless-suppression# pylint: disable=wrong-import-orderfromefro.terminalimportClrfromefrotools.utilimportget_files_hash# pylint: enable=wrong-import-order# pylint: enable=useless-suppressionifTYPE_CHECKING:fromtypingimportSequence,AnyfrompathlibimportPath
[docs]classFileCache:"""A cache of file hashes/etc. used in linting/formatting/etc."""def__init__(self,path:Path):self._path=pathself.curhashes:dict[str,str|None]={}self.mtimes:dict[str,float]={}self.entries:dict[str,Any]ifnotos.path.exists(path):self.entries={}else:withopen(path,'r',encoding='utf-8')asinfile:self.entries=json.loads(infile.read())
[docs]defupdate(self,filenames:Sequence[str],extrahash:str)->None:"""Update the cache for the provided files and hash type. Hashes will be checked for all files (incorporating extrahash) and mismatched hash values cleared. Entries for no-longer-existing files will be cleared as well. """# First, completely prune entries for nonexistent files.self.entries={path:valforpath,valinself.entries.items()ifos.path.isfile(path)}# Also remove any not in our passed list.self.entries={path:valforpath,valinself.entries.items()ifpathinfilenames}# Add empty entries for files that lack them.# Also check and store current hashes for all files and clear# any entry hashes that differ so we know they're dirty.forfilenameinfilenames:iffilenamenotinself.entries:self.entries[filename]={}self.curhashes[filename]=curhash=get_files_hash([filename],extrahash)# Also store modtimes; we'll abort cache writes if# anything changed.self.mtimes[filename]=os.path.getmtime(filename)entry=self.entries[filename]if'hash'inentryandentry['hash']!=curhash:delentry['hash']
[docs]defget_dirty_files(self)->Sequence[str]:"""Return paths for all entries with no hash value."""return[keyforkey,valueinself.entries.items()if'hash'notinvalue]
[docs]defmark_clean(self,files:Sequence[str])->None:"""Marks provided files as up to date."""forfnameinfiles:self.entries[fname]['hash']=self.curhashes[fname]# Also update their registered mtimes.self.mtimes[fname]=os.path.getmtime(fname)
[docs]defwrite(self)->None:"""Writes the state back to its file."""# Check all file mtimes against the ones we started with;# if anything has been modified, don't write.forfname,mtimeinself.mtimes.items():ifos.path.getmtime(fname)!=mtime:print(f'{Clr.MAG}File changed during run:'f' "{fname}"; cache not updated.{Clr.RST}')returnout=json.dumps(self.entries)self._path.parent.mkdir(parents=True,exist_ok=True)withself._path.open('w')asoutfile:outfile.write(out)