Skip to content

Commit 2ff3dc3

Browse files
committed
Merge branch 'master' into feature/multiplexer
2 parents 4eb6788 + edac8ed commit 2ff3dc3

File tree

11 files changed

+258
-144
lines changed

11 files changed

+258
-144
lines changed

.gitlab-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ tests:
1515

1616
coverage:
1717
script:
18-
- tox -e py27-cov,py35-cov,py36-cov
18+
- tox -e py27-cov,py35-cov,py36-cov,py37-cov,py38-cov
1919
artifacts:
2020
paths:
2121
- coverage.xml
2222

2323
diffcov:
2424
script:
25-
- tox -e py27-diffcov,py35-diffcov,py36-diffcov
25+
- tox -e py27-diffcov,py35-diffcov,py36-diffcov,py37-diffcov,py38-diffcov
2626

2727
docs:
2828
script:

docs/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
importlib_resources NEWS
33
==========================
44

5+
v1.5.0
6+
======
7+
8+
* Traversable is now a Protocol instead of an Abstract Base
9+
Class (Python 2.7 and Python 3.8+).
10+
11+
* Traversable objects now require a ``.name`` property.
12+
513
v1.4.0
614
======
715
* #79: Temporary files created will now reflect the filename of

importlib_resources/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import sys
44

55
from ._compat import metadata
6-
from ._common import as_file
6+
from ._common import (
7+
as_file, files,
8+
)
79

