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