Skip to content

Commit 9b5bed0

Browse files
committed
Issue #17911: traceback module overhaul
Provide a way to seed the linecache for a PEP-302 module without actually loading the code. Provide a new object API for traceback, including the ability to not lookup lines at all until the traceback is actually rendered, without any trace of the original objects being kept alive.
1 parent 32efec3 commit 9b5bed0

File tree

2 files changed

+110
-24
lines changed

2 files changed

+110
-24
lines changed

linecache2/__init__.py

+65-24
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
that name.
66
"""
77

8+
import functools
89
import io
910
import sys
1011
import os
@@ -22,7 +23,9 @@ def getline(filename, lineno, module_globals=None):
2223

2324
# The cache
2425

25-
cache = {} # The cache
26+
# The cache. Maps filenames to either a thunk which will provide source code,
27+
# or a tuple (size, mtime, lines, fullname) once loaded.
28+
cache = {}
2629

2730

2831
def clearcache():
@@ -37,6 +40,9 @@ def getlines(filename, module_globals=None):
3740
Update the cache if it doesn't contain an entry for this file already."""
3841

3942
if filename in cache:
43+
entry = cache[filename]
44+
if len(entry) == 1:
45+
return updatecache(filename, module_globals)
4046
return cache[filename][2]
4147
else:
4248
return updatecache(filename, module_globals)
@@ -55,7 +61,11 @@ def checkcache(filename=None):
5561
return
5662

5763
for filename in filenames:
58-
size, mtime, lines, fullname = cache[filename]
64+
entry = cache[filename]
65+
if len(entry) == 1:
66+
# lazy cache entry, leave it lazy.
67+
continue
68+
size, mtime, lines, fullname = entry
5969
if mtime is None:
6070
continue # no-op for files loaded via a __loader__
6171
try:
@@ -73,7 +83,8 @@ def updatecache(filename, module_globals=None):
7383
and return an empty list."""
7484

7585
if filename in cache:
76-
del cache[filename]
86+
if len(cache[filename]) != 1:
87+
del cache[filename]
7788
if not filename or (filename.startswith('<') and filename.endswith('>')):
7889
return []
7990

@@ -83,27 +94,23 @@ def updatecache(filename, module_globals=None):
8394
except OSError:
8495
basename = filename
8596

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

108115
# Try looking through the module search path, which is only useful
109116
# when handling a relative filename.
@@ -134,6 +141,40 @@ def updatecache(filename, module_globals=None):
134141
cache[filename] = size, mtime, lines, fullname
135142
return lines
136143

144+
145+
def lazycache(filename, module_globals):
146+
"""Seed the cache for filename with module_globals.
147+
148+
The module loader will be asked for the source only when getlines is
149+
called, not immediately.
150+
151+
If there is an entry in the cache already, it is not altered.
152+
153+
:return: True if a lazy load is registered in the cache,
154+
otherwise False. To register such a load a module loader with a
155+
get_source method must be found, the filename must be a cachable
156+
filename, and the filename must not be already cached.
157+
"""
158+
if filename in cache:
159+
if len(cache[filename]) == 1:
160+
return True
161+
else:
162+
return False
163+
if not filename or (filename.startswith('<') and filename.endswith('>')):
164+
return False
165+
# Try for a __loader__, if available
166+
if module_globals and '__loader__' in module_globals:
167+
name = module_globals.get('__name__')
168+
loader = module_globals['__loader__']
169+
get_source = getattr(loader, 'get_source', None)
170+
171+
if name and get_source:
172+
get_lines = functools.partial(get_source, name)
173+
cache[filename] = (get_lines,)
174+
return True
175+
return False
176+
177+
137178
#### ---- avoiding having a tokenize2 backport for now ----
138179
from codecs import lookup, BOM_UTF8
139180
import re

linecache2/tests/test_linecache.py

+45
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
FILENAME = os.__file__
1111
if FILENAME.endswith('.pyc'):
1212
FILENAME = FILENAME[:-1]
13+
NONEXISTENT_FILENAME = FILENAME + '.missing'
1314
INVALID_NAME = '!@$)(!@#_1'
1415
EMPTY = ''
1516
TESTS = 'inspect_fodder inspect_fodder2 mapping_tests'
@@ -137,3 +138,47 @@ def test_checkcache(self):
137138
for index, line in enumerate(source):
138139
self.assertEqual(line, getline(source_name, index + 1))
139140
source_list.append(line)
141+
142+
def test_lazycache_no_globals(self):
143+
lines = linecache.getlines(FILENAME)
144+
linecache.clearcache()
145+
self.assertEqual(False, linecache.lazycache(FILENAME, None))
146+
self.assertEqual(lines, linecache.getlines(FILENAME))
147+
148+
@unittest.skipIf("__loader__" not in globals(), "Modules not PEP302 by default")
149+
def test_lazycache_smoke(self):
150+
lines = linecache.getlines(NONEXISTENT_FILENAME, globals())
151+
linecache.clearcache()
152+
self.assertEqual(
153+
True, linecache.lazycache(NONEXISTENT_FILENAME, globals()))
154+
self.assertEqual(1, len(linecache.cache[NONEXISTENT_FILENAME]))
155+
# Note here that we're looking up a non existant filename with no
156+
# globals: this would error if the lazy value wasn't resolved.
157+
self.assertEqual(lines, linecache.getlines(NONEXISTENT_FILENAME))
158+
159+
def test_lazycache_provide_after_failed_lookup(self):
160+
linecache.clearcache()
161+
lines = linecache.getlines(NONEXISTENT_FILENAME, globals())
162+
linecache.clearcache()
163+
linecache.getlines(NONEXISTENT_FILENAME)
164+
linecache.lazycache(NONEXISTENT_FILENAME, globals())
165+
self.assertEqual(lines, linecache.updatecache(NONEXISTENT_FILENAME))
166+
167+
def test_lazycache_check(self):
168+
linecache.clearcache()
169+
linecache.lazycache(NONEXISTENT_FILENAME, globals())
170+
linecache.checkcache()
171+
172+
def test_lazycache_bad_filename(self):
173+
linecache.clearcache()
174+
self.assertEqual(False, linecache.lazycache('', globals()))
175+
self.assertEqual(False, linecache.lazycache('<foo>', globals()))
176+
177+
@unittest.skipIf("__loader__" not in globals(), "Modules not PEP302 by default")
178+
def test_lazycache_already_cached(self):
179+
linecache.clearcache()
180+
lines = linecache.getlines(NONEXISTENT_FILENAME, globals())
181+
self.assertEqual(
182+
False,
183+
linecache.lazycache(NONEXISTENT_FILENAME, globals()))
184+
self.assertEqual(4, len(linecache.cache[NONEXISTENT_FILENAME]))

0 commit comments

Comments
 (0)