Skip to content

Commit e4c596d

Browse files
[3.14] gh-133253: making linecache thread-safe (GH-133305) (gh-143910)
(cherry picked from commit 8054184) Co-authored-by: vfdev <[email protected]>
1 parent 6219497 commit e4c596d

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

@@ -149,23 +141,27 @@ def updatecache(filename, module_globals=None):
149141

150142
# Realise a lazy loader based lookup if there is one
151143
# otherwise try to lookup right now.
152-
if lazycache(filename, module_globals):
144+
lazy_entry = entry if entry is not None and len(entry) == 1 else None
145+
if lazy_entry is None:
146+
lazy_entry = _make_lazycache_entry(filename, module_globals)
147+
if lazy_entry is not None:
153148
try:
154-
data = cache[filename][0]()
149+
data = lazy_entry[0]()
155150
except (ImportError, OSError):
156151
pass
157152
else:
158153
if data is None:
159154
# No luck, the PEP302 loader cannot find the source
160155
# for this module.
161156
return []
162-
cache[filename] = (
157+
entry = (
163158
len(data),
164159
None,
165160
[line + '\n' for line in data.splitlines()],
166161
fullname
167162
)
168-
return cache[filename][2]
163+
cache[filename] = entry
164+
return entry[2]
169165

170166
# Try looking through the module search path, which is only useful
171167
# when handling a relative filename.
@@ -214,13 +210,20 @@ def lazycache(filename, module_globals):
214210
get_source method must be found, the filename must be a cacheable
215211
filename, and the filename must not be already cached.
216212
"""
217-
if filename in cache:
218-
if len(cache[filename]) == 1:
219-
return True
220-
else:
221-
return False
213+
entry = cache.get(filename, None)
214+
if entry is not None:
215+
return len(entry) == 1
216+
217+
lazy_entry = _make_lazycache_entry(filename, module_globals)
218+
if lazy_entry is not None:
219+
cache[filename] = lazy_entry
220+
return True
221+
return False
222+
223+
224+
def _make_lazycache_entry(filename, module_globals):
222225
if not filename or (filename.startswith('<') and filename.endswith('>')):
223-
return False
226+
return None
224227
# Try for a __loader__, if available
225228
if module_globals and '__name__' in module_globals:
226229
spec = module_globals.get('__spec__')
@@ -233,9 +236,10 @@ def lazycache(filename, module_globals):
233236
if name and get_source:
234237
def get_lines(name=name, *args, **kwargs):
235238
return get_source(name, *args, **kwargs)
236-
cache[filename] = (get_lines,)
237-
return True
238-
return False
239+
return (get_lines,)
240+
return None
241+
242+
239243

240244
def _register_code(code, string, name):
241245
entry = (len(string),
@@ -248,4 +252,5 @@ def _register_code(code, string, name):
248252
for const in code.co_consts:
249253
if isinstance(const, type(code)):
250254
stack.append(const)
251-
_interactive_cache[_make_key(code)] = entry
255+
key = _make_key(code)
256+
_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)