1"""Cache lines from 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
8import sys
9import os
10
11__all__ = ["getline", "clearcache", "checkcache"]
12
13def getline(filename, lineno, module_globals=None):
14    lines = getlines(filename, module_globals)
15    if 1 <= lineno <= len(lines):
16        return lines[lineno-1]
17    else:
18        return ''
19
20
21# The cache
22
23cache = {} # The cache
24
25
26def clearcache():
27    """Clear the cache entirely."""
28
29    global cache
30    cache = {}
31
32
33def getlines(filename, module_globals=None):
34    """Get the lines for a file from the cache.
35    Update the cache if it doesn't contain an entry for this file already."""
36
37    if filename in cache:
38        return cache[filename][2]
39    else:
40        return updatecache(filename, module_globals)
41
42
43def checkcache(filename=None):
44    """Discard cache entries that are out of date.
45    (This is not checked upon each call!)"""
46
47    if filename is None:
48        filenames = cache.keys()
49    else:
50        if filename in cache:
51            filenames = [filename]
52        else:
53            return
54
55    for filename in filenames:
56        size, mtime, lines, fullname = cache[filename]
57        if mtime is None:
58            continue   # no-op for files loaded via a __loader__
59        try:
60            stat = os.stat(fullname)
61        except os.error:
62            del cache[filename]
63            continue
64        if size != stat.st_size or mtime != stat.st_mtime:
65            del cache[filename]
66
67
68def updatecache(filename, module_globals=None):
69    """Update a cache entry and return its list of lines.
70    If something's wrong, print a message, discard the cache entry,
71    and return an empty list."""
72
73    if filename in cache:
74        del cache[filename]
75    if not filename or (filename.startswith('<') and filename.endswith('>')):
76        return []
77
78    fullname = filename
79    try:
80        stat = os.stat(fullname)
81    except OSError:
82        basename = filename
83
84        # Try for a __loader__, if available
85        if module_globals and '__loader__' in module_globals:
86            name = module_globals.get('__name__')
87            loader = module_globals['__loader__']
88            get_source = getattr(loader, 'get_source', None)
89
90            if name and get_source:
91                try:
92                    data = get_source(name)
93                except (ImportError, IOError):
94                    pass
95                else:
96                    if data is None:
97                        # No luck, the PEP302 loader cannot find the source
98                        # for this module.
99                        return []
100                    cache[filename] = (
101                        len(data), None,
102                        [line+'\n' for line in data.splitlines()], fullname
103                    )
104                    return cache[filename][2]
105
106        # Try looking through the module search path, which is only useful
107        # when handling a relative filename.
108        if os.path.isabs(filename):
109            return []
110
111        for dirname in sys.path:
112            # When using imputil, sys.path may contain things other than
113            # strings; ignore them when it happens.
114            try:
115                fullname = os.path.join(dirname, basename)
116            except (TypeError, AttributeError):
117                # Not sufficiently string-like to do anything useful with.
118                continue
119            try:
120                stat = os.stat(fullname)
121                break
122            except os.error:
123                pass
124        else:
125            return []
126    try:
127        with open(fullname, 'rU') as fp:
128            lines = fp.readlines()
129    except IOError:
130        return []
131    if lines and not lines[-1].endswith('\n'):
132        lines[-1] += '\n'
133    size, mtime = stat.st_size, stat.st_mtime
134    cache[filename] = size, mtime, lines, fullname
135    return lines
136