• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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