1*cda5da8dSAndroid Build Coastguard Worker"""Cache lines from Python source files. 2*cda5da8dSAndroid Build Coastguard Worker 3*cda5da8dSAndroid Build Coastguard WorkerThis is intended to read lines from modules imported -- hence if a filename 4*cda5da8dSAndroid Build Coastguard Workeris not found, it will look down the module search path for a file by 5*cda5da8dSAndroid Build Coastguard Workerthat name. 6*cda5da8dSAndroid Build Coastguard Worker""" 7*cda5da8dSAndroid Build Coastguard Worker 8*cda5da8dSAndroid Build Coastguard Workerimport functools 9*cda5da8dSAndroid Build Coastguard Workerimport sys 10*cda5da8dSAndroid Build Coastguard Workerimport os 11*cda5da8dSAndroid Build Coastguard Workerimport tokenize 12*cda5da8dSAndroid Build Coastguard Worker 13*cda5da8dSAndroid Build Coastguard Worker__all__ = ["getline", "clearcache", "checkcache", "lazycache"] 14*cda5da8dSAndroid Build Coastguard Worker 15*cda5da8dSAndroid Build Coastguard Worker 16*cda5da8dSAndroid Build Coastguard Worker# The cache. Maps filenames to either a thunk which will provide source code, 17*cda5da8dSAndroid Build Coastguard Worker# or a tuple (size, mtime, lines, fullname) once loaded. 18*cda5da8dSAndroid Build Coastguard Workercache = {} 19*cda5da8dSAndroid Build Coastguard Worker 20*cda5da8dSAndroid Build Coastguard Worker 21*cda5da8dSAndroid Build Coastguard Workerdef clearcache(): 22*cda5da8dSAndroid Build Coastguard Worker """Clear the cache entirely.""" 23*cda5da8dSAndroid Build Coastguard Worker cache.clear() 24*cda5da8dSAndroid Build Coastguard Worker 25*cda5da8dSAndroid Build Coastguard Worker 26*cda5da8dSAndroid Build Coastguard Workerdef getline(filename, lineno, module_globals=None): 27*cda5da8dSAndroid Build Coastguard Worker """Get a line for a Python source file from the cache. 28*cda5da8dSAndroid Build Coastguard Worker Update the cache if it doesn't contain an entry for this file already.""" 29*cda5da8dSAndroid Build Coastguard Worker 30*cda5da8dSAndroid Build Coastguard Worker lines = getlines(filename, module_globals) 31*cda5da8dSAndroid Build Coastguard Worker if 1 <= lineno <= len(lines): 32*cda5da8dSAndroid Build Coastguard Worker return lines[lineno - 1] 33*cda5da8dSAndroid Build Coastguard Worker return '' 34*cda5da8dSAndroid Build Coastguard Worker 35*cda5da8dSAndroid Build Coastguard Worker 36*cda5da8dSAndroid Build Coastguard Workerdef getlines(filename, module_globals=None): 37*cda5da8dSAndroid Build Coastguard Worker """Get the lines for a Python source file from the cache. 38*cda5da8dSAndroid Build Coastguard Worker Update the cache if it doesn't contain an entry for this file already.""" 39*cda5da8dSAndroid Build Coastguard Worker 40*cda5da8dSAndroid Build Coastguard Worker if filename in cache: 41*cda5da8dSAndroid Build Coastguard Worker entry = cache[filename] 42*cda5da8dSAndroid Build Coastguard Worker if len(entry) != 1: 43*cda5da8dSAndroid Build Coastguard Worker return cache[filename][2] 44*cda5da8dSAndroid Build Coastguard Worker 45*cda5da8dSAndroid Build Coastguard Worker try: 46*cda5da8dSAndroid Build Coastguard Worker return updatecache(filename, module_globals) 47*cda5da8dSAndroid Build Coastguard Worker except MemoryError: 48*cda5da8dSAndroid Build Coastguard Worker clearcache() 49*cda5da8dSAndroid Build Coastguard Worker return [] 50*cda5da8dSAndroid Build Coastguard Worker 51*cda5da8dSAndroid Build Coastguard Worker 52*cda5da8dSAndroid Build Coastguard Workerdef checkcache(filename=None): 53*cda5da8dSAndroid Build Coastguard Worker """Discard cache entries that are out of date. 54*cda5da8dSAndroid Build Coastguard Worker (This is not checked upon each call!)""" 55*cda5da8dSAndroid Build Coastguard Worker 56*cda5da8dSAndroid Build Coastguard Worker if filename is None: 57*cda5da8dSAndroid Build Coastguard Worker filenames = list(cache.keys()) 58*cda5da8dSAndroid Build Coastguard Worker elif filename in cache: 59*cda5da8dSAndroid Build Coastguard Worker filenames = [filename] 60*cda5da8dSAndroid Build Coastguard Worker else: 61*cda5da8dSAndroid Build Coastguard Worker return 62*cda5da8dSAndroid Build Coastguard Worker 63*cda5da8dSAndroid Build Coastguard Worker for filename in filenames: 64*cda5da8dSAndroid Build Coastguard Worker entry = cache[filename] 65*cda5da8dSAndroid Build Coastguard Worker if len(entry) == 1: 66*cda5da8dSAndroid Build Coastguard Worker # lazy cache entry, leave it lazy. 67*cda5da8dSAndroid Build Coastguard Worker continue 68*cda5da8dSAndroid Build Coastguard Worker size, mtime, lines, fullname = entry 69*cda5da8dSAndroid Build Coastguard Worker if mtime is None: 70*cda5da8dSAndroid Build Coastguard Worker continue # no-op for files loaded via a __loader__ 71*cda5da8dSAndroid Build Coastguard Worker try: 72*cda5da8dSAndroid Build Coastguard Worker stat = os.stat(fullname) 73*cda5da8dSAndroid Build Coastguard Worker except OSError: 74*cda5da8dSAndroid Build Coastguard Worker cache.pop(filename, None) 75*cda5da8dSAndroid Build Coastguard Worker continue 76*cda5da8dSAndroid Build Coastguard Worker if size != stat.st_size or mtime != stat.st_mtime: 77*cda5da8dSAndroid Build Coastguard Worker cache.pop(filename, None) 78*cda5da8dSAndroid Build Coastguard Worker 79*cda5da8dSAndroid Build Coastguard Worker 80*cda5da8dSAndroid Build Coastguard Workerdef updatecache(filename, module_globals=None): 81*cda5da8dSAndroid Build Coastguard Worker """Update a cache entry and return its list of lines. 82*cda5da8dSAndroid Build Coastguard Worker If something's wrong, print a message, discard the cache entry, 83*cda5da8dSAndroid Build Coastguard Worker and return an empty list.""" 84*cda5da8dSAndroid Build Coastguard Worker 85*cda5da8dSAndroid Build Coastguard Worker if filename in cache: 86*cda5da8dSAndroid Build Coastguard Worker if len(cache[filename]) != 1: 87*cda5da8dSAndroid Build Coastguard Worker cache.pop(filename, None) 88*cda5da8dSAndroid Build Coastguard Worker if not filename or (filename.startswith('<') and filename.endswith('>')): 89*cda5da8dSAndroid Build Coastguard Worker return [] 90*cda5da8dSAndroid Build Coastguard Worker 91*cda5da8dSAndroid Build Coastguard Worker fullname = filename 92*cda5da8dSAndroid Build Coastguard Worker try: 93*cda5da8dSAndroid Build Coastguard Worker stat = os.stat(fullname) 94*cda5da8dSAndroid Build Coastguard Worker except OSError: 95*cda5da8dSAndroid Build Coastguard Worker basename = filename 96*cda5da8dSAndroid Build Coastguard Worker 97*cda5da8dSAndroid Build Coastguard Worker # Realise a lazy loader based lookup if there is one 98*cda5da8dSAndroid Build Coastguard Worker # otherwise try to lookup right now. 99*cda5da8dSAndroid Build Coastguard Worker if lazycache(filename, module_globals): 100*cda5da8dSAndroid Build Coastguard Worker try: 101*cda5da8dSAndroid Build Coastguard Worker data = cache[filename][0]() 102*cda5da8dSAndroid Build Coastguard Worker except (ImportError, OSError): 103*cda5da8dSAndroid Build Coastguard Worker pass 104*cda5da8dSAndroid Build Coastguard Worker else: 105*cda5da8dSAndroid Build Coastguard Worker if data is None: 106*cda5da8dSAndroid Build Coastguard Worker # No luck, the PEP302 loader cannot find the source 107*cda5da8dSAndroid Build Coastguard Worker # for this module. 108*cda5da8dSAndroid Build Coastguard Worker return [] 109*cda5da8dSAndroid Build Coastguard Worker cache[filename] = ( 110*cda5da8dSAndroid Build Coastguard Worker len(data), 111*cda5da8dSAndroid Build Coastguard Worker None, 112*cda5da8dSAndroid Build Coastguard Worker [line + '\n' for line in data.splitlines()], 113*cda5da8dSAndroid Build Coastguard Worker fullname 114*cda5da8dSAndroid Build Coastguard Worker ) 115*cda5da8dSAndroid Build Coastguard Worker return cache[filename][2] 116*cda5da8dSAndroid Build Coastguard Worker 117*cda5da8dSAndroid Build Coastguard Worker # Try looking through the module search path, which is only useful 118*cda5da8dSAndroid Build Coastguard Worker # when handling a relative filename. 119*cda5da8dSAndroid Build Coastguard Worker if os.path.isabs(filename): 120*cda5da8dSAndroid Build Coastguard Worker return [] 121*cda5da8dSAndroid Build Coastguard Worker 122*cda5da8dSAndroid Build Coastguard Worker for dirname in sys.path: 123*cda5da8dSAndroid Build Coastguard Worker try: 124*cda5da8dSAndroid Build Coastguard Worker fullname = os.path.join(dirname, basename) 125*cda5da8dSAndroid Build Coastguard Worker except (TypeError, AttributeError): 126*cda5da8dSAndroid Build Coastguard Worker # Not sufficiently string-like to do anything useful with. 127*cda5da8dSAndroid Build Coastguard Worker continue 128*cda5da8dSAndroid Build Coastguard Worker try: 129*cda5da8dSAndroid Build Coastguard Worker stat = os.stat(fullname) 130*cda5da8dSAndroid Build Coastguard Worker break 131*cda5da8dSAndroid Build Coastguard Worker except OSError: 132*cda5da8dSAndroid Build Coastguard Worker pass 133*cda5da8dSAndroid Build Coastguard Worker else: 134*cda5da8dSAndroid Build Coastguard Worker return [] 135*cda5da8dSAndroid Build Coastguard Worker try: 136*cda5da8dSAndroid Build Coastguard Worker with tokenize.open(fullname) as fp: 137*cda5da8dSAndroid Build Coastguard Worker lines = fp.readlines() 138*cda5da8dSAndroid Build Coastguard Worker except (OSError, UnicodeDecodeError, SyntaxError): 139*cda5da8dSAndroid Build Coastguard Worker return [] 140*cda5da8dSAndroid Build Coastguard Worker if lines and not lines[-1].endswith('\n'): 141*cda5da8dSAndroid Build Coastguard Worker lines[-1] += '\n' 142*cda5da8dSAndroid Build Coastguard Worker size, mtime = stat.st_size, stat.st_mtime 143*cda5da8dSAndroid Build Coastguard Worker cache[filename] = size, mtime, lines, fullname 144*cda5da8dSAndroid Build Coastguard Worker return lines 145*cda5da8dSAndroid Build Coastguard Worker 146*cda5da8dSAndroid Build Coastguard Worker 147*cda5da8dSAndroid Build Coastguard Workerdef lazycache(filename, module_globals): 148*cda5da8dSAndroid Build Coastguard Worker """Seed the cache for filename with module_globals. 149*cda5da8dSAndroid Build Coastguard Worker 150*cda5da8dSAndroid Build Coastguard Worker The module loader will be asked for the source only when getlines is 151*cda5da8dSAndroid Build Coastguard Worker called, not immediately. 152*cda5da8dSAndroid Build Coastguard Worker 153*cda5da8dSAndroid Build Coastguard Worker If there is an entry in the cache already, it is not altered. 154*cda5da8dSAndroid Build Coastguard Worker 155*cda5da8dSAndroid Build Coastguard Worker :return: True if a lazy load is registered in the cache, 156*cda5da8dSAndroid Build Coastguard Worker otherwise False. To register such a load a module loader with a 157*cda5da8dSAndroid Build Coastguard Worker get_source method must be found, the filename must be a cacheable 158*cda5da8dSAndroid Build Coastguard Worker filename, and the filename must not be already cached. 159*cda5da8dSAndroid Build Coastguard Worker """ 160*cda5da8dSAndroid Build Coastguard Worker if filename in cache: 161*cda5da8dSAndroid Build Coastguard Worker if len(cache[filename]) == 1: 162*cda5da8dSAndroid Build Coastguard Worker return True 163*cda5da8dSAndroid Build Coastguard Worker else: 164*cda5da8dSAndroid Build Coastguard Worker return False 165*cda5da8dSAndroid Build Coastguard Worker if not filename or (filename.startswith('<') and filename.endswith('>')): 166*cda5da8dSAndroid Build Coastguard Worker return False 167*cda5da8dSAndroid Build Coastguard Worker # Try for a __loader__, if available 168*cda5da8dSAndroid Build Coastguard Worker if module_globals and '__name__' in module_globals: 169*cda5da8dSAndroid Build Coastguard Worker name = module_globals['__name__'] 170*cda5da8dSAndroid Build Coastguard Worker if (loader := module_globals.get('__loader__')) is None: 171*cda5da8dSAndroid Build Coastguard Worker if spec := module_globals.get('__spec__'): 172*cda5da8dSAndroid Build Coastguard Worker try: 173*cda5da8dSAndroid Build Coastguard Worker loader = spec.loader 174*cda5da8dSAndroid Build Coastguard Worker except AttributeError: 175*cda5da8dSAndroid Build Coastguard Worker pass 176*cda5da8dSAndroid Build Coastguard Worker get_source = getattr(loader, 'get_source', None) 177*cda5da8dSAndroid Build Coastguard Worker 178*cda5da8dSAndroid Build Coastguard Worker if name and get_source: 179*cda5da8dSAndroid Build Coastguard Worker get_lines = functools.partial(get_source, name) 180*cda5da8dSAndroid Build Coastguard Worker cache[filename] = (get_lines,) 181*cda5da8dSAndroid Build Coastguard Worker return True 182*cda5da8dSAndroid Build Coastguard Worker return False 183