10000 Demonstrate python/cpython#127012 · python/importlib_resources@912a9e5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 912a9e5

Browse files
kurtmckeejaraco
authored andcommitted
This adds an in-memory finder, loader, and traversable implementation, which allows the `Traversable` protocol and concrete methods to be tested. This additional infrastructure demonstrates python/cpython#127012, but also highlights that the `Traversable.joinpath()` concrete method raises `TraversalError` which is not getting caught in several places.
1 parent fa27acb commit 912a9e5

File tree

3 files changed

+153
-8
lines changed

3 files changed

+153
-8
lines changed

importlib_resources/tests/test_functional.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
from . import util
88
from .compat.py39 import warnings_helper
99

10-
# Since the functional API forwards to Traversable, we only test
11-
# filesystem resources here -- not zip files, namespace packages etc.
12-
# We do test for two kinds of Anchor, though.
13-
1410

1511
class StringAnchorMixin:
1612
anchor01 = 'data01'
@@ -27,7 +23,7 @@ def anchor02(self):
2723
return importlib.import_module('data02')
2824

2925

30-
class FunctionalAPIBase(util.DiskSetup):
26+
class FunctionalAPIBase:
3127
def setUp(self):
3228
super().setUp()
3329
self.load_fixture('data02')
@@ -244,17 +240,28 @@ def test_text_errors(self):
244240
)
245241

246242

247-
class FunctionalAPITest_StringAnchor(
243+
class FunctionalAPITest_StringAnchor_Disk(
248244
StringAnchorMixin,
249245
FunctionalAPIBase,
246+
util.DiskSetup,
250247
unittest.TestCase,
251248
):
252249
pass
253250

254251

255-
class FunctionalAPITest_ModuleAnchor(
252+
class FunctionalAPITest_ModuleAnchor_Disk(
256253
ModuleAnchorMixin,
257254
FunctionalAPIBase,
255+
util.DiskSetup,
256+
unittest.TestCase,
257+
):
258+
pass
259+
260+
261+
class FunctionalAPITest_StringAnchor_Memory(
262+
StringAnchorMixin,
263+
FunctionalAPIBase,
264+
util.MemorySetup,
258265
unittest.TestCase,
259266
):
260267
pass
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import unittest
2+
3+
from .util import MemorySetup, Traversable
4+
5+
6+
class TestMemoryTraversableImplementation(unittest.TestCase):
7+
def test_concrete_methods_are_not_overridden(self):
8+
"""`MemoryTraversable` must not override `Traversable` concrete methods.
9+
10+
This test is not an attempt to enforce a particular `Traversable` protocol;
11+
it merely catches changes in the `Traversable` abstract/concrete methods
12+
that have not been mirrored in the `MemoryTraversable` subclass.
13+
"""
14+
15+
traversable_concrete_methods = {
16+
method
17+
for method, value in Traversable.__dict__.items()
18+
if callable(value) and method not in Traversable.__abstractmethods__
19+
}
20+
memory_traversable_concrete_methods = {
21+
method
22+
for method, value in MemorySetup.MemoryTraversable.__dict__.items()
23+
if callable(value) and not method.startswith("__")
24+
}
25+
overridden_methods = (
26+
memory_traversable_concrete_methods & traversable_concrete_methods
27+
)
28+
29+
if overridden_methods:
30+
raise AssertionError(
31+
"MemorySetup.MemoryTraversable overrides Traversable concrete methods, "
32+
"which may mask problems in the Traversable protocol. "
33+
"Please remove the following methods in MemoryTraversable: "
34+
+ ", ".join(overridden_methods)
35+
)

importlib_resources/tests/util.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import abc
22
import contextlib
3+
import functools
34
import importlib
45
import io
56
import pathlib
67
import sys
78
import types
89
from importlib.machinery import ModuleSpec
910

10-
from ..abc import ResourceReader
11+
from ..abc import ResourceReader, Traversable, TraversableResources
1112
from . import _path
1213
from . import zip as zip_
1314
from .compat.py39 import import_helper, os_helper
@@ -200,5 +201,107 @@ def tree_on_path(self, spec):
200201
self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir))
201202

