Skip to content

Commit 8054184

Browse files
vfdev-5colesbury
andauthored
gh-133253: making linecache thread-safe (#133305)
Co-authored-by: Sam Gross <colesbury@gmail.com>
1 parent 6d5a8c2 commit 8054184

File tree

3 files changed

+74
-31
lines changed

3 files changed

+74
-31
lines changed

Lib/linecache.py

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@ def getlines(filename, module_globals=None):
3333
"""Get the lines for a Python source file from the cache.
3434
Update the cache if it doesn't contain an entry for this file already."""
3535

36-
if filename in cache:
37-
entry = cache[filename]
38-
if len(entry) != 1:
39-
return cache[filename][2]
36+
entry = cache.get(filename, None)
37+
if entry is not None and len(entry) != 1:
38+
return entry[2]
4039

4140
try:
4241
return updatecache(filename, module_globals)
@@ -56,10 +55,9 @@ def _make_key(code):
5655

5756
def _getlines_from_code(code):
5857
code_id = _make_key(code)
59-
if code_id in _interactive_cache:
60-
entry = _interactive_cache[code_id]
61-
if len(entry) != 1:
62-
return _interactive_cache[code_id][2]
58+
entry = _interactive_cache.get(code_id, None)
59+
if entry is not None and len(entry) != 1:
60+
return entry[2]
6361
return []
6462

6563

@@ -84,12 +82,8 @@ def checkcache(filename=None):
8482
filenames = [filename]
8583

8684
for filename in filenames:
87-
try:
88-
entry = cache[filename]
89-
except KeyError:
90-
continue
91-
92-
if len(entry) == 1:
85+
entry = cache.get(filename, None)
86+
if entry is None or len(entry) == 1:
9387
# lazy cache entry, leave it lazy.
9488
continue
9589
size, mtime, lines, fullname = entry
@@ -125,9 +119,7 @@ def updatecache(filename, module_globals=None):
125119
# These import can fail if the interpreter is shutting down
126120
return []
127121

128-
if filename in cache:
129-
if len(cache[filename]) != 1:
130-
cache.pop(filename, None)
122+
entry = cache.pop(filename, None)
131123
if _source_unavailable(filename):
132124
return []
133125

@@ -146,23 +138,27 @@ def updatecache(filename, module_globals=None):
146138

147139
# Realise a lazy loader based lookup if there is one
148140
# otherwise try to lookup right now.
149-
if lazycache(filename, module_globals):
141+
lazy_entry = entry if entry is not None and len(entry) == 1 else None
142+
if lazy_entry is None:
143+
lazy_entry = _make_lazycache_entry(filename, module_globals)
144+
if lazy_entry is not None:
150145
try:
151-
data = cache[filename][0]()
146+
data = lazy_entry[0]()
152147
except (ImportError, OSError):
153148
pass
154149
else:
155150
if data is None:
156151
# No luck, the PEP302 loader cannot find the source
157152
# for this module.
158153
return []
159-
cache[filename] = (
154+
entry = (
160155
len(data),
161156
None,
162157
[line + '\n' for line in data.splitlines()],
163158
fullname
164159
)
165-
return cache[filename][2]
160+
cache[filename] = entry
161+
return entry[2]
166162

167163
# Try looking through the module search path, which is only useful
168164
# when handling a relative filename.
@@ -211,13 +207,20 @@ def lazycache(filename, module_globals):
211207
get_source method must be found, the filename must be a cacheable
212208
filename, and the filename must not be already cached.
213209
"""
214-
if filename in cache:
215-
if len(cache[filename]) == 1:
216-
return True
217-
else:
218-
return False
210+
entry = cache.get(filename, None)
211+
if entry is not None:
212+
return len(entry) == 1
213+
214+
lazy_entry = _make_lazycache_entry(filename, module_globals)
215+
if lazy_entry is not None:
216+
cache[filename] = lazy_entry
217+
return True
218+
return False
219+
220+
221+
def _make_lazycache_entry(filename, module_globals):
219222
if not filename or (filename.startswith('<') and filename.endswith('>')):
220-
return False
223+
return None
221224
# Try for a __loader__, if available
222225
if module_globals and '__name__' in module_globals:
223226
spec = module_globals.get('__spec__')
@@ -230,9 +233,10 @@ def lazycache(filename, module_globals):
230233
if name and get_source:
231234
def get_lines(name=name, *args, **kwargs):
232235
return get_source(name, *args, **kwargs)
233-
cache[filename] = (get_lines,)
234-
return True
235-
return False
236+
return (get_lines,)
237+
return None
238+
239+
236240

237241
def _register_code(code, string, name):
238242
entry = (len(string),
@@ -245,4 +249,5 @@ def _register_code(code, string, name):
245249
for const in code.co_consts:
246250
if isinstance(const, type(code)):
247251
stack.append(const)
248-
_interactive_cache[_make_key(code)] = entry
252+
key = _make_key(code)
253+
_interactive_cache[key] = entry

Lib/test/test_linecache.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import unittest
55
import os.path
66
import tempfile
7+
import threading
78
import tokenize
89
from importlib.machinery import ModuleSpec
910
from test import support
1011
from test.support import os_helper
12+
from test.support import threading_helper
1113
from test.support.script_helper import assert_python_ok
1214

1315

@@ -374,5 +376,40 @@ def test_checkcache_with_no_parameter(self):
374376
self.assertIn(self.unchanged_file, linecache.cache)
375377

376378

379+
class MultiThreadingTest(unittest.TestCase):
380+
@threading_helper.reap_threads
381+
@threading_helper.requires_working_threading()
382+
def test_read_write_safety(self):
383+
384+
with tempfile.TemporaryDirectory() as tmpdirname:
385+
filenames = []
386+
for i in range(10):
387+
name = os.path.join(tmpdirname, f"test_{i}.py")
388+
with open(name, "w") as h:
389+
h.write("import time\n")
390+
h.write("import system\n")
391+
filenames.append(name)
392+
393+
def linecache_get_line(b):
394+
b.wait()
395+
for _ in range(100):
396+
for name in filenames:
397+
linecache.getline(name, 1)
398+
399+
def check(funcs):
400+
barrier = threading.Barrier(len(funcs))
401+
threads = []
402+
403+
for func in funcs:
404+
thread = threading.Thread(target=func, args=(barrier,))
405+
406+
threads.append(thread)
407+
408+
with threading_helper.start_threads(threads):
409+
pass
410+
411+
check([linecache_get_line] * 20)
412+
413+
377414
if __name__ == "__main__":
378415
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix thread-safety issues in :mod:`linecache`.

0 commit comments

Comments
 (0)