| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent fbf75b9 commit 6714825
21 files changed
| 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 | |
|---|---|---|---|
@@ -1259,6 +1259,10 @@ def load_module(self, fullname): | |||
| 1259 | 1259 | # Warning implemented in _load_module_shim(). | |
| 1260 | 1260 | return _bootstrap._load_module_shim(self, fullname) | |
| 1261 | 1261 | ||
| 1262 | + def get_resource_reader(self, module): | ||
| 1263 | + from importlib.readers import NamespaceReader | ||
| 1264 | + return NamespaceReader(self._path) | ||
| 1265 | + | ||
| 1262 | 1266 | ||
| 1263 | 1267 | # Finders ##################################################################### | |
| 1264 | 1268 | ||
| 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 | ||
@@ -43,18 +45,15 @@ def get_resource_reader(package): | |||
| 43 | 45 | # zipimport.zipimporter does not support weak references, resulting in a | |
| 44 | 46 | # TypeError. That seems terrible. | |
| 45 | 47 | spec = package.__spec__ | |
| 46 | - reader = getattr(spec.loader, 'get_resource_reader', None) | ||
| 48 | + reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore | ||
| 47 | 49 | if reader is None: | |
| 48 | 50 | return None | |
| 49 | - return reader(spec.name) | ||
| 51 | + return reader(spec.name) # type: ignore | ||
| 50 | 52 | ||
| 51 | 53 | ||
| 52 | 54 | def resolve(cand): | |
| 53 | 55 | # type: (Package) -> types.ModuleType | |
| 54 | - return ( | ||
| 55 | - cand if isinstance(cand, types.ModuleType) | ||
| 56 | - else importlib.import_module(cand) | ||
| 57 | - ) | ||
| 56 | + return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand) | ||
| 58 | 57 | ||
| 59 | 58 | ||
| 60 | 59 | def get_package(package): | |
@@ -64,7 +63,7 @@ def get_package(package): | |||
| 64 | 63 | Raise an exception if the resolved module is not a package. | |
| 65 | 64 | """ | |
| 66 | 65 | resolved = resolve(package) | |
| 67 | - if resolved.__spec__.submodule_search_locations is None: | ||
| 66 | + if wrap_spec(resolved).submodule_search_locations is None: | ||
| 68 | 67 | raise TypeError('{!r} is not a package'.format(package)) | |
| 69 | 68 | return resolved | |
| 70 | 69 | ||
@@ -74,7 +73,7 @@ def from_package(package): | |||
| 74 | 73 | Return a Traversable object for the given package. | |
| 75 | 74 | ||
| 76 | 75 | """ | |
| 77 | - spec = package.__spec__ | ||
| 76 | + spec = wrap_spec(package) | ||
| 78 | 77 | reader = spec.loader.get_resource_reader(spec.name) | |
| 79 | 78 | return reader.files() | |
| 80 | 79 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,7 @@ | |||
| 14 | 14 | from ._abc import Loader | |
| 15 | 15 | import abc | |
| 16 | 16 | import warnings | |
| 17 | + from typing import BinaryIO, Iterable, Text | ||
| 17 | 18 | from typing import Protocol, runtime_checkable | |
| 18 | 19 | ||
| 19 | 20 | ||
@@ -297,49 +298,45 @@ def set_data(self, path, data): | |||
| 297 | 298 | ||
| 298 | 299 | ||
| 299 | 300 | class ResourceReader(metaclass=abc.ABCMeta): | |
| 300 | - | ||
| 301 | - """Abstract base class to provide resource-reading support. | ||
| 302 | - | ||
| 303 | - Loaders that support resource reading are expected to implement | ||
| 304 | - the ``get_resource_reader(fullname)`` method and have it either return None | ||
| 305 | - or an object compatible with this ABC. | ||
| 306 | - """ | ||
| 301 | + """Abstract base class for loaders to provide resource reading support.""" | ||
| 307 | 302 | ||
| 308 | 303 | @abc.abstractmethod | |
| 309 | - def open_resource(self, resource): | ||
| 304 | + def open_resource(self, resource: Text) -> BinaryIO: | ||
| 310 | 305 | """Return an opened, file-like object for binary reading. | |
| 311 | 306 | ||
| 312 | - The 'resource' argument is expected to represent only a file name | ||
| 313 | - and thus not contain any subdirectory components. | ||
| 314 | - | ||
| 307 | + The 'resource' argument is expected to represent only a file name. | ||
| 315 | 308 | If the resource cannot be found, FileNotFoundError is raised. | |
| 316 | 309 | """ | |
| 310 | + # This deliberately raises FileNotFoundError instead of | ||
| 311 | + # NotImplementedError so that if this method is accidentally called, | ||
| 312 | + # it'll still do the right thing. | ||
| 317 | 313 | raise FileNotFoundError | |
| 318 | 314 | ||
| 319 | 315 | @abc.abstractmethod | |
| 320 | - def resource_path(self, resource): | ||
| 316 | + def resource_path(self, resource: Text) -> Text: | ||
| 321 | 317 | """Return the file system path to the specified resource. | |
| 322 | 318 | ||
| 323 | - The 'resource' argument is expected to represent only a file name | ||
| 324 | - and thus not contain any subdirectory components. | ||
| 325 | - | ||
| 319 | + The 'resource' argument is expected to represent only a file name. | ||
| 326 | 320 | If the resource does not exist on the file system, raise | |
| 327 | 321 | FileNotFoundError. | |
| 328 | 322 | """ | |
| 323 | + # This deliberately raises FileNotFoundError instead of | ||
| 324 | + # NotImplementedError so that if this method is accidentally called, | ||
| 325 | + # it'll still do the right thing. | ||
| 329 | 326 | raise FileNotFoundError | |
| 330 | 327 | ||
| 331 | 328 | @abc.abstractmethod | |
| 332 | - def is_resource(self, name): | ||
| 333 | - """Return True if the named 'name' is consider a resource.""" | ||
| 329 | + def is_resource(self, path: Text) -> bool: | ||
| 330 | + """Return True if the named 'path' is a resource. | ||
| 331 | + | ||
| 332 | + Files are resources, directories are not. | ||
| 333 | + """ | ||
| 334 | 334 | raise FileNotFoundError | |
| 335 | 335 | ||
| 336 | 336 | @abc.abstractmethod | |
| 337 | - def contents(self): | ||
| 338 | - """Return an iterable of strings over the contents of the package.""" | ||
| 339 | - return [] | ||
| 340 | - | ||
| 341 | - | ||
| 342 | - _register(ResourceReader, machinery.SourceFileLoader) | ||
| 337 | + def contents(self) -> Iterable[str]: | ||
| 338 | + """Return an iterable of entries in `package`.""" | ||
| 339 | + raise FileNotFoundError | ||
| 343 | 340 | ||
| 344 | 341 | ||
| 345 | 342 | @runtime_checkable | |
@@ -355,26 +352,28 @@ def iterdir(self): | |||
| 355 | 352 | Yield Traversable objects in self | |
| 356 | 353 | """ | |
| 357 | 354 | ||
| 358 | - @abc.abstractmethod | ||
| 359 | 355 | def read_bytes(self): | |
| 360 | 356 | """ | |
| 361 | 357 | Read contents of self as bytes | |
| 362 | 358 | """ | |
| 359 | + with self.open('rb') as strm: | ||
| 360 | + return strm.read() | ||
| 363 | 361 | ||
| 364 | - @abc.abstractmethod | ||
| 365 | 362 | def read_text(self, encoding=None): | |
| 366 | 363 | """ | |
| 367 | - Read contents of self as bytes | ||
| 364 | + Read contents of self as text | ||
| 368 | 365 | """ | |
| 366 | + with self.open(encoding=encoding) as strm: | ||
| 367 | + return strm.read() | ||
| 369 | 368 | ||
| 370 | 369 | @abc.abstractmethod | |
| 371 | - def is_dir(self): | ||
| 370 | + def is_dir(self) -> bool: | ||
| 372 | 371 | """ | |
| 373 | 372 | Return True if self is a dir | |
| 374 | 373 | """ | |
| 375 | 374 | ||
| 376 | 375 | @abc.abstractmethod | |
| 377 | - def is_file(self): | ||
| 376 | + def is_file(self) -> bool: | ||
| 378 | 377 | """ | |
| 379 | 378 | Return True if self is a file | |
| 380 | 379 | """ | |
@@ -385,11 +384,11 @@ def joinpath(self, child): | |||
| 385 | 384 | Return Traversable child in self | |
| 386 | 385 | """ | |
| 387 | 386 | ||
| 388 | - @abc.abstractmethod | ||
| 389 | 387 | def __truediv__(self, child): | |
| 390 | 388 | """ | |
| 391 | 389 | Return Traversable child in self | |
| 392 | 390 | """ | |
| 391 | + return self.joinpath(child) | ||
| 393 | 392 | ||
| 394 | 393 | @abc.abstractmethod | |
| 395 | 394 | def open(self, mode='r', *args, **kwargs): | |
@@ -402,14 +401,18 @@ def open(self, mode='r', *args, **kwargs): | |||
| 402 | 401 | """ | |
| 403 | 402 | ||
| 404 | 403 | @abc.abstractproperty | |
| 405 | - def name(self): | ||
| 406 | - # type: () -> str | ||
| 404 | + def name(self) -> str: | ||
| 407 | 405 | """ | |
| 408 | 406 | The base name of this object without any parent references. | |
| 409 | 407 | """ | |
| 410 | 408 | ||
| 411 | 409 | ||
| 412 | 410 | class TraversableResources(ResourceReader): | |
| 411 | + """ | ||
| 412 | + The required interface for providing traversable | ||
| 413 | + resources. | ||
| 414 | + """ | ||
| 415 | + | ||
| 413 | 416 | @abc.abstractmethod | |
| 414 | 417 | def files(self): | |
| 415 | 418 | """Return a Traversable object for the loaded package.""" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,8 +1,13 @@ | |||
| 1 | + import collections | ||
| 1 | 2 | import zipfile | |
| 2 | 3 | import pathlib | |
| 3 | 4 | from . import abc | |
| 4 | 5 | ||
| 5 | 6 | ||
| 7 | + def remove_duplicates(items): | ||
| 8 | + return iter(collections.OrderedDict.fromkeys(items)) | ||
| 9 | + | ||
| 10 | + | ||
| 6 | 11 | class FileReader(abc.TraversableResources): | |
| 7 | 12 | def __init__(self, loader): | |
| 8 | 13 | self.path = pathlib.Path(loader.path).parent | |
@@ -39,3 +44,80 @@ def is_resource(self, path): | |||
| 39 | 44 | ||
| 40 | 45 | def files(self): | |
| 41 | 46 | return zipfile.Path(self.archive, self.prefix) | |
| 47 | + | ||
| 48 | + | ||
| 49 | + class MultiplexedPath(abc.Traversable): | ||
| 50 | + """ | ||
| 51 | + Given a series of Traversable objects, implement a merged | ||
| 52 | + version of the interface across all objects. Useful for | ||
| 53 | + namespace packages which may be multihomed at a single | ||
| 54 | + name. | ||
| 55 | + """ | ||
| 56 | + | ||
| 57 | + def __init__(self, *paths): | ||
| 58 | + self._paths = list(map(pathlib.Path, remove_duplicates(paths))) | ||
| 59 | + if not self._paths: | ||
| 60 | + message = 'MultiplexedPath must contain at least one path' | ||
| 61 | + raise FileNotFoundError(message) | ||
| 62 | + if not all(path.is_dir() for path in self._paths): | ||
| 63 | + raise NotADirectoryError('MultiplexedPath only supports directories') | ||
| 64 | + | ||
| 65 | + def iterdir(self): | ||
| 66 | + visited = [] | ||
| 67 | + for path in self._paths: | ||
| 68 | + for file in path.iterdir(): | ||
| 69 | + if file.name in visited: | ||
| 70 | + continue | ||
| 71 | + visited.append(file.name) | ||
| 72 | + yield file | ||
| 73 | + | ||
| 74 | + def read_bytes(self): | ||
| 75 | + raise FileNotFoundError(f'{self} is not a file') | ||
| 76 | + | ||
| 77 | + def read_text(self, *args, **kwargs): | ||
| 78 | + raise FileNotFoundError(f'{self} is not a file') | ||
| 79 | + | ||
| 80 | + def is_dir(self): | ||
| 81 | + return True | ||
| 82 | + | ||
| 83 | + def is_file(self): | ||
| 84 | + return False | ||
| 85 | + | ||
| 86 | + def joinpath(self, child): | ||
| 87 | + # first try to find child in current paths | ||
| 88 | + for file in self.iterdir(): | ||
| 89 | + if file.name == child: | ||
| 90 | + return file | ||
| 91 | + # if it does not exist, construct it with the first path | ||
| 92 | + return self._paths[0] / child | ||
| 93 | + | ||
| 94 | + __truediv__ = joinpath | ||
| 95 | + | ||
| 96 | + def open(self, *args, **kwargs): | ||
| 97 | + raise FileNotFoundError('{} is not a file'.format(self)) | ||
| 98 | + | ||
| 99 | + def name(self): | ||
| 100 | + return self._paths[0].name | ||
| 101 | + | ||
| 102 | + def __repr__(self): | ||
| 103 | + return 'MultiplexedPath({})'.format( | ||
| 104 | + ', '.join("'{}'".format(path) for path in self._paths) | ||
| 105 | + ) | ||
| 106 | + | ||
| 107 | + | ||
| 108 | + class NamespaceReader(abc.TraversableResources): | ||
| 109 | + def __init__(self, namespace_path): | ||
| 110 | + if 'NamespacePath' not in str(namespace_path): | ||
| 111 | + raise ValueError('Invalid path') | ||
| 112 | + self.path = MultiplexedPath(*list(namespace_path)) | ||
| 113 | + | ||
| 114 | + def resource_path(self, resource): | ||
| 115 | + """ | ||
| 116 | + Return the file system path to prevent | ||
| 117 | + `resources.path()` from creating a temporary | ||
| 118 | + copy. | ||
| 119 | + """ | ||
| 120 | + return str(self.path.joinpath(resource)) | ||
| 121 | + | ||
| 122 | + def files(self): | ||
| 123 | + return self.path | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments