1import functools
2import pathlib
3from contextlib import suppress
4from types import SimpleNamespace
5
6from .. import readers, _adapters
7
8
9def _block_standard(reader_getter):
10 """
11 Wrap _adapters.TraversableResourcesLoader.get_resource_reader
12 and intercept any standard library readers.
13 """
14
15 @functools.wraps(reader_getter)
16 def wrapper(*args, **kwargs):
17 """
18 If the reader is from the standard library, return None to allow
19 allow likely newer implementations in this library to take precedence.
20 """
21 try:
22 reader = reader_getter(*args, **kwargs)
23 except NotADirectoryError:
24 # MultiplexedPath may fail on zip subdirectory
25 return
26 # Python 3.10+
27 mod_name = reader.__class__.__module__
28 if mod_name.startswith('importlib.') and mod_name.endswith('readers'):
29 return
30 # Python 3.8, 3.9
31 if isinstance(reader, _adapters.CompatibilityFiles) and (
32 reader.spec.loader.__class__.__module__.startswith('zipimport')
33 or reader.spec.loader.__class__.__module__.startswith(
34 '_frozen_importlib_external'
35 )
36 ):
37 return
38 return reader
39
40 return wrapper
41
42
43def _skip_degenerate(reader):
44 """
45 Mask any degenerate reader. Ref #298.
46 """
47 is_degenerate = (
48 isinstance(reader, _adapters.CompatibilityFiles) and not reader._reader
49 )
50 return reader if not is_degenerate else None
51
52
53class TraversableResourcesLoader(_adapters.TraversableResourcesLoader):
54 """
55 Adapt loaders to provide TraversableResources and other
56 compatibility.
57
58 Ensures the readers from importlib_resources are preferred
59 over stdlib readers.
60 """
61
62 def get_resource_reader(self, name):
63 return (
64 _skip_degenerate(_block_standard(super().get_resource_reader)(name))
65 or self._standard_reader()
66 or super().get_resource_reader(name)
67 )
68
69 def _standard_reader(self):
70 return self._zip_reader() or self._namespace_reader() or self._file_reader()
71
72 def _zip_reader(self):
73 with suppress(AttributeError):
74 return readers.ZipReader(self.spec.loader, self.spec.name)
75
76 def _namespace_reader(self):
77 with suppress(AttributeError, ValueError):
78 return readers.NamespaceReader(self.spec.submodule_search_locations)
79
80 def _file_reader(self):
81 try:
82 path = pathlib.Path(self.spec.origin)
83 except TypeError:
84 return None
85 if path.exists():
86 return readers.FileReader(SimpleNamespace(path=path))
87
88
89def wrap_spec(package):
90 """
91 Override _adapters.wrap_spec to use TraversableResourcesLoader
92 from above. Ensures that future behavior is always available on older
93 Pythons.
94 """
95 return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)