summaryrefslogtreecommitdiffstats
path: root/Lib/test/test_importlib/test_lazy.py
diff options
context:
space:
mode:
authorChris Markiewicz <effigies@gmail.com>2024-02-24 00:02:16 (GMT)
committerGitHub <noreply@github.com>2024-02-24 00:02:16 (GMT)
commit200271c61db44d90759f8a8934949aefd72d5724 (patch)
tree9fddf73bc8d17f6d71c8ac26591911c98a445a9c /Lib/test/test_importlib/test_lazy.py
parentef6074b352a95706f44a592ffe31baace690cc1c (diff)
downloadcpython-200271c61db44d90759f8a8934949aefd72d5724.zip
cpython-200271c61db44d90759f8a8934949aefd72d5724.tar.gz
cpython-200271c61db44d90759f8a8934949aefd72d5724.tar.bz2
gh-114763: Protect lazy loading modules from attribute access races (GH-114781)
Setting the __class__ attribute of a lazy-loading module to ModuleType enables other threads to attempt to access attributes before the loading is complete. Now that is protected by a lock.
Diffstat (limited to 'Lib/test/test_importlib/test_lazy.py')
-rw-r--r--Lib/test/test_importlib/test_lazy.py42
1 files changed, 40 insertions, 2 deletions
diff --git a/Lib/test/test_importlib/test_lazy.py b/Lib/test/test_importlib/test_lazy.py
index cc993f3..38ab219 100644
--- a/Lib/test/test_importlib/test_lazy.py
+++ b/Lib/test/test_importlib/test_lazy.py
@@ -2,9 +2,12 @@ import importlib
from importlib import abc
from importlib import util
import sys
+import time
+import threading
import types
import unittest
+from test.support import threading_helper
from test.test_importlib import util as test_util
@@ -40,6 +43,7 @@ class TestingImporter(abc.MetaPathFinder, abc.Loader):
module_name = 'lazy_loader_test'
mutated_name = 'changed'
loaded = None
+ load_count = 0
source_code = 'attr = 42; __name__ = {!r}'.format(mutated_name)
def find_spec(self, name, path, target=None):
@@ -48,8 +52,10 @@ class TestingImporter(abc.MetaPathFinder, abc.Loader):
return util.spec_from_loader(name, util.LazyLoader(self))
def exec_module(self, module):
+ time.sleep(0.01) # Simulate a slow load.
exec(self.source_code, module.__dict__)
self.loaded = module
+ self.load_count += 1
class LazyLoaderTests(unittest.TestCase):
@@ -59,8 +65,9 @@ class LazyLoaderTests(unittest.TestCase):
# Classes that don't define exec_module() trigger TypeError.
util.LazyLoader(object)
- def new_module(self, source_code=None):
- loader = TestingImporter()
+ def new_module(self, source_code=None, loader=None):
+ if loader is None:
+ loader = TestingImporter()
if source_code is not None:
loader.source_code = source_code
spec = util.spec_from_loader(TestingImporter.module_name,
@@ -140,6 +147,37 @@ class LazyLoaderTests(unittest.TestCase):
# Force the load; just care that no exception is raised.
module.__name__
+ @threading_helper.requires_working_threading()
+ def test_module_load_race(self):
+ with test_util.uncache(TestingImporter.module_name):
+ loader = TestingImporter()
+ module = self.new_module(loader=loader)
+ self.assertEqual(loader.load_count, 0)
+
+ class RaisingThread(threading.Thread):
+ exc = None
+ def run(self):
+ try:
+ super().run()
+ except Exception as exc:
+ self.exc = exc
+
+ def access_module():
+ return module.attr
+
+ threads = []
+ for _ in range(2):
+ threads.append(thread := RaisingThread(target=access_module))
+ thread.start()
+
+ # Races could cause errors
+ for thread in threads:
+ thread.join()
+ self.assertIsNone(thread.exc)
+
+ # Or multiple load attempts
+ self.assertEqual(loader.load_count, 1)
+
if __name__ == '__main__':
unittest.main()