Skip to content

Commit 5b2a675

Browse files
authored
Merge pull request #221 from FFY00/use-files-in-leacy-api
Replace legacy API implementation with files()
2 parents 7ff61a3 + e016e66 commit 5b2a675

File tree

9 files changed

+314
-192
lines changed

9 files changed

+314
-192
lines changed

importlib_resources/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@
33
from ._common import (
44
as_file,
55
files,
6-
)
7-
8-
from importlib_resources._py3 import (
96
Package,
107
Resource,
8+
)
9+
10+
from ._legacy import (
1111
contents,
12-
is_resource,
1312
open_binary,
14-
open_text,
15-
path,
1613
read_binary,
14+
open_text,
1715
read_text,
16+
is_resource,
17+
path,
1818
)
19+
1920
from importlib_resources.abc import ResourceReader
2021

2122

importlib_resources/_adapters.py

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from contextlib import suppress
2+
from io import TextIOWrapper
23

34
from . import abc
45

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

2728
def get_resource_reader(self, name):
28-
return DegenerateFiles(self.spec)._native()
29+
return CompatibilityFiles(self.spec)._native()
2930

3031

31-
class DegenerateFiles:
32+
def _io_wrapper(file, mode='r', *args, **kwargs):
33+
if mode == 'r':
34+
return TextIOWrapper(file, *args, **kwargs)
35+
elif mode == 'rb':
36+
return file
37+
raise ValueError(
38+
"Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode)
39+
)
40+
41+
42+
class CompatibilityFiles:
3243
"""
3344
Adapter for an existing or non-existant resource reader
34-
to provide a degenerate .files().
45+
to provide a compability .files().
3546
"""
3647

37-
class Path(abc.Traversable):
48+
class SpecPath(abc.Traversable):
49+
"""
50+
Path tied to a module spec.
51+
Can be read and exposes the resource reader children.
52+
"""
53+
54+
def __init__(self, spec, reader):
55+
self._spec = spec
56+
self._reader = reader
57+
58+
def iterdir(self):
59+
if not self._reader:
60+
return iter(())
61+
return iter(
62+
CompatibilityFiles.ChildPath(self._reader, path)
63+
for path in self._reader.contents()
64+
)
65+
66+
def is_file(self):
67+
return False
68+
69+
is_dir = is_file
70+
71+
def joinpath(self, other):
72+
if not self._reader:
73+
return CompatibilityFiles.OrphanPath(other)
74+
return CompatibilityFiles.ChildPath(self._reader, other)
75+
76+
@property
77+
def name(self):
78+
return self._spec.name
79+
80+
def open(self, mode='r', *args, **kwargs):
81+
return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs)
82+
83+
class ChildPath(abc.Traversable):
84+
"""
85+
Path tied to a resource reader child.
86+
Can be read but doesn't expose any meaningfull children.
87+
"""
88+
89+
def __init__(self, reader, name):
90+
self._reader = reader
91+
self._name = name
92+
3893
def iterdir(self):
3994
return iter(())
4095

96+
def is_file(self):
97+
return self._reader.is_resource(self.name)
98+
4199
def is_dir(self):
100+
return not self.is_file()
101+
102+
def joinpath(self, other):
103+
return CompatibilityFiles.OrphanPath(self.name, other)
104+
105+
@property
106+
def name(self):
107+
return self._name
108+
109+
def open(self, mode='r', *args, **kwargs):
110+
return _io_wrapper(
111+
self._reader.open_resource(self.name), mode, *args, **kwargs
112+
)
113+
114+
class OrphanPath(abc.Traversable):
115+
"""
116+
Orphan path, not tied to a module spec or resource reader.
117+
Can't be read and doesn't expose any meaningful children.
118+
"""
119+
120+
def __init__(self, *path_parts):
121+
if len(path_parts) < 1:
122+
raise ValueError('Need at least one path part to construct a path')
123+
self._path = path_parts
124+
125+
def iterdir(self):
126+
return iter(())
127+
128+
def is_file(self):
42129
return False
43130

44-
is_file = exists = is_dir # type: ignore
131+
is_dir = is_file
45132

46133
def joinpath(self, other):
47-
return DegenerateFiles.Path()
134+
return CompatibilityFiles.OrphanPath(*self._path, other)
48135

136+
@property
49137
def name(self):
50-
return ''
138+
return self._path[-1]
51139

52-
def open(self):
53-
raise ValueError()
140+
def open(self, mode='r', *args, **kwargs):
141+
raise FileNotFoundError("Can't open orphan path")
54142

