If I do import A from within a directory containing both A.py and A.so, the .so file will be imported. I'm interested in changing the order of import file types, so that .py takes precedence over .so, though only temporarily, i.e. between code line i and j. Surely this can be achieved through some importlib magic?
Currently I get around the issue by copying the .py into a separate directory, prepending this directory to sys.path and then do the import, which is just awful.
The .so file(s) are cython-compiled versions of the .py files. I'm doing some custom code transformation on top of cython, for which I need to import the .py source even when the "equivalent" .so is present.
Here follows a simple test setup.
# A.py
import B
# B.py
import C
print('hello from B')
# C.py
pass
Running python A.py successfully prints out the message from B.py. Now add B.so (as the content of the .so files is irrelevant, having B.so really be a text file is fine):
# B.so
this is a fake binary
Now python A.py fails. Though importlib is the modern way of doing things, I so far only know how to import a specific file directly using the deprecated imp module. Updating A.py to
# A.py
import imp
B = imp.load_source('B', 'B.py')
makes it work again. However, introducing C.so breaks it again, as the lookup for the .py rather than .so is not registered globally in the import mechanism:
# C.so
this is a fake binary
Note that in this example I'm only allowed to edit A.py. I'm in need of a solution for Python 3.8, but I suspect any solution for 3.x works on 3.8 as well.
I now have a working solution. It's somewhat hacky, but I think it's robust.
It turns out that sys.path_importer_cache store various finders which in turn store a list of loaders, which are quarried by import in order. These loaders are stored as 2-tuples, with the first element exactly being the file extension which the given loader handles.
I simply traverse all list's of loaders and push those with .so extension to the back of the list's, achieving the lowest precedence possible (I could remove them completely, but then I can't import any .so files). I keep track of the changes to sys.path_importer_cache and undo them once I'm done with my special import. All of this is neatly wrapped up in a context manager:
import collections, contextlib, sys
@contextlib.contextmanager
def disable_loader(ext):
ext = '.' + ext.lstrip('.')
# Push any loaders for the ext extension to the back
edits = collections.defaultdict(list)
path_importer_cache = list(sys.path_importer_cache.values())
for i, finder in enumerate(path_importer_cache):
loaders = getattr(finder, '_loaders', None)
if loaders is None:
continue
for j, loader in enumerate(loaders):
if j + len(edits[i]) == len(loaders):
break
if loader[0] != ext:
continue
# Loader for the ext extension found.
# Push to the back.
loaders.append(loaders.pop(j))
edits[i].append(j)
try:
# Yield control back to the caller
yield
finally:
# Undo changes to path importer cache
for i, edit in edits.items():
loaders = path_importer_cache[i]._loaders
for j in reversed(edit):
loaders.insert(j, loaders.pop())
# Demonstrate import failure
try:
import A
except Exception as e:
print(e)
# Demonstrate solution
with disable_loader('.so'):
import A
# Demonstrate (wanted) failure outside with statement
import A2
Note that for the import A2 to fail properly, you need to copy the test setup so that you also have A2.py, B2.py, C2.py, B2.so and C2.so, which import each other in the same way as the original test files.
One can get rid of the somewhat complicated bookkeeping involving edits by just taking a complete backup copy.deepcopy(sys.path_importer_cache) before making the changes, and sticking this backup onto sys once done. It does work in the limited test above, but as various parts of the import machinery might hold references to the different nested objects, I thought it safer to use mutation only.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With