@@ -60,13 +60,17 @@ cimport cython
60
60
from cpython.module cimport PyImport_ImportModuleLevel
61
61
from cpython.object cimport PyObject_RichCompare
62
62
from cpython.number cimport PyNumber_TrueDivide, PyNumber_Power, PyNumber_Index
63
+ from cpython.pystate cimport PyThreadState, PyThreadState_Get
64
+ cdef extern from * :
65
+ ctypedef PyThreadState volatile_PyThreadState " volatile PyThreadState"
63
66
64
67
import os
65
68
import sys
66
69
from six.moves import builtins, cPickle as pickle
67
70
import inspect
68
- from . import sageinspect
71
+ from time import sleep
69
72
73
+ from . import sageinspect
70
74
from .lazy_import_cache import get_cache_file
71
75
72
76
@@ -1243,6 +1247,11 @@ cdef class LazyModule:
1243
1247
return LazyImport(self .module, name = attr, as_name = NotImplemented , ** self .kwds)
1244
1248
1245
1249
1250
+ # Unique thread which is inside a LazyImportContext(). To avoid
1251
+ # threading issues, at most one thread can do this. Due to the Python
1252
+ # GIL, there are no race conditions in accessing this global pointer.
1253
+ cdef volatile_PyThreadState* lazyimport_thread = NULL
1254
+
1246
1255
class LazyImportContext (object ):
1247
1256
"""
1248
1257
Context in which all imports become lazy imports.
@@ -1327,7 +1336,26 @@ class LazyImportContext(object):
1327
1336
sage: with lazyimport:
1328
1337
....: print(__import__)
1329
1338
<bound method LazyImportContext.__import__ of <sage.misc.lazy_import.LazyImportContext object at ...>>
1330
- """
1339
+
1340
+ Nesting is illegal::
1341
+
1342
+ sage: with lazyimport:
1343
+ ....: with lazyimport:
1344
+ ....: pass
1345
+ Traceback (most recent call last):
1346
+ ...
1347
+ RuntimeError: nesting "with lazyimport" contexts is not allowed
1348
+ """
1349
+ global lazyimport_thread
1350
+ while lazyimport_thread is not NULL :
1351
+ if lazyimport_thread is PyThreadState_Get():
1352
+ # Current thread is already doing lazy imports.
1353
+ raise RuntimeError (' nesting "with lazyimport" contexts is not allowed' )
1354
+ # A different thread is doing lazy imports, yield the GIL
1355
+ # for 20ms.
1356
+ sleep(0.020 )
1357
+
1358
+ lazyimport_thread = PyThreadState_Get()
1331
1359
self .old_import = builtins.__import__
1332
1360
builtins.__import__ = self .__import__
1333
1361
return self
@@ -1351,8 +1379,21 @@ class LazyImportContext(object):
1351
1379
Exception
1352
1380
sage: print(__import__)
1353
1381
<built-in function __import__>
1382
+
1383
+ We get a message if ``__import__`` was changed inside the
1384
+ context::
1385
+
1386
+ sage: from six.moves import builtins
1387
+ sage: with lazyimport:
1388
+ ....: builtins.__import__ = None
1389
+ WARNING: __import__ was changed inside a "with lazyimport" context to None (possibly by a different thread)
1354
1390
"""
1391
+ global lazyimport_thread
1392
+ if builtins.__import__ != self .__import__:
1393
+ print (f' WARNING: __import__ was changed inside a "with lazyimport" context to {builtins.__import__} (possibly by a different thread)' )
1355
1394
builtins.__import__ = self .old_import
1395
+ del self .old_import
1396
+ lazyimport_thread = NULL
1356
1397
1357
1398
def __call__ (self , **kwds ):
1358
1399
"""
@@ -1373,17 +1414,62 @@ class LazyImportContext(object):
1373
1414
"""
1374
1415
Replacement function for ``builtins.__import__``.
1375
1416
1417
+ It is not allowed to call this outside of a ``with lazyimport``
1418
+ context. If one thread does ``with lazyimport``, then other
1419
+ threads doing imports will fall back to the previous (non-lazy)
1420
+ ``__import__`` function.
1421
+
1376
1422
TESTS::
1377
1423
1378
1424
sage: from sage.misc.lazy_import import lazyimport
1379
- sage: lazyimport.__import__("sage.all", {}, {}, ["ZZ"])
1425
+ sage: with lazyimport as lazy:
1426
+ ....: print(lazy.__import__("sage.all", {}, {}, ["ZZ"]))
1380
1427
<lazy imported module 'sage.all'>
1428
+ sage: lazyimport.__import__("sage.all", {}, {}, ["ZZ"])
1429
+ Traceback (most recent call last):
1430
+ ...
1431
+ RuntimeError: __import__ called outside "with lazyimport" context
1381
1432
sage: with lazyimport:
1382
1433
....: import sage.all
1383
1434
Traceback (most recent call last):
1384
1435
...
1385
1436
RuntimeError: lazy import only works with 'from ... import ...' imports, not with 'import ...'
1386
- """
1437
+
1438
+ We test 100 threads each doing an import, half of them lazy and
1439
+ half of them a real import. Note that this will not cause a
1440
+ lot of CPU activity due to the Python GIL. ::
1441
+
1442
+ sage: from sage.misc.lazy_import import LazyImport, lazyimport
1443
+ sage: from threading import Thread
1444
+ sage: from time import sleep
1445
+ sage: def do_import():
1446
+ ....: sleep(0.001) # Yield the GIL
1447
+ ....: from sage.structure.sage_object import SageObject
1448
+ ....: t = type(SageObject)
1449
+ ....: if t is not type:
1450
+ ....: print("SageObject should be type, got {!r}".format(t))
1451
+ sage: def do_lazy_import():
1452
+ ....: with lazyimport:
1453
+ ....: sleep(0.001) # Yield the GIL
1454
+ ....: from sage.structure.sage_object import SageObject
1455
+ ....: t = type(SageObject)
1456
+ ....: if t is not LazyImport:
1457
+ ....: print("SageObject should be LazyImport, got {!r}".format(t))
1458
+ sage: threads = [Thread(target=do_import if i%2 else do_lazy_import) for i in range(100)]
1459
+ sage: for T in threads:
1460
+ ....: T.start()
1461
+ sage: for T in threads:
1462
+ ....: T.join()
1463
+ """
1464
+ # If __import__ was called from a different thread, call
1465
+ # old_import() instead.
1466
+ if PyThreadState_Get() is not lazyimport_thread:
1467
+ try :
1468
+ old = self .old_import
1469
+ except AttributeError :
1470
+ raise RuntimeError (' __import__ called outside "with lazyimport" context' )
1471
+ return old(module, globals , locals , fromlist, level)
1472
+
1387
1473
if not fromlist:
1388
1474
raise RuntimeError (" lazy import only works with 'from ... import ...' imports, not with 'import ...'" )
1389
1475
return LazyModule(module, namespace = globals , level = level, ** self .kwds)
0 commit comments