810
# for compatibility. Ref #88
911
__import__('importlib_resources.trees')
@@ -30,7 +32,6 @@
3032
Package,
3133
Resource,
3234
contents,
33-
files,
3435
is_resource,
3536
open_binary,
3637
open_text,
@@ -42,7 +43,6 @@
4243
else:
4344
from importlib_resources._py2 import (
4445
contents,
45-
files,
4646
is_resource,
4747
open_binary,
4848
open_text,

importlib_resources/_common.py

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,85 @@
33
import os
44
import tempfile
55
import contextlib
6+
import types
7+
import importlib
68

79
from ._compat import (
8-
Path, package_spec, FileNotFoundError, ZipPath,
9-
singledispatch, suppress,
10+
Path, FileNotFoundError,
11+
singledispatch, package_spec,
1012
)
1113

14+
if False: # TYPE_CHECKING
15+
from typing import Union, Any, Optional
16+
from .abc import ResourceReader
17+
Package = Union[types.ModuleType, str]
1218

13-
def from_package(package):
19+
20+
def files(package):
1421
"""
15-
Return a Traversable object for the given package.
22+
Get a Traversable resource from a package
23+
"""
24+
return from_package(get_package(package))
25+
1626

27+
def normalize_path(path):
28+
# type: (Any) -> str
29+
"""Normalize a path by ensuring it is a string.
30+
31+
If the resulting string contains path separators, an exception is raised.
1732
"""
18-
spec = package_spec(package)
19-
return from_traversable_resources(spec) or fallback_resources(spec)
33+
str_path = str(path)
34+
parent, file_name = os.path.split(str_path)
35+
if parent:
36+
raise ValueError('{!r} must be only a file name'.format(path))
37+
return file_name
2038

2139

22-
def from_traversable_resources(spec):
40+
def get_resource_reader(package):
41+
# type: (types.ModuleType) -> Optional[ResourceReader]
2342
"""
24-
If the spec.loader implements TraversableResources,
25-
directly or implicitly, it will have a ``files()`` method.
43+
Return the package's loader if it's a ResourceReader.
2644
"""
27-
with suppress(AttributeError):
28-
return spec.loader.files()
45+
# We can't use
46+
# a issubclass() check here because apparently abc.'s __subclasscheck__()
47+
# hook wants to create a weak reference to the object, but
48+
# zipimport.zipimporter does not support weak references, resulting in a
49+
# TypeError. That seems terrible.
50+
spec = package.__spec__
51+
reader = getattr(spec.loader, 'get_resource_reader', None)
52+
if reader is None:
53+
return None
54+
return reader(spec.name)
2955

3056

31-
def fallback_resources(spec):
32-
package_directory = Path(spec.origin).parent
33-
try:
34-
archive_path = spec.loader.archive
35-
rel_path = package_directory.relative_to(archive_path)
36-
return ZipPath(archive_path, str(rel_path) + '/')
37-
except Exception:
38-
pass
39-
return package_directory
57+
def resolve(cand):
58+
# type: (Package) -> types.ModuleType
59+
return (
60+
cand if isinstance(cand, types.ModuleType)
61+
else importlib.import_module(cand)
62+
)
63+
64+
65+
def get_package(package):
66+
# type: (Package) -> types.ModuleType
67+
"""Take a package name or module object and return the module.
68+
69+
Raise an exception if the resolved module is not a package.
70+
"""
71+
resolved = resolve(package)
72+
if package_spec(resolved).submodule_search_locations is None:
73+
raise TypeError('{!r} is not a package'.format(package))
74+
return resolved
75+
76+
77+
def from_package(package):
78+
"""
79+
Return a Traversable object for the given package.
80+
81+
"""
82+
spec = package_spec(package)
83+
reader = spec.loader.get_resource_reader(spec.name)
84+
return reader.files()
4085

4186

4287
@contextlib.contextmanager

importlib_resources/_compat.py

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,86 @@ class ABC(object): # type: ignore
4747
from zipp import Path as ZipPath # type: ignore
4848

4949

50-
class PackageSpec(object):
51-
def __init__(self, **kwargs):
52-
vars(self).update(kwargs)
50+
try:
51+
from typing import runtime_checkable # type: ignore
52+
except ImportError:
53+
def runtime_checkable(cls): # type: ignore
54+
return cls
55+
56+
57+
try:
58+
from typing import Protocol # type: ignore
59+
except ImportError:
60+
Protocol = ABC # type: ignore
61+
62+
63+
__metaclass__ = type
64+
65+
66+
class PackageSpec:
67+
def __init__(self, **kwargs):
68+
vars(self).update(kwargs)
69+
70+
71+
class TraversableResourcesAdapter:
72+
def __init__(self, spec):
73+
self.spec = spec
74+
self.loader = LoaderAdapter(spec)
75+
76+
def __getattr__(self, name):
77+
return getattr(self.spec, name)
78+
79+
80+
class LoaderAdapter:
81+
"""
82+
Adapt loaders to provide TraversableResources and other
83+
compatibility.
84+
"""
85+
def __init__(self, spec):
86+
self.spec = spec
87+
88+
@property
89+
def path(self):
90+
# Python < 3
91+
return self.spec.origin
92+
93+
def get_resource_reader(self, name):
94+
# Python < 3.9
95+
from . import readers
96+
97+
def _zip_reader(spec):
98+
with suppress(AttributeError):
99+
return readers.ZipReader(spec.loader, spec.name)
100+
101+
def _available_reader(spec):
102+
with suppress(AttributeError):
103+
return spec.loader.get_resource_reader(spec.name)
104+
105+
def _native_reader(spec):
106+
reader = _available_reader(spec)
107+
return reader if hasattr(reader, 'files') else None
108+
109+
return (
110+
# native reader if it supplies 'files'
111+
_native_reader(self.spec) or
112+
# local ZipReader if a zip module
113+
_zip_reader(self.spec) or
114+
# local FileReader
115+
readers.FileReader(self)
116+
)
53117

54118

55119
def package_spec(package):
56-
return getattr(package, '__spec__', None) or \
57-
PackageSpec(
58-
origin=package.__file__,
59-
loader=getattr(package, '__loader__', None),
60-
)
120+
"""
121+
Construct a minimal package spec suitable for
122+
matching the interfaces this library relies upon
123+
in later Python versions.
124+
"""
125+
spec = getattr(package, '__spec__', None) or \
126+
PackageSpec(
127+
origin=package.__file__,
128+
loader=getattr(package, '__loader__', None),
129+
name=package.__name__,
130+
submodule_search_locations=getattr(package, '__path__', None),
131+
)
132+
return TraversableResourcesAdapter(spec)

importlib_resources/_py2.py

Lines changed: 6 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,13 @@
33

44
from . import _common
55
from ._compat import FileNotFoundError
6-
from importlib import import_module
76
from io import BytesIO, TextIOWrapper, open as io_open
87

98

10-
def _resolve(name):
11-
"""If name is a string, resolve to a module."""
12-
if not isinstance(name, basestring): # noqa: F821
13-
return name
14-
return import_module(name)
15-
16-
17-
def _get_package(package):
18-
"""Normalize a path by ensuring it is a string.
19-
20-
If the resulting string contains path separators, an exception is raised.
21-
"""
22-
module = _resolve(package)
23-
if not hasattr(module, '__path__'):
24-
raise TypeError("{!r} is not a package".format(package))
25-
return module
26-
27-
28-
def _normalize_path(path):
29-
"""Normalize a path by ensuring it is a string.
30-
31-
If the resulting string contains path separators, an exception is raised.
32-
"""
33-
str_path = str(path)
34-
parent, file_name = os.path.split(str_path)
35-
if parent:
36-
raise ValueError("{!r} must be only a file name".format(path))
37-
return file_name
38-
39-
409
def open_binary(package, resource):
4110
"""Return a file-like object opened for binary reading of the resource."""
42-
resource = _normalize_path(resource)
43-
package = _get_package(package)
11+
resource = _common.normalize_path(resource)
12+
package = _common.get_package(package)
4413
# Using pathlib doesn't work well here due to the lack of 'strict' argument
4514
# for pathlib.Path.resolve() prior to Python 3.6.
4615
package_path = os.path.dirname(package.__file__)
@@ -89,10 +58,6 @@ def read_text(package, resource, encoding='utf-8', errors='strict'):
8958
return fp.read()
9059

9160

92-
def files(package):
93-
return _common.from_package(_get_package(package))
94-
95-
9661
def path(package, resource):
9762
"""A context manager providing a file path object to the resource.
9863
@@ -102,7 +67,7 @@ def path(package, resource):
10267
raised if the file was deleted prior to the context manager
10368
exiting).
10469
"""
105-
path = files(package).joinpath(_normalize_path(resource))
70+
path = _common.files(package).joinpath(_common.normalize_path(resource))
10671
if not path.is_file():
10772
raise FileNotFoundError(path)
10873
return _common.as_file(path)
@@ -113,8 +78,8 @@ def is_resource(package, name):
11378
11479
Directories are *not* resources.
11580
"""
116-
package = _get_package(package)
117-
_normalize_path(name)
81+
package = _common.get_package(package)
82+
_common.normalize_path(name)
11883
try:
11984
package_contents = set(contents(package))
12085
except OSError as error:
@@ -138,5 +103,5 @@ def contents(package):
138103
not considered resources. Use `is_resource()` on each entry returned here
139104
to check if it is a resource or not.
140105
"""
141-
package = _get_package(package)
106+
package = _common.get_package(package)
142107
return list(item.name for item in _common.from_package(package).iterdir())

0 commit comments

Comments
 (0)