Skip to content

Commit 32efec3

Browse files
committed
Backport linecache for Python 2.6 and up.
1 parent f634074 commit 32efec3

File tree

7 files changed

+1238
-0
lines changed

7 files changed

+1238
-0
lines changed

linecache2/__init__.py

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""Cache lines from 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 io
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+
cache = {} # The cache
26+
27+
28+
def clearcache():
29+
"""Clear the cache entirely."""
30+
31+
global cache
32+
cache = {}
33+
34+
35+
def getlines(filename, module_globals=None):
36+
"""Get the lines for a file from the cache.
37+
Update the cache if it doesn't contain an entry for this file already."""
38+
39+
if filename in cache:
40+
return cache[filename][2]
41+
else:
42+
return updatecache(filename, module_globals)
43+
44+
45+
def checkcache(filename=None):
46+
"""Discard cache entries that are out of date.
47+
(This is not checked upon each call!)"""
48+
49+
if filename is None:
50+
filenames = list(cache.keys())
51+
else:
52+
if filename in cache:
53+
filenames = [filename]
54+
else:
55+
return
56+
57+
for filename in filenames:
58+
size, mtime, lines, fullname = cache[filename]
59+
if mtime is None:
60+
continue # no-op for files loaded via a __loader__
61+
try:
62+
stat = os.stat(fullname)
63+
except OSError:
64+
del cache[filename]
65+
continue
66+
if size != stat.st_size or mtime != stat.st_mtime:
67+
del cache[filename]
68+
69+
70+
def updatecache(filename, module_globals=None):
71+
"""Update a cache entry and return its list of lines.
72+
If something's wrong, print a message, discard the cache entry,
73+
and return an empty list."""
74+
75+
if filename in cache:
76+
del cache[filename]
77+
if not filename or (filename.startswith('<') and filename.endswith('>')):
78+
return []
79+
80+
fullname = filename
81+
try:
82+
stat = os.stat(fullname)
83+
except OSError:
84+
basename = filename
85+
86+
# Try for a __loader__, if available
87+
if module_globals and '__loader__' in module_globals:
88+
name = module_globals.get('__name__')
89+
loader = module_globals['__loader__']
90+
get_source = getattr(loader, 'get_source', None)
91+
92+
if name and get_source:
93+
try:
94+
data = get_source(name)
95+
except (ImportError, OSError):
96+
pass
97+
else:
98+
if data is None:
99+
# No luck, the PEP302 loader cannot find the source
100+
# for this module.
101+
return []
102+
cache[filename] = (
103+
len(data), None,
104+
[line+'\n' for line in data.splitlines()], fullname
105+
)
106+
return cache[filename][2]
107+
108+
# Try looking through the module search path, which is only useful
109+
# when handling a relative filename.
110+
if os.path.isabs(filename):
111+
return []
112+
113+
for dirname in sys.path:
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 OSError:
123+
pass
124+
else:
125+
return []
126+
try:
127+
with _tokenize_open(fullname) as fp:
128+
lines = fp.readlines()
129+
except OSError:
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+
137+
#### ---- avoiding having a tokenize2 backport for now ----
138+
from codecs import lookup, BOM_UTF8
139+
import re
140+
cookie_re = re.compile(r'^[ \t\f]*#.*coding[:=][ \t]*([-\w.]+)'.encode('utf8'))
141+
blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)'.encode('utf8'))
142+
143+
144+
def _tokenize_open(filename):
145+
"""Open a file in read only mode using the encoding detected by
146+
detect_encoding().
147+
"""
148+
buffer = io.open(filename, 'rb')
149+
encoding, lines = _detect_encoding(buffer.readline)
150+
buffer.seek(0)
151+
text = io.TextIOWrapper(buffer, encoding, line_buffering=True)
152+
text.mode = 'r'
153+
return text
154+
155+
156+
def _get_normal_name(orig_enc):
157+
"""Imitates get_normal_name in tokenizer.c."""
158+
# Only care about the first 12 characters.
159+
enc = orig_enc[:12].lower().replace("_", "-")
160+
if enc == "utf-8" or enc.startswith("utf-8-"):
161+
return "utf-8"
162+
if enc in ("latin-1", "iso-8859-1", "iso-latin-1") or \
163+
enc.startswith(("latin-1-", "iso-8859-1-", "iso-latin-1-")):
164+
return "iso-8859-1"
165+
return orig_enc
166+
167+
168+
def _detect_encoding(readline):
169+
"""
170+
The detect_encoding() function is used to detect the encoding that should
171+
be used to decode a Python source file. It requires one argument, readline,
172+
in the same way as the tokenize() generator.
173+
174+
It will call readline a maximum of twice, and return the encoding used
175+
(as a string) and a list of any lines (left as bytes) it has read in.
176+
177+
It detects the encoding from the presence of a utf-8 bom or an encoding
178+
cookie as specified in pep-0263. If both a bom and a cookie are present,
179+
but disagree, a SyntaxError will be raised. If the encoding cookie is an
180+
invalid charset, raise a SyntaxError. Note that if a utf-8 bom is found,
181+
'utf-8-sig' is returned.
182+
183+
If no encoding is specified, then the default of 'utf-8' will be returned.
184+
"""
185+
try:
186+
filename = readline.__self__.name
187+
except AttributeError:
188+
filename = None
189+
bom_found = False
190+
encoding = None
191+
default = 'utf-8'
192+
def read_or_stop():
193+
try:
194+
return readline()
195+
except StopIteration:
196+
return b''
197+
198+
def find_cookie(line):
199+
try:
200+
# Decode as UTF-8. Either the line is an encoding declaration,
201+
# in which case it should be pure ASCII, or it must be UTF-8
202+
# per default encoding.
203+
line_string = line.decode('utf-8')
204+
except UnicodeDecodeError:
205+
msg = "invalid or missing encoding declaration"
206+
if filename is not None:
207+
msg = '{0} for {1!r}'.format(msg, filename)
208+
raise SyntaxError(msg)
209+
210+
match = cookie_re.match(line)
211+
if not match:
212+
return None
213+
encoding = _get_normal_name(match.group(1).decode('utf-8'))
214+
try:
215+
codec = lookup(encoding)
216+
except LookupError:
217+
# This behaviour mimics the Python interpreter
218+
if filename is None:
219+
msg = "unknown encoding: " + encoding
220+
else:
221+
msg = "unknown encoding for {!r}: {}".format(filename,
222+
encoding)
223+
raise SyntaxError(msg)
224+
225+
if bom_found:
226+
if encoding != 'utf-8':
227+
# This behaviour mimics the Python interpreter
228+
if filename is None:
229+
msg = 'encoding problem: utf-8'
230+
else:
231+
msg = 'encoding problem for {!r}: utf-8'.format(filename)
232+
raise SyntaxError(msg)
233+
encoding += '-sig'
234+
return encoding
235+
236+
first = read_or_stop()
237+
if first.startswith(BOM_UTF8):
238+
bom_found = True
239+
first = first[3:]
240+
default = 'utf-8-sig'
241+
if not first:
242+
return default, []
243+
244+
encoding = find_cookie(first)
245+
if encoding:
246+
return encoding, [first]
247+
if not blank_re.match(first):
248+
return default, [first]
249+
250+
second = read_or_stop()
251+
if not second:
252+
return default, [first]
253+
254+
encoding = find_cookie(second)
255+
if encoding:
256+
return encoding, [first, second]
257+
258+
return default, [first, second]
259+
260+

