https://github.com/python/cpython/commit/8054184f9f32c7ba561e6e23b358074824e4928d
commit: 8054184f9f32c7ba561e6e23b358074824e4928d
branch: main
author: vfdev <vfde...@gmail.com>
committer: kumaraditya303 <kumaradi...@python.org>
date: 2025-05-09T12:15:16+05:30
summary:

gh-133253: making linecache thread-safe (#133305)

Co-authored-by: Sam Gross <colesb...@gmail.com>

files:
A Misc/NEWS.d/next/Library/2025-05-05-10-41-41.gh-issue-133253.J5-xDD.rst
M Lib/linecache.py
M Lib/test/test_linecache.py

diff --git a/Lib/linecache.py b/Lib/linecache.py
index 87d7d6fda657e4..ef73d1aa99774a 100644
--- a/Lib/linecache.py
+++ b/Lib/linecache.py
@@ -33,10 +33,9 @@ def getlines(filename, module_globals=None):
     """Get the lines for a Python source file from the cache.
     Update the cache if it doesn't contain an entry for this file already."""
 
-    if filename in cache:
-        entry = cache[filename]
-        if len(entry) != 1:
-            return cache[filename][2]
+    entry = cache.get(filename, None)
+    if entry is not None and len(entry) != 1:
+        return entry[2]
 
     try:
         return updatecache(filename, module_globals)
@@ -56,10 +55,9 @@ def _make_key(code):
 
 def _getlines_from_code(code):
     code_id = _make_key(code)
-    if code_id in _interactive_cache:
-        entry = _interactive_cache[code_id]
-        if len(entry) != 1:
-            return _interactive_cache[code_id][2]
+    entry = _interactive_cache.get(code_id, None)
+    if entry is not None and len(entry) != 1:
+        return entry[2]
     return []
 
 
@@ -84,12 +82,8 @@ def checkcache(filename=None):
         filenames = [filename]
 
     for filename in filenames:
-        try:
-            entry = cache[filename]
-        except KeyError:
-            continue
-
-        if len(entry) == 1:
+        entry = cache.get(filename, None)
+        if entry is None or len(entry) == 1:
             # lazy cache entry, leave it lazy.
             continue
         size, mtime, lines, fullname = entry
@@ -125,9 +119,7 @@ def updatecache(filename, module_globals=None):
         # These import can fail if the interpreter is shutting down
         return []
 
-    if filename in cache:
-        if len(cache[filename]) != 1:
-            cache.pop(filename, None)
+    entry = cache.pop(filename, None)
     if _source_unavailable(filename):
         return []
 
@@ -146,9 +138,12 @@ def updatecache(filename, module_globals=None):
 
         # Realise a lazy loader based lookup if there is one
         # otherwise try to lookup right now.
-        if lazycache(filename, module_globals):
+        lazy_entry = entry if entry is not None and len(entry) == 1 else None
+        if lazy_entry is None:
+            lazy_entry = _make_lazycache_entry(filename, module_globals)
+        if lazy_entry is not None:
             try:
-                data = cache[filename][0]()
+                data = lazy_entry[0]()
             except (ImportError, OSError):
                 pass
             else:
@@ -156,13 +151,14 @@ def updatecache(filename, module_globals=None):
                     # No luck, the PEP302 loader cannot find the source
                     # for this module.
                     return []
-                cache[filename] = (
+                entry = (
                     len(data),
                     None,
                     [line + '\n' for line in data.splitlines()],
                     fullname
                 )
-                return cache[filename][2]
+                cache[filename] = entry
+                return entry[2]
 
         # Try looking through the module search path, which is only useful
         # when handling a relative filename.
@@ -211,13 +207,20 @@ def lazycache(filename, module_globals):
         get_source method must be found, the filename must be a cacheable
         filename, and the filename must not be already cached.
     """
-    if filename in cache:
-        if len(cache[filename]) == 1:
-            return True
-        else:
-            return False
+    entry = cache.get(filename, None)
+    if entry is not None:
+        return len(entry) == 1
+
+    lazy_entry = _make_lazycache_entry(filename, module_globals)
+    if lazy_entry is not None:
+        cache[filename] = lazy_entry
+        return True
+    return False
+
+
+def _make_lazycache_entry(filename, module_globals):
     if not filename or (filename.startswith('<') and filename.endswith('>')):
-        return False
+        return None
     # Try for a __loader__, if available
     if module_globals and '__name__' in module_globals:
         spec = module_globals.get('__spec__')
@@ -230,9 +233,10 @@ def lazycache(filename, module_globals):
         if name and get_source:
             def get_lines(name=name, *args, **kwargs):
                 return get_source(name, *args, **kwargs)
-            cache[filename] = (get_lines,)
-            return True
-    return False
+            return (get_lines,)
+    return None
+
+
 
 def _register_code(code, string, name):
     entry = (len(string),
@@ -245,4 +249,5 @@ def _register_code(code, string, name):
         for const in code.co_consts:
             if isinstance(const, type(code)):
                 stack.append(const)
-        _interactive_cache[_make_key(code)] = entry
+        key = _make_key(code)
+        _interactive_cache[key] = entry
diff --git a/Lib/test/test_linecache.py b/Lib/test/test_linecache.py
index e4aa41ebb43762..02f65338428c8f 100644
--- a/Lib/test/test_linecache.py
+++ b/Lib/test/test_linecache.py
@@ -4,10 +4,12 @@
 import unittest
 import os.path
 import tempfile
+import threading
 import tokenize
 from importlib.machinery import ModuleSpec
 from test import support
 from test.support import os_helper
+from test.support import threading_helper
 from test.support.script_helper import assert_python_ok
 
 
@@ -374,5 +376,40 @@ def test_checkcache_with_no_parameter(self):
         self.assertIn(self.unchanged_file, linecache.cache)
 
 
+class MultiThreadingTest(unittest.TestCase):
+    @threading_helper.reap_threads
+    @threading_helper.requires_working_threading()
+    def test_read_write_safety(self):
+
+        with tempfile.TemporaryDirectory() as tmpdirname:
+            filenames = []
+            for i in range(10):
+                name = os.path.join(tmpdirname, f"test_{i}.py")
+                with open(name, "w") as h:
+                    h.write("import time\n")
+                    h.write("import system\n")
+                filenames.append(name)
+
+            def linecache_get_line(b):
+                b.wait()
+                for _ in range(100):
+                    for name in filenames:
+                        linecache.getline(name, 1)
+
+            def check(funcs):
+                barrier = threading.Barrier(len(funcs))
+                threads = []
+
+                for func in funcs:
+                    thread = threading.Thread(target=func, args=(barrier,))
+
+                    threads.append(thread)
+
+                with threading_helper.start_threads(threads):
+                    pass
+
+            check([linecache_get_line] * 20)
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git 
a/Misc/NEWS.d/next/Library/2025-05-05-10-41-41.gh-issue-133253.J5-xDD.rst 
b/Misc/NEWS.d/next/Library/2025-05-05-10-41-41.gh-issue-133253.J5-xDD.rst
new file mode 100644
index 00000000000000..7009ca258bcabd
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-05-10-41-41.gh-issue-133253.J5-xDD.rst
@@ -0,0 +1 @@
+Fix thread-safety issues in :mod:`linecache`.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to