Skip to content

Commit

Permalink
Merge pull request #221 from FFY00/use-files-in-leacy-api
Browse files Browse the repository at this point in the history
Replace legacy API implementation with files()
  • Loading branch information
jaraco committed Jun 28, 2021
2 parents 7ff61a3 + e016e66 commit 5b2a675
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 192 deletions.
13 changes: 7 additions & 6 deletions importlib_resources/__init__.py
Expand Up @@ -3,19 +3,20 @@
from ._common import (
as_file,
files,
)

from importlib_resources._py3 import (
Package,
Resource,
)

from ._legacy import (
contents,
is_resource,
open_binary,
open_text,
path,
read_binary,
open_text,
read_text,
is_resource,
path,
)

from importlib_resources.abc import ResourceReader


Expand Down
108 changes: 98 additions & 10 deletions importlib_resources/_adapters.py
@@ -1,4 +1,5 @@
from contextlib import suppress
from io import TextIOWrapper

from . import abc

Expand All @@ -25,32 +26,119 @@ def __init__(self, spec):
self.spec = spec

def get_resource_reader(self, name):
return DegenerateFiles(self.spec)._native()
return CompatibilityFiles(self.spec)._native()


class DegenerateFiles:
def _io_wrapper(file, mode='r', *args, **kwargs):
if mode == 'r':
return TextIOWrapper(file, *args, **kwargs)
elif mode == 'rb':
return file
raise ValueError(
"Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode)
)


class CompatibilityFiles:
"""
Adapter for an existing or non-existant resource reader
to provide a degenerate .files().
to provide a compability .files().
"""

class Path(abc.Traversable):
class SpecPath(abc.Traversable):
"""
Path tied to a module spec.
Can be read and exposes the resource reader children.
"""

def __init__(self, spec, reader):
self._spec = spec
self._reader = reader

def iterdir(self):
if not self._reader:
return iter(())
return iter(
CompatibilityFiles.ChildPath(self._reader, path)
for path in self._reader.contents()
)

def is_file(self):
return False

is_dir = is_file

def joinpath(self, other):
if not self._reader:
return CompatibilityFiles.OrphanPath(other)
return CompatibilityFiles.ChildPath(self._reader, other)

@property
def name(self):
return self._spec.name

def open(self, mode='r', *args, **kwargs):
return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs)

class ChildPath(abc.Traversable):
"""
Path tied to a resource reader child.
Can be read but doesn't expose any meaningfull children.
"""

def __init__(self, reader, name):
self._reader = reader
self._name = name

def iterdir(self):
return iter(())

def is_file(self):
return self._reader.is_resource(self.name)

def is_dir(self):
return not self.is_file()

def joinpath(self, other):
return CompatibilityFiles.OrphanPath(self.name, other)

@property
def name(self):
return self._name

def open(self, mode='r', *args, **kwargs):
return _io_wrapper(
self._reader.open_resource(self.name), mode, *args, **kwargs
)

class OrphanPath(abc.Traversable):
"""
Orphan path, not tied to a module spec or resource reader.
Can't be read and doesn't expose any meaningful children.
"""

def __init__(self, *path_parts):
if len(path_parts) < 1:
raise ValueError('Need at least one path part to construct a path')
self._path = path_parts

def iterdir(self):
return iter(())

def is_file(self):
return False

is_file = exists = is_dir # type: ignore
is_dir = is_file

def joinpath(self, other):
return DegenerateFiles.Path()
return CompatibilityFiles.OrphanPath(*self._path, other)

@property
def name(self):
return ''
return self._path[-1]

def open(self):
raise ValueError()
def open(self, mode='r', *args, **kwargs):
raise FileNotFoundError("Can't open orphan path")

def __init__(self, spec):
self.spec = spec
Expand All @@ -71,7 +159,7 @@ def __getattr__(self, attr):
return getattr(self._reader, attr)

def files(self):
return DegenerateFiles.Path()
return CompatibilityFiles.SpecPath(self.spec, self._reader)


def wrap_spec(package):
Expand Down
3 changes: 2 additions & 1 deletion importlib_resources/_common.py
Expand Up @@ -12,6 +12,7 @@
from ._compat import wrap_spec

Package = Union[types.ModuleType, str]
Resource = Union[str, os.PathLike]


def files(package):
Expand Down Expand Up @@ -93,7 +94,7 @@ def _tempfile(reader, suffix=''):
finally:
try:
os.remove(raw_path)
except FileNotFoundError:
except (FileNotFoundError, PermissionError):
pass


Expand Down
9 changes: 7 additions & 2 deletions importlib_resources/_compat.py
Expand Up @@ -61,7 +61,11 @@ def _native_reader(spec):
return reader if hasattr(reader, 'files') else None

def _file_reader(spec):
if pathlib.Path(self.path).exists():
try:
path = pathlib.Path(self.path)
except TypeError:
return None
if path.exists():
return readers.FileReader(self)

return (
Expand All @@ -76,7 +80,8 @@ def _file_reader(spec):
or
# local FileReader
_file_reader(self.spec)
or _adapters.DegenerateFiles(self.spec)
# fallback - adapt the spec ResourceReader to TraversableReader
or _adapters.CompatibilityFiles(self.spec)
)


Expand Down
85 changes: 85 additions & 0 deletions importlib_resources/_legacy.py
@@ -0,0 +1,85 @@
import os
import pathlib
import types

from typing import Union, Iterable, ContextManager
from typing.io import BinaryIO, TextIO

from . import _common

Package = Union[types.ModuleType, str]
Resource = Union[str, os.PathLike]


def open_binary(package: Package, resource: Resource) -> BinaryIO:
"""Return a file-like object opened for binary reading of the resource."""
return (_common.files(package) / _common.normalize_path(resource)).open('rb')


def read_binary(package: Package, resource: Resource) -> bytes:
"""Return the binary contents of the resource."""
return (_common.files(package) / _common.normalize_path(resource)).read_bytes()


def open_text(
package: Package,
resource: Resource,
encoding: str = 'utf-8',
errors: str = 'strict',
) -> TextIO:
"""Return a file-like object opened for text reading of the resource."""
return (_common.files(package) / _common.normalize_path(resource)).open(
'r', encoding=encoding, errors=errors
)


def read_text(
package: Package,
resource: Resource,
encoding: str = 'utf-8',
errors: str = 'strict',
) -> str:
"""Return the decoded string of the resource.
The decoding-related arguments have the same semantics as those of
bytes.decode().
"""
with open_text(package, resource, encoding, errors) as fp:
return fp.read()


def contents(package: Package) -> Iterable[str]:
"""Return an iterable of entries in `package`.
Note that not all entries are resources. Specifically, directories are
not considered resources. Use `is_resource()` on each entry returned here
to check if it is a resource or not.
"""
return [path.name for path in _common.files(package).iterdir()]


def is_resource(package: Package, name: str) -> bool:
"""True if `name` is a resource inside `package`.
Directories are *not* resources.
"""
resource = _common.normalize_path(name)
return any(
traversable.name == resource and traversable.is_file()
for traversable in _common.files(package).iterdir()
)


def path(
package: Package,
resource: Resource,
) -> ContextManager[pathlib.Path]:
"""A context manager providing a file path object to the resource.
If the resource does not already exist on its own on the file system,
a temporary file will be created. If the file was created, the file
will be deleted upon exiting the context manager (no exception is
raised if the file was deleted prior to the context manager
exiting).
"""
return _common.as_file(_common.files(package) / _common.normalize_path(resource))

0 comments on commit 5b2a675

Please sign in to comment.