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