"""Cache lines from Python source files. This is intended to read lines from modules imported -- hence if a filename is not found, it will look down the module search path for a file by that name. """ __all__ = ["getline", "clearcache", "checkcache", "lazycache"] # The cache. Maps filenames to either a thunk which will provide source code, # or a tuple (size, mtime, lines, fullname) once loaded. cache = {} _interactive_cache = {} def clearcache(): """Clear the cache entirely.""" cache.clear() def getline(filename, lineno, module_globals=None): """Get a line for a Python source file from the cache. Update the cache if it doesn't contain an entry for this file already.""" lines = getlines(filename, module_globals) if 1 <= lineno <= len(lines): return lines[lineno - 1] return '' def getlines(filename, module_globals=None): """Get the lines for a Python source file from the cache. Update the cache if it doesn't contain an entry for this file already.""" entry = cache.get(filename, None) if entry is not None and len(entry) != 1: return entry[2] try: return updatecache(filename, module_globals) except MemoryError: clearcache() return [] def _getline_from_code(filename, lineno): lines = _getlines_from_code(filename) if 1 <= lineno <= len(lines): return lines[lineno - 1] return '' def _make_key(code): return (code.co_filename, code.co_qualname, code.co_firstlineno) def _getlines_from_code(code): code_id = _make_key(code) entry = _interactive_cache.get(code_id, None) if entry is not None and len(entry) != 1: return entry[2] return [] def _source_unavailable(filename): """Return True if the source code is unavailable for such file name.""" return ( not filename or (filename.startswith('<') and filename.endswith('>') and not filename.startswith('')): return None if module_globals is not None and not isinstance(module_globals, dict): raise TypeError(f'module_globals must be a dict, not {type(module_globals).__qualname__}') if not module_globals or '__name__' not in module_globals: return None spec = module_globals.get('__spec__') name = getattr(spec, 'name', None) or module_globals['__name__'] if name is None: return None loader = _bless_my_loader(module_globals) if loader is None: return None get_source = getattr(loader, 'get_source', None) if get_source is None: return None def get_lines(name=name, *args, **kwargs): return get_source(name, *args, **kwargs) return (get_lines,) def _bless_my_loader(module_globals): # Similar to _bless_my_loader() in importlib._bootstrap_external, # but always emits warnings instead of errors. loader = module_globals.get('__loader__') if loader is None and '__spec__' not in module_globals: return None spec = module_globals.get('__spec__') # The __main__ module has __spec__ = None. if spec is None and module_globals.get('__name__') == '__main__': return loader spec_loader = getattr(spec, 'loader', None) if spec_loader is None: import warnings warnings.warn( 'Module globals is missing a __spec__.loader', DeprecationWarning) return loader assert spec_loader is not None if loader is not None and loader != spec_loader: import warnings warnings.warn( 'Module globals; __loader__ != __spec__.loader', DeprecationWarning) return loader return spec_loader def _register_code(code, string, name): entry = (len(string), None, [line + '\n' for line in string.splitlines()], name) stack = [code] while stack: code = stack.pop() for const in code.co_consts: if isinstance(const, type(code)): stack.append(const) key = _make_key(code) _interactive_cache[key] = entry