Skip to content

Commit 0f21962

Browse files
colesburyvfdev-5
andauthored
[3.13] gh-133253: making linecache thread-safe (GH-133305) (#143911)
(cherry picked from commit 8054184) Co-authored-by: vfdev <vfdev.5@gmail.com>
1 parent 7025d75 commit 0f21962

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

@@ -74,12 +72,8 @@ def checkcache(filename=None):
7472
filenames = [filename]
7573

7674
for filename in filenames:
77-
try:
78-
entry = cache[filename]
79-
except KeyError:
80-
continue
81-
82-
if len(entry) == 1:
75+
entry = cache.get(filename, None)
76+
if entry is None or len(entry) == 1:
8377
# lazy cache entry, leave it lazy.
8478
continue
8579
size, mtime, lines, fullname = entry
@@ -115,9 +109,7 @@ def updatecache(filename, module_globals=None):
115109
# These import can fail if the interpreter is shutting down
116110
return []
117111

118-
if filename in cache:
119-
if len(cache[filename]) != 1:
120-
cache.pop(filename, None)
112+
entry = cache.pop(filename, None)
121113
if not filename or (filename.startswith('<') and filename.endswith('>')):
122114
return []
123115

@@ -129,23 +121,27 @@ def updatecache(filename, module_globals=None):
129121

130122
# Realise a lazy loader based lookup if there is one
131123
# otherwise try to lookup right now.
132-
if lazycache(filename, module_globals):
124+
lazy_entry = entry if entry is not None and len(entry) == 1 else None
125+
if lazy_entry is None:
126+
lazy_entry = _make_lazycache_entry(filename, module_globals)
127+
if lazy_entry is not None:
133128
try:
134-
data = cache[filename][0]()
129+
data = lazy_entry[0]()
135130
except (ImportError, OSError):
136131
pass
137132
else:
138133
if data is None:
139134
# No luck, the PEP302 loader cannot find the source
140135
# for this module.
141136
return []
142-
cache[filename] = (
137+
entry = (
143138
len(data),
144139
None,
145140
[line + '\n' for line in data.splitlines()],
146141
fullname
147142
)
148-
return cache[filename][2]
143+
cache[filename] = entry
144+
return entry[2]
149145

150146
# Try looking through the module search path, which is only useful
151147
# when handling a relative filename.
@@ -194,13 +190,20 @@ def lazycache(filename, module_globals):
194190
get_source method must be found, the filename must be a cacheable
195191
filename, and the filename must not be already cached.
196192
"""
197-
if filename in cache:
198-
if len(cache[filename]) == 1:
199-
return True
200-
else:
201-
return False
193+
entry = cache.get(filename, None)
194+
if entry is not None:
195+
return len(entry) == 1
196+
197+
lazy_entry = _make_lazycache_entry(filename, module_globals)
198+
if lazy_entry is not None:
199+
cache[filename] = lazy_entry
200+
return True
201+
return False
202+
203+
204+
def _make_lazycache_entry(filename, module_globals):
202205
if not filename or (filename.startswith('<') and filename.endswith('>')):
203-
return False
206+
return None
204207
# Try for a __loader__, if available
205208
if module_globals and '__name__' in module_globals:
206209
spec = module_globals.get('__spec__')
@@ -213,9 +216,10 @@ def lazycache(filename, module_globals):
213216
if name and get_source:
214217
def get_lines(name=name, *args, **kwargs):
215218
return get_source(name, *args, **kwargs)
216-
cache[filename] = (get_lines,)
217-
return True
218-
return False
219+
return (get_lines,)
220+
return None
221+
222+
219223

220224
def _register_code(code, string, name):
221225
entry = (len(string),
@@ -228,4 +232,5 @@ def _register_code(code, string, name):
228232
for const in code.co_consts:
229233
if isinstance(const, type(code)):
230234
stack.append(const)
231-
_interactive_cache[_make_key(code)] = entry
235+
key = _make_key(code)
236+
_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

@@ -361,5 +363,40 @@ def test_checkcache_with_no_parameter(self):
361363
self.assertIn(self.unchanged_file, linecache.cache)
362364

363365

366+
class MultiThreadingTest(unittest.TestCase):
367+
@threading_helper.reap_threads
368+
@threading_helper.requires_working_threading()
369+
def test_read_write_safety(self):
370+
371+
with tempfile.TemporaryDirectory() as tmpdirname:
372+
filenames = []
373+
for i in range(10):
374+
name = os.path.join(tmpdirname, f"test_{i}.py")
375+
with open(name, "w") as h:
376+
h.write("import time\n")
377+
h.write("import system\n")
378+
filenames.append(name)
379+
380+
def linecache_get_line(b):
381+
b.wait()
382+
for _ in range(100):
383+
for name in filenames:
384+
linecache.getline(name, 1)
385+
386+
def check(funcs):
387+
barrier = threading.Barrier(len(funcs))
388+
threads = []
389+
390+
for func in funcs:
391+
thread = threading.Thread(target=func, args=(barrier,))
392+
393+
threads.append(thread)
394+
395+
with threading_helper.start_threads(threads):
396+
pass
397+
398+
check([linecache_get_line] * 20)
399+
400+
364401
if __name__ == "__main__":
365402
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)