I don’t think that’s correct, actually — or at least, if it is correct, it’s only a temporary situation.
Don’t forget that, even though func_timeout()
has returned, redis/__init__.py
is still sleeping, so it’s not yet completed and been cleaned up.
Two things are informative:
- Running with
python3 -v
, so you can see imports happening as they go
- Adding some
sleep()
s to the mock_airflow.py
code, to match the ones added to the redis/__init__.py
code.
If you do those two things, i.e.:
--- mock_airflow.py 2024-08-09 10:54:55.000000000 -0400
+++ my_mock_airflow.py 2024-08-10 06:14:27.110493907 -0400
@@ -1,6 +1,6 @@
import os
import sys
-
+import time
from func_timeout import func_timeout, FunctionTimedOut
@@ -27,12 +27,17 @@
except FunctionTimedOut:
print("FunctionTimedOut\n")
+time.sleep(2)
print("After failed import redis")
print(f"'redis' in sys.modules: {'redis' in sys.modules}")
print(f"'redis.client' in sys.modules: {'redis.client' in sys.modules}")
print()
+time.sleep(6)
+print("After waiting")
+print(f"'redis' in sys.modules: {'redis' in sys.modules}")
+print(f"'redis.client' in sys.modules: {'redis.client' in sys.modules}")
print("Reimport mock_kombu_transport_redis")
Do that and run it with python3 -v -m mock_airflow
, and what you’ll see is something like this:
$ python3 -v -m mock_airflow
import _frozen_importlib # frozen
import _imp # builtin
import '_thread' # <class '_frozen_importlib.BuiltinImporter'>
[...]
import 'asyncio.selector_events' # <_frozen_importlib_external.SourceFileLoader object at 0x7fb56ad6b350>
import 'asyncio.unix_events' # <_frozen_importlib_external.SourceFileLoader object at 0x7fb56ad3fc80>
import 'asyncio' # <_frozen_importlib_external.SourceFileLoader object at 0x7fb56bfeb7d0>
[...]
import 'redis.commands' # <_frozen_importlib_external.SourceFileLoader object at 0x7fb56a9af5f0>
# /tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/__pycache__/lock.cpython-312.pyc matches /tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/lock.py
# code object from '/tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/__pycache__/lock.cpython-312.pyc'
import 'redis.lock' # <_frozen_importlib_external.SourceFileLoader object at 0x7fb56aa84dd0>
import 'redis.client' # <_frozen_importlib_external.SourceFileLoader object at 0x7fb56a9ad790>
import 'redis.asyncio.client' # <_frozen_importlib_external.SourceFileLoader object at 0x7fb56bfe8f80>
[...]import 'redis.sentinel' # <_frozen_importlib_external.SourceFileLoader object at 0x7fb56bfe8bc0>
FunctionTimedOut
After failed import redis
'redis' in sys.modules: True
'redis.client' in sys.modules: True
# destroy mock_kombu_transport_redis
After waiting
'redis' in sys.modules: False
'redis.client' in sys.modules: True
In other words, sys.modules['redis']
only exists, on your initial check, because the attempt to import mock_kombu_transport_redis
is still incomplete, waiting for the sleep(10)
in redis/__init__.py
to complete. When it does, the import tracing prints,
# destroy mock_kombu_transport_redis
…and both that module and redis
are removed from sys.modules
because they failed to successfully import. But a ton of the REST of Redis did successfully import, so now when you attempt to import redis
again, those inner modules aren’t reinitialized because they’re already in sys.modules
. And because they aren’t getting reimported, they don’t end up in the redis
namespace.
You can easily create the same situation “by hand” with stock Redis (or any package). Try this in the REPL:
>>> import sys; from pprint import pp
>>> import redis
>>> redis.client
<module 'redis.client' from '/tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/client.py'>
>>> del redis
>>> sys.modules.pop('redis')
<module 'redis' from '/tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/__init__.py'>
>>> import redis
>>> pp(dir(redis))
['AuthenticationError',
'AuthenticationWrongNumberOfArgsError',
'BlockingConnectionPool',
'BusyLoadingError',
'ChildDeadlockedError',
'Connection',
'ConnectionError',
'ConnectionPool',
'CredentialProvider',
'DataError',
'InvalidResponse',
'OutOfMemoryError',
'PubSubError',
'ReadOnlyError',
'Redis',
'RedisCluster',
'RedisError',
'ResponseError',
'SSLConnection',
'Sentinel',
'SentinelConnectionPool',
'SentinelManagedConnection',
'SentinelManagedSSLConnection',
'StrictRedis',
'TimeoutError',
'UnixDomainSocketConnection',
'UsernamePasswordCredentialProvider',
'VERSION',
'WatchError',
'__all__',
'__builtins__',
'__cached__',
'__doc__',
'__file__',
'__loader__',
'__name__',
'__package__',
'__path__',
'__spec__',
'__version__',
'asyncio',
'default_backoff',
'from_url',
'int_or_str',
'metadata',
'os',
'sys',
'time']
(For fun, as long as 'redis.client'
is in sys.modules
but 'redis'
isn’t, you can even do an import redis.client
or from redis.client import Redis
, either of which will work fine — before or after import redis
— but neither one will cause redis.client
to actually be defined. Unless the redis.client
module gets forcibly ejected from sys.modules
and reloaded, the symbol won’t be imported into redis
.)
…If you also sys.modules.pop('redis.client')
, then an import redis
WILL reimport redis.client
and client
will be a member of redis
, but redis
will still be missing all of the other redis.*
symbols that don’t get reinitialized.
>>> del redis
>>> sys.modules.pop('redis.client')
<module 'redis.client' from '/tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/client.py'>
>>> sys.modules.pop('redis')
<module 'redis' from '/tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/__init__.py'>
>>> import redis
# /tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/__pycache__/__init__.cpython-312.pyc matches /tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/__init__.py
# code object from '/tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/__pycache__/__init__.cpython-312.pyc'
# /tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/__pycache__/client.cpython-312.pyc matches /tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/client.py
# code object from '/tmp/mcve/venv3.12/lib64/python3.12/site-packages/redis/__pycache__/client.cpython-312.pyc'
import 'redis.client' # <_frozen_importlib_external.SourceFileLoader object at 0x7faf0f695be0>
import 'redis' # <_frozen_importlib_external.SourceFileLoader object at 0x7faf0f695700>
>>> 'client' in dir(redis)
True
>>> [x in dir(redis) for x in ['sentinel', 'backoff', 'cluster', 'crc']]
[False, False, False, False]