55143
def __init__(self, spec):
56144
self.spec = spec
@@ -71,7 +159,7 @@ def __getattr__(self, attr):
71159
return getattr(self._reader, attr)
72160

73161
def files(self):
74-
return DegenerateFiles.Path()
162+
return CompatibilityFiles.SpecPath(self.spec, self._reader)
75163

76164

77165
def wrap_spec(package):

importlib_resources/_common.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ._compat import wrap_spec
1313

1414
Package = Union[types.ModuleType, str]
15+
Resource = Union[str, os.PathLike]
1516

1617

1718
def files(package):
@@ -93,7 +94,7 @@ def _tempfile(reader, suffix=''):
9394
finally:
9495
try:
9596
os.remove(raw_path)
96-
except FileNotFoundError:
97+
except (FileNotFoundError, PermissionError):
9798
pass
9899

99100

importlib_resources/_compat.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ def _native_reader(spec):
6161
return reader if hasattr(reader, 'files') else None
6262

6363
def _file_reader(spec):
64-
if pathlib.Path(self.path).exists():
64+
try:
65+
path = pathlib.Path(self.path)
66+
except TypeError:
67+
return None
68+
if path.exists():
6569
return readers.FileReader(self)
6670

6771
return (
@@ -76,7 +80,8 @@ def _file_reader(spec):
7680
or
7781
# local FileReader
7882
_file_reader(self.spec)
79-
or _adapters.DegenerateFiles(self.spec)
83+
# fallback - adapt the spec ResourceReader to TraversableReader
84+
or _adapters.CompatibilityFiles(self.spec)
8085
)
8186

8287

importlib_resources/_legacy.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
import pathlib
3+
import types
4+
5+
from typing import Union, Iterable, ContextManager
6+
from typing.io import BinaryIO, TextIO
7+
8+
from . import _common
9+
10+
Package = Union[types.ModuleType, str]
11+
Resource = Union[str, os.PathLike]
12+
13+
14+
def open_binary(package: Package, resource: Resource) -> BinaryIO:
15+
"""Return a file-like object opened for binary reading of the resource."""
16+
return (_common.files(package) / _common.normalize_path(resource)).open('rb')
17+
18+
19+
def read_binary(package: Package, resource: Resource) -> bytes:
20+
"""Return the binary contents of the resource."""
21+
return (_common.files(package) / _common.normalize_path(resource)).read_bytes()
22+
23+
24+
def open_text(
25+
package: Package,
26+
resource: Resource,
27+
encoding: str = 'utf-8',
28+
errors: str = 'strict',
29+
) -> TextIO:
30+
"""Return a file-like object opened for text reading of the resource."""
31+
return (_common.files(package) / _common.normalize_path(resource)).open(
32+
'r', encoding=encoding, errors=errors
33+
)
34+
35+
36+
def read_text(
37+
package: Package,
38+
resource: Resource,
39+
encoding: str = 'utf-8',
40+
errors: str = 'strict',
41+
) -> str:
42+
"""Return the decoded string of the resource.
43+
44+
The decoding-related arguments have the same semantics as those of
45+
bytes.decode().
46+
"""
47+
with open_text(package, resource, encoding, errors) as fp:
48+
return fp.read()
49+
50+
51+
def contents(package: Package) -> Iterable[str]:
52+
"""Return an iterable of entries in `package`.
53+
54+
Note that not all entries are resources. Specifically, directories are
55+
not considered resources. Use `is_resource()` on each entry returned here
56+
to check if it is a resource or not.
57+
"""
58+
return [path.name for path in _common.files(package).iterdir()]
59+
60+
61+
def is_resource(package: Package, name: str) -> bool:
62+
"""True if `name` is a resource inside `package`.
63+
64+
Directories are *not* resources.
65+
"""
66+
resource = _common.normalize_path(name)
67+
return any(
68+
traversable.name == resource and traversable.is_file()
69+
for traversable in _common.files(package).iterdir()
70+
)
71+
72+
73+
def path(
74+
package: Package,
75+
resource: Resource,
76+
) -> ContextManager[pathlib.Path]:
77+
"""A context manager providing a file path object to the resource.
78+
79+
If the resource does not already exist on its own on the file system,
80+
a temporary file will be created. If the file was created, the file
81+
will be deleted upon exiting the context manager (no exception is
82+
raised if the file was deleted prior to the context manager
83+
exiting).
84+
"""
85+
return _common.as_file(_common.files(package) / _common.normalize_path(resource))

0 commit comments

Comments
 (0)