| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,82 @@ | |||
| 1 | + from contextlib import suppress | ||
| 2 | + | ||
| 3 | + from . import abc | ||
| 4 | + | ||
| 5 | + | ||
| 6 | + class SpecLoaderAdapter: | ||
| 7 | + """ | ||
| 8 | + Adapt a package spec to adapt the underlying loader. | ||
| 9 | + """ | ||
| 10 | + | ||
| 11 | + def __init__(self, spec, adapter=lambda spec: spec.loader): | ||
| 12 | + self.spec = spec | ||
| 13 | + self.loader = adapter(spec) | ||
| 14 | + | ||
| 15 | + def __getattr__(self, name): | ||
| 16 | + return getattr(self.spec, name) | ||
| 17 | + | ||
| 18 | + | ||
| 19 | + class TraversableResourcesLoader: | ||
| 20 | + """ | ||
| 21 | + Adapt a loader to provide TraversableResources. | ||
| 22 | + """ | ||
| 23 | + | ||
| 24 | + def __init__(self, spec): | ||
| 25 | + self.spec = spec | ||
| 26 | + | ||
| 27 | + def get_resource_reader(self, name): | ||
| 28 | + return DegenerateFiles(self.spec)._native() | ||
| 29 | + | ||
| 30 | + | ||
| 31 | + class DegenerateFiles: | ||
| 32 | + """ | ||
| 33 | + Adapter for an existing or non-existant resource reader | ||
| 34 | + to provide a degenerate .files(). | ||
| 35 | + """ | ||
| 36 | + | ||
| 37 | + class Path(abc.Traversable): | ||
| 38 | + def iterdir(self): | ||
| 39 | + return iter(()) | ||
| 40 | + | ||
| 41 | + def is_dir(self): | ||
| 42 | + return False | ||
| 43 | + | ||
| 44 | + is_file = exists = is_dir # type: ignore | ||
| 45 | + | ||
| 46 | + def joinpath(self, other): | ||
| 47 | + return DegenerateFiles.Path() | ||
| 48 | + | ||
| 49 | + def name(self): | ||
| 50 | + return '' | ||
| 51 | + | ||
| 52 | + def open(self): | ||
| 53 | + raise ValueError() | ||
| 54 | + | ||
| 55 | + def __init__(self, spec): | ||
| 56 | + self.spec = spec | ||
| 57 | + | ||
| 58 | + @property | ||
| 59 | + def _reader(self): | ||
| 60 | + with suppress(AttributeError): | ||
| 61 | + return self.spec.loader.get_resource_reader(self.spec.name) | ||
| 62 | + | ||
| 63 | + def _native(self): | ||
| 64 | + """ | ||
| 65 | + Return the native reader if it supports files(). | ||
| 66 | + """ | ||
| 67 | + reader = self._reader | ||
| 68 | + return reader if hasattr(reader, 'files') else self | ||
| 69 | + | ||
| 70 | + def __getattr__(self, attr): | ||
| 71 | + return getattr(self._reader, attr) | ||
| 72 | + | ||
| 73 | + def files(self): | ||
| 74 | + return DegenerateFiles.Path() | ||
| 75 | + | ||
| 76 | + | ||
| 77 | + def wrap_spec(package): | ||
| 78 | + """ | ||
| 79 | + Construct a package spec with traversable compatibility | ||
| 80 | + on the spec/loader/reader. | ||
| 81 | + """ | ||
| 82 | + return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,6 +9,8 @@ | |||
| 9 | 9 | from typing import Union, Any, Optional | |
| 10 | 10 | from .abc import ResourceReader | |
| 11 | 11 | ||
| 12 | + from ._adapters import wrap_spec | ||
| 13 | + | ||
| 12 | 14 | Package = Union[types.ModuleType, str] | |
| 13 | 15 | ||
| 14 | 16 | ||
@@ -61,7 +63,7 @@ def get_package(package): | |||
| 61 | 63 | Raise an exception if the resolved module is not a package. | |
| 62 | 64 | """ | |
| 63 | 65 | resolved = resolve(package) | |
| 64 | - if resolved.__spec__.submodule_search_locations is None: | ||
| 66 | + if wrap_spec(resolved).submodule_search_locations is None: | ||
| 65 | 67 | raise TypeError('{!r} is not a package'.format(package)) | |
| 66 | 68 | return resolved | |
| 67 | 69 | ||
@@ -71,7 +73,7 @@ def from_package(package): | |||
| 71 | 73 | Return a Traversable object for the given package. | |
| 72 | 74 | ||
| 73 | 75 | """ | |
| 74 | - spec = package.__spec__ | ||
| 76 | + spec = wrap_spec(package) | ||
| 75 | 77 | reader = spec.loader.get_resource_reader(spec.name) | |
| 76 | 78 | return reader.files() | |
| 77 | 79 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -352,26 +352,28 @@ def iterdir(self): | |||
| 352 | 352 | Yield Traversable objects in self | |
| 353 | 353 | """ | |
| 354 | 354 | ||
| 355 | - @abc.abstractmethod | ||
| 356 | 355 | def read_bytes(self): | |
| 357 | 356 | """ | |
| 358 | 357 | Read contents of self as bytes | |
| 359 | 358 | """ | |
| 359 | + with self.open('rb') as strm: | ||
| 360 | + return strm.read() | ||
| 360 | 361 | ||
| 361 | - @abc.abstractmethod | ||
| 362 | 362 | def read_text(self, encoding=None): | |
| 363 | 363 | """ | |
| 364 | - Read contents of self as bytes | ||
| 364 | + Read contents of self as text | ||
| 365 | 365 | """ | |
| 366 | + with self.open(encoding=encoding) as strm: | ||
| 367 | + return strm.read() | ||
| 366 | 368 | ||
| 367 | 369 | @abc.abstractmethod | |
| 368 | - def is_dir(self): | ||
| 370 | + def is_dir(self) -> bool: | ||
| 369 | 371 | """ | |
| 370 | 372 | Return True if self is a dir | |
| 371 | 373 | """ | |
| 372 | 374 | ||
| 373 | 375 | @abc.abstractmethod | |
| 374 | - def is_file(self): | ||
| 376 | + def is_file(self) -> bool: | ||
| 375 | 377 | """ | |
| 376 | 378 | Return True if self is a file | |
| 377 | 379 | """ | |
@@ -382,11 +384,11 @@ def joinpath(self, child): | |||
| 382 | 384 | Return Traversable child in self | |
| 383 | 385 | """ | |
| 384 | 386 | ||
| 385 | - @abc.abstractmethod | ||
| 386 | 387 | def __truediv__(self, child): | |
| 387 | 388 | """ | |
| 388 | 389 | Return Traversable child in self | |
| 389 | 390 | """ | |
| 391 | + return self.joinpath(child) | ||
| 390 | 392 | ||
| 391 | 393 | @abc.abstractmethod | |
| 392 | 394 | def open(self, mode='r', *args, **kwargs): | |
@@ -406,6 +408,11 @@ def name(self) -> str: | |||
| 406 | 408 | ||
| 407 | 409 | ||
| 408 | 410 | class TraversableResources(ResourceReader): | |
| 411 | + """ | ||
| 412 | + The required interface for providing traversable | ||
| 413 | + resources. | ||
| 414 | + """ | ||
| 415 | + | ||
| 409 | 416 | @abc.abstractmethod | |
| 410 | 417 | def files(self): | |
| 411 | 418 | """Return a Traversable object for the loaded package.""" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,3 +1,4 @@ | |||
| 1 | + import io | ||
| 1 | 2 | import unittest | |
| 2 | 3 | ||
| 3 | 4 | from importlib import resources | |
@@ -37,6 +38,17 @@ def test_natural_path(self): | |||
| 37 | 38 | assert 'data' in str(path) | |
| 38 | 39 | ||
| 39 | 40 | ||
| 41 | + class PathMemoryTests(PathTests, unittest.TestCase): | ||
| 42 | + def setUp(self): | ||
| 43 | + file = io.BytesIO(b'Hello, UTF-8 world!\n') | ||
| 44 | + self.addCleanup(file.close) | ||
| 45 | + self.data = util.create_package( | ||
| 46 | + file=file, path=FileNotFoundError("package exists only in memory") | ||
| 47 | + ) | ||
| 48 | + self.data.__spec__.origin = None | ||
| 49 | + self.data.__spec__.has_location = False | ||
| 50 | + | ||
| 51 | + | ||
| 40 | 52 | class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase): | |
| 41 | 53 | def test_remove_in_context_manager(self): | |
| 42 | 54 | # It is not an error if the file that was temporarily stashed on the | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,53 @@ | |||
| 1 | + """ | ||
| 2 | + Generate the zip test data files. | ||
| 3 | + | ||
| 4 | + Run to build the tests/zipdataNN/ziptestdata.zip files from | ||
| 5 | + files in tests/dataNN. | ||
| 6 | + | ||
| 7 | + Replaces the file with the working copy, but does commit anything | ||
| 8 | + to the source repo. | ||
| 9 | + """ | ||
| 10 | + | ||
| 11 | + import contextlib | ||
| 12 | + import os | ||
| 13 | + import pathlib | ||
| 14 | + import zipfile | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + def main(): | ||
| 18 | + """ | ||
| 19 | + >>> from unittest import mock | ||
| 20 | + >>> monkeypatch = getfixture('monkeypatch') | ||
| 21 | + >>> monkeypatch.setattr(zipfile, 'ZipFile', mock.MagicMock()) | ||
| 22 | + >>> print(); main() # print workaround for bpo-32509 | ||
| 23 | + <BLANKLINE> | ||
| 24 | + ...data01... -> ziptestdata/... | ||
| 25 | + ... | ||
| 26 | + ...data02... -> ziptestdata/... | ||
| 27 | + ... | ||
| 28 | + """ | ||
| 29 | + suffixes = '01', '02' | ||
| 30 | + tuple(map(generate, suffixes)) | ||
| 31 | + | ||
| 32 | + | ||
| 33 | + def generate(suffix): | ||
| 34 | + root = pathlib.Path(__file__).parent.relative_to(os.getcwd()) | ||
| 35 | + zfpath = root / f'zipdata{suffix}/ziptestdata.zip' | ||
| 36 | + with zipfile.ZipFile(zfpath, 'w') as zf: | ||
| 37 | + for src, rel in walk(root / f'data{suffix}'): | ||
| 38 | + dst = 'ziptestdata' / pathlib.PurePosixPath(rel.as_posix()) | ||
| 39 | + print(src, '->', dst) | ||
| 40 | + zf.write(src, dst) | ||
| 41 | + | ||
| 42 | + | ||
| 43 | + def walk(datapath): | ||
| 44 | + for dirpath, dirnames, filenames in os.walk(datapath): | ||
| 45 | + with contextlib.suppress(KeyError): | ||
| 46 | + dirnames.remove('__pycache__') | ||
| 47 | + for filename in filenames: | ||
| 48 | + res = pathlib.Path(dirpath) / filename | ||
| 49 | + rel = res.relative_to(datapath) | ||
| 50 | + yield res, rel | ||
| 51 | + | ||
| 52 | + | ||
| 53 | + __name__ == '__main__' and main() | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments