linecache.py 5.19 KB
Newer Older
1
"""Cache lines from Python source files.
2 3 4 5 6

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.
"""
Guido van Rossum's avatar
Guido van Rossum committed
7

8
import functools
9
import sys
Guido van Rossum's avatar
Guido van Rossum committed
10
import os
11
import tokenize
Guido van Rossum's avatar
Guido van Rossum committed
12

13
__all__ = ["getline", "clearcache", "checkcache"]
14

15 16
def getline(filename, lineno, module_globals=None):
    lines = getlines(filename, module_globals)
17 18 19 20
    if 1 <= lineno <= len(lines):
        return lines[lineno-1]
    else:
        return ''
Guido van Rossum's avatar
Guido van Rossum committed
21 22 23 24


# The cache

25 26 27
# The cache. Maps filenames to either a thunk which will provide source code,
# or a tuple (size, mtime, lines, fullname) once loaded.
cache = {}
Guido van Rossum's avatar
Guido van Rossum committed
28 29 30


def clearcache():
31
    """Clear the cache entirely."""
Guido van Rossum's avatar
Guido van Rossum committed
32

33 34
    global cache
    cache = {}
Guido van Rossum's avatar
Guido van Rossum committed
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."""
Guido van Rossum's avatar
Guido van Rossum committed
40

41
    if filename in cache:
42
        entry = cache[filename]
43 44
        if len(entry) != 1:
            return cache[filename][2]
45 46

    try:
47
        return updatecache(filename, module_globals)
48 49 50
    except MemoryError:
        clearcache()
        return []
Guido van Rossum's avatar
Guido van Rossum committed
51 52


53
def checkcache(filename=None):
54 55
    """Discard cache entries that are out of date.
    (This is not checked upon each call!)"""
Guido van Rossum's avatar
Guido van Rossum committed
56

57
    if filename is None:
58
        filenames = list(cache.keys())
59 60 61 62 63 64 65
    else:
        if filename in cache:
            filenames = [filename]
        else:
            return

    for filename in filenames:
66 67 68 69 70
        entry = cache[filename]
        if len(entry) == 1:
            # lazy cache entry, leave it lazy.
            continue
        size, mtime, lines, fullname = entry
71 72
        if mtime is None:
            continue   # no-op for files loaded via a __loader__
73 74
        try:
            stat = os.stat(fullname)
75
        except OSError:
76 77
            del cache[filename]
            continue
78
        if size != stat.st_size or mtime != stat.st_mtime:
79
            del cache[filename]
Guido van Rossum's avatar
Guido van Rossum committed
80 81


82
def updatecache(filename, module_globals=None):
83 84 85 86
    """Update a cache entry and return its list of lines.
    If something's wrong, print a message, discard the cache entry,
    and return an empty list."""

87
    if filename in cache:
88 89
        if len(cache[filename]) != 1:
            del cache[filename]
90
    if not filename or (filename.startswith('<') and filename.endswith('>')):
91
        return []
92

93 94 95
    fullname = filename
    try:
        stat = os.stat(fullname)
96
    except OSError:
97
        basename = filename
98

99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
        # Realise a lazy loader based lookup if there is one
        # otherwise try to lookup right now.
        if lazycache(filename, module_globals):
            try:
                data = cache[filename][0]()
            except (ImportError, OSError):
                pass
            else:
                if data is None:
                    # No luck, the PEP302 loader cannot find the source
                    # for this module.
                    return []
                cache[filename] = (
                    len(data), None,
                    [line+'\n' for line in data.splitlines()], fullname
                )
                return cache[filename][2]
116

117 118 119 120
        # Try looking through the module search path, which is only useful
        # when handling a relative filename.
        if os.path.isabs(filename):
            return []
121

122 123
        for dirname in sys.path:
            try:
Tim Peters's avatar
Tim Peters committed
124 125 126
                fullname = os.path.join(dirname, basename)
            except (TypeError, AttributeError):
                # Not sufficiently string-like to do anything useful with.
127 128 129 130
                continue
            try:
                stat = os.stat(fullname)
                break
131
            except OSError:
132 133 134
                pass
        else:
            return []
135
    try:
136
        with tokenize.open(fullname) as fp:
137
            lines = fp.readlines()
138
    except OSError:
139
        return []
140 141
    if lines and not lines[-1].endswith('\n'):
        lines[-1] += '\n'
142
    size, mtime = stat.st_size, stat.st_mtime
143 144
    cache[filename] = size, mtime, lines, fullname
    return lines
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177


def lazycache(filename, module_globals):
    """Seed the cache for filename with module_globals.

    The module loader will be asked for the source only when getlines is
    called, not immediately.

    If there is an entry in the cache already, it is not altered.

    :return: True if a lazy load is registered in the cache,
        otherwise False. To register such a load a module loader with a
        get_source method must be found, the filename must be a cachable
        filename, and the filename must not be already cached.
    """
    if filename in cache:
        if len(cache[filename]) == 1:
            return True
        else:
            return False
    if not filename or (filename.startswith('<') and filename.endswith('>')):
        return False
    # Try for a __loader__, if available
    if module_globals and '__loader__' in module_globals:
        name = module_globals.get('__name__')
        loader = module_globals['__loader__']
        get_source = getattr(loader, 'get_source', None)

        if name and get_source:
            get_lines = functools.partial(get_source, name)
            cache[filename] = (get_lines,)
            return True
    return False