202203

204+
class MemorySetup(ModuleSetup):
205+
"""Support loading a module in memory."""
206+
207+
MODULE = 'data01'
208+
209+
def load_fixture(self, module):
210+
self.fixtures.enter_context(self.augment_sys_metapath(module))
211+
return importlib.import_module(module)
212+
213+
@contextlib.contextmanager
214+
def augment_sys_metapath(self, module):
215+
finder_instance = self.MemoryFinder(module)
216+
sys.meta_path.append(finder_instance)
217+
yield
218+
sys.meta_path.remove(finder_instance)
219+
220+
class MemoryFinder(importlib.abc.MetaPathFinder):
221+
def __init__(self, module):
222+
self._module = module
223+
224+
def find_spec(self, fullname, path, target=None):
225+
if fullname != self._module:
226+
return None
227+
228+
return importlib.machinery.ModuleSpec(
229+
name=fullname,
230+
loader=MemorySetup.MemoryLoader(self._module),
231+
is_package=True,
232+
)
233+
234+
class MemoryLoader(importlib.abc.Loader):
235+
def __init__(self, module):
236+
self._module = module
237+
238+
def exec_module(self, module):
239+
pass
240+
241+
def get_resource_reader(self, fullname):
242+
return MemorySetup.MemoryTraversableResources(self._module, fullname)
243+
244+
class MemoryTraversableResources(TraversableResources):
245+
def __init__(self, module, fullname):
246+
self._module = module
247+
self._fullname = fullname
248+
249+
def files(self):
250+
return MemorySetup.MemoryTraversable(self._module, self._fullname)
251+
252+
class MemoryTraversable(Traversable):
253+
"""Implement only the abstract methods of `Traversable`.
254+
255+
Besides `.__init__()`, no other methods may be implemented or overridden.
256+
This is critical for validating the concrete `Traversable` implementations.
257+
"""
258+
259+
def __init__(self, module, fullname):
260+
self._module = module
261+
self._fullname = fullname
262+
263+
def iterdir(self):
264+
path = pathlib.PurePosixPath(self._fullname)
265+
directory = functools.reduce(lambda d, p: d[p], path.parts, fixtures)
266+
if not isinstance(directory, dict):
267+
# Filesystem openers raise OSError, and that exception is mirrored here.
268+
raise OSError(f"{self._fullname} is not a directory")
269+
for path in directory:
270+
yield MemorySetup.MemoryTraversable(
271+
self._module, f"{self._fullname}/{path}"
272+
)
273+
274+
def is_dir(self) -> bool:
275+
path = pathlib.PurePosixPath(self._fullname)
276+
# Fully traverse the `fixtures` dictionary.
277+
# This should be wrapped in a `try/except KeyError`
278+
# but it is not currently needed, and lowers the code coverage numbers.
279+
directory = functools.reduce(lambda d, p: d[p], path.parts, fixtures)
280+
return isinstance(directory, dict)
281+
282+
def is_file(self) -> bool:
283+
path = pathlib.PurePosixPath(self._fullname)
284+
directory = functools.reduce(lambda d, p: d[p], path.parts, fixtures)
285+
return not isinstance(directory, dict)
286+
287+
def open(self, mode='r', encoding=None, errors=None, *_, **__):
288+
path = pathlib.PurePosixPath(self._fullname)
289+
contents = functools.reduce(lambda d, p: d[p], path.parts, fixtures)
290+
if isinstance(contents, dict):
291+
# Filesystem openers raise OSError when attempting to open a directory,
292+
# and that exception is mirrored here.
293+
raise OSError(f"{self._fullname} is a directory")
294+
if isinstance(contents, str):
295+
contents = contents.encode("utf-8")
296+
result = io.BytesIO(contents)
297+
if "b" in mode:
298+
return result
299+
return io.TextIOWrapper(result, encoding=encoding, errors=errors)
300+
301+
@property
302+
def name(self):
303+
return pathlib.PurePosixPath(self._fullname).name
304+
305+
203306
class CommonTests(DiskSetup, CommonTestsBase):
204307
pass

0 commit comments

Comments
 (0)
0