linecache2/tests/__init__.py

Whitespace-only changes.

linecache2/tests/inspect_fodder.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# line 1
2+
'A module docstring.'
3+
4+
import sys, inspect
5+
# line 5
6+
7+
# line 7
8+
def spam(a, b, c, d=3, e=4, f=5, *g, **h):
9+
eggs(b + d, c + f)
10+
11+
# line 11
12+
def eggs(x, y):
13+
"A docstring."
14+
global fr, st
15+
fr = inspect.currentframe()
16+
st = inspect.stack()
17+
p = x
18+
q = y / 0
19+
20+
# line 20
21+
class StupidGit:
22+
"""A longer,
23+
24+
indented
25+
26+
docstring."""
27+
# line 27
28+
29+
def abuse(self, a, b, c):
30+
"""Another
31+
32+
\tdocstring
33+
34+
containing
35+
36+
\ttabs
37+
\t
38+
"""
39+
self.argue(a, b, c)
40+
# line 40
41+
def argue(self, a, b, c):
42+
try:
43+
spam(a, b, c)
44+
except:
45+
self.ex = sys.exc_info()
46+
self.tr = inspect.trace()
47+
48+
# line 48
49+
class MalodorousPervert(StupidGit):
50+
pass
51+
52+
Tit = MalodorousPervert
53+
54+
class ParrotDroppings:
55+
pass
56+
57+
class FesteringGob(MalodorousPervert, ParrotDroppings):
58+
pass

0 commit comments

Comments
 (0)