|
1 | 1 | import abc
|
2 | 2 | import contextlib
|
| 3 | +import functools |
3 | 4 | import importlib
|
4 | 5 | import io
|
5 | 6 | import pathlib
|
6 | 7 | import sys
|
7 | 8 | import types
|
8 | 9 | from importlib.machinery import ModuleSpec
|
9 | 10 |
|
10 |
| -from ..abc import ResourceReader |
| 11 | +from ..abc import ResourceReader, Traversable, TraversableResources |
11 | 12 | from . import _path
|
12 | 13 | from . import zip as zip_
|
13 | 14 | from .compat.py39 import import_helper, os_helper
|
@@ -200,5 +201,107 @@ def tree_on_path(self, spec):
|
200 | 201 | self.fixtures.enter_context(import_helper.DirsOnSysPath(temp_dir))
|
201 | 202 |
|
202 | 203 |
|
| 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 | + |
203 | 306 | class CommonTests(DiskSetup, CommonTestsBase):
|
204 | 307 | pass
|
0 commit comments