Skip to content

Commit 362c36c

Browse files
committed
Apply changes from importlib_resources 5.10.
1 parent f10f503 commit 362c36c

File tree

7 files changed

+171
-72
lines changed

7 files changed

+171
-72
lines changed

Lib/importlib/resources/_adapters.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def _io_wrapper(file, mode='r', *args, **kwargs):
3535
elif mode == 'rb':
3636
return file
3737
raise ValueError(
38-
f"Invalid mode value '{mode}', only 'r' and 'rb' are supported"
38+
"Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode)
3939
)
4040

4141

Lib/importlib/resources/_common.py

+67-19
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,58 @@
55
import contextlib
66
import types
77
import importlib
8+
import inspect
9+
import warnings
10+
import itertools
811

9-
from typing import Union, Optional
12+
from typing import Union, Optional, cast
1013
from .abc import ResourceReader, Traversable
1114

1215
from ._adapters import wrap_spec
1316

1417
Package = Union[types.ModuleType, str]
18+
Anchor = Package
1519

1620

17-
def files(package):
18-
# type: (Package) -> Traversable
21+
def package_to_anchor(func):
1922
"""
20-
Get a Traversable resource from a package
23+
Replace 'package' parameter as 'anchor' and warn about the change.
24+
25+
Other errors should fall through.
26+
27+
>>> files('a', 'b')
28+
Traceback (most recent call last):
29+
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
30+
"""
31+
undefined = object()
32+
33+
@functools.wraps(func)
34+
def wrapper(anchor=undefined, package=undefined):
35+
if package is not undefined:
36+
if anchor is not undefined:
37+
return func(anchor, package)
38+
warnings.warn(
39+
"First parameter to files is renamed to 'anchor'",
40+
DeprecationWarning,
41+
stacklevel=2,
42+
)
43+
return func(package)
44+
elif anchor is undefined:
45+
return func()
46+
return func(anchor)
47+
48+
return wrapper
49+
50+
51+
@package_to_anchor
52+
def files(anchor: Optional[Anchor] = None) -> Traversable:
53+
"""
54+
Get a Traversable resource for an anchor.
2155
"""
22-
return from_package(get_package(package))
56+
return from_package(resolve(anchor))
2357

2458

25-
def get_resource_reader(package):
26-
# type: (types.ModuleType) -> Optional[ResourceReader]
59+
def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
2760
"""
2861
Return the package's loader if it's a ResourceReader.
2962
"""
@@ -39,24 +72,39 @@ def get_resource_reader(package):
3972
return reader(spec.name) # type: ignore
4073

4174

42-
def resolve(cand):
43-
# type: (Package) -> types.ModuleType
44-
return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand)
75+
@functools.singledispatch
76+
def resolve(cand: Optional[Anchor]) -> types.ModuleType:
77+
return cast(types.ModuleType, cand)
78+
79+
80+
@resolve.register
81+
def _(cand: str) -> types.ModuleType:
82+
return importlib.import_module(cand)
83+
4584

85+
@resolve.register
86+
def _(cand: None) -> types.ModuleType:
87+
return resolve(_infer_caller().f_globals['__name__'])
4688

47-
def get_package(package):
48-
# type: (Package) -> types.ModuleType
49-
"""Take a package name or module object and return the module.
5089

51-
Raise an exception if the resolved module is not a package.
90+
def _infer_caller():
5291
"""
53-
resolved = resolve(package)
54-
if wrap_spec(resolved).submodule_search_locations is None:
55-
raise TypeError(f'{package!r} is not a package')
56-
return resolved
92+
Walk the stack and find the frame of the first caller not in this module.
93+
"""
94+
95+
def is_this_file(frame_info):
96+
return frame_info.filename == __file__
97+
98+
def is_wrapper(frame_info):
99+
return frame_info.function == 'wrapper'
100+
101+
not_this_file = itertools.filterfalse(is_this_file, inspect.stack())
102+
# also exclude 'wrapper' due to singledispatch in the call stack
103+
callers = itertools.filterfalse(is_wrapper, not_this_file)
104+
return next(callers).frame
57105

58106

59-
def from_package(package):
107+
def from_package(package: types.ModuleType):
60108
"""
61109
Return a Traversable object for the given package.
62110

Lib/importlib/resources/_legacy.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ def wrapper(*args, **kwargs):
2727
return wrapper
2828

2929

30-
def normalize_path(path):
31-
# type: (Any) -> str
30+
def normalize_path(path: Any) -> str:
3231
"""Normalize a path by ensuring it is a string.
3332
3433
If the resulting string contains path separators, an exception is raised.

Lib/importlib/resources/abc.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ def open(self, mode='r', *args, **kwargs):
142142
accepted by io.TextIOWrapper.
143143
"""
144144

145-
@abc.abstractproperty
145+
@property
146+
@abc.abstractmethod
146147
def name(self) -> str:
147148
"""
148149
The base name of this object without any parent references.

Lib/importlib/resources/simple.py

+30-35
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,28 @@ class SimpleReader(abc.ABC):
1616
provider.
1717
"""
1818

19-
@abc.abstractproperty
20-
def package(self):
21-
# type: () -> str
19+
@property
20+
@abc.abstractmethod
21+
def package(self) -> str:
2222
"""
2323
The name of the package for which this reader loads resources.
2424
"""
2525

2626
@abc.abstractmethod
27-
def children(self):
28-
# type: () -> List['SimpleReader']
27+
def children(self) -> List['SimpleReader']:
2928
"""
3029
Obtain an iterable of SimpleReader for available
3130
child containers (e.g. directories).
3231
"""
3332

3433
@abc.abstractmethod
35-
def resources(self):
36-
# type: () -> List[str]
34+
def resources(self) -> List[str]:
3735
"""
3836
Obtain available named resources for this virtual package.
3937
"""
4038

4139
@abc.abstractmethod
42-
def open_binary(self, resource):
43-
# type: (str) -> BinaryIO
40+
def open_binary(self, resource: str) -> BinaryIO:
4441
"""
4542
Obtain a File-like for a named resource.
4643
"""
@@ -50,13 +47,35 @@ def name(self):
5047
return self.package.split('.')[-1]
5148

5249

50+
class ResourceContainer(Traversable):
51+
"""
52+
Traversable container for a package's resources via its reader.
53+
"""
54+
55+
def __init__(self, reader: SimpleReader):
56+
self.reader = reader
57+
58+
def is_dir(self):
59+
return True
60+
61+
def is_file(self):
62+
return False
63+
64+
def iterdir(self):
65+
files = (ResourceHandle(self, name) for name in self.reader.resources)
66+
dirs = map(ResourceContainer, self.reader.children())
67+
return itertools.chain(files, dirs)
68+
69+
def open(self, *args, **kwargs):
70+
raise IsADirectoryError()
71+
72+
5373
class ResourceHandle(Traversable):
5474
"""
5575
Handle to a named resource in a ResourceReader.
5676
"""
5777

58-
def __init__(self, parent, name):
59-
# type: (ResourceContainer, str) -> None
78+
def __init__(self, parent: ResourceContainer, name: str):
6079
self.parent = parent
6180
self.name = name # type: ignore
6281

@@ -76,30 +95,6 @@ def joinpath(self, name):
7695
raise RuntimeError("Cannot traverse into a resource")
7796

7897

79-
class ResourceContainer(Traversable):
80-
"""
81-
Traversable container for a package's resources via its reader.
82-
"""
83-
84-
def __init__(self, reader):
85-
# type: (SimpleReader) -> None
86-
self.reader = reader
87-
88-
def is_dir(self):
89-
return True
90-
91-
def is_file(self):
92-
return False
93-
94-
def iterdir(self):
95-
files = (ResourceHandle(self, name) for name in self.reader.resources)
96-
dirs = map(ResourceContainer, self.reader.children())
97-
return itertools.chain(files, dirs)
98-
99-
def open(self, *args, **kwargs):
100-
raise IsADirectoryError()
101-
102-
10398
class TraversableReader(TraversableResources, SimpleReader):
10499
"""
105100
A TraversableResources based on SimpleReader. Resource providers

Lib/test/test_importlib/resources/test_files.py

+67
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import typing
2+
import textwrap
23
import unittest
4+
import warnings
5+
import importlib
6+
import contextlib
37

48
from importlib import resources
59
from importlib.resources.abc import Traversable
610
from . import data01
711
from . import util
12+
from . import _path
13+
from test.support import os_helper
14+
from test.support import import_helper
15+
16+
17+
@contextlib.contextmanager
18+
def suppress_known_deprecation():
19+
with warnings.catch_warnings(record=True) as ctx:
20+
warnings.simplefilter('default', category=DeprecationWarning)
21+
yield ctx
822

923

1024
class FilesTests:
@@ -25,6 +39,14 @@ def test_read_text(self):
2539
def test_traversable(self):
2640
assert isinstance(resources.files(self.data), Traversable)
2741

42+
def test_old_parameter(self):
43+
"""
44+
Files used to take a 'package' parameter. Make sure anyone
45+
passing by name is still supported.
46+
"""
47+
with suppress_known_deprecation():
48+
resources.files(package=self.data)
49+
2850

2951
class OpenDiskTests(FilesTests, unittest.TestCase):
3052
def setUp(self):
@@ -42,5 +64,50 @@ def setUp(self):
4264
self.data = namespacedata01
4365

4466

67+
class SiteDir:
68+
def setUp(self):
69+
self.fixtures = contextlib.ExitStack()
70+
self.addCleanup(self.fixtures.close)
71+
self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())
72+
self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir))
73+
self.fixtures.enter_context(import_helper.CleanImport())
74+
75+
76+
class ModulesFilesTests(SiteDir, unittest.TestCase):
77+
def test_module_resources(self):
78+
"""
79+
A module can have resources found adjacent to the module.
80+
"""
81+
spec = {
82+
'mod.py': '',
83+
'res.txt': 'resources are the best',
84+
}
85+
_path.build(spec, self.site_dir)
86+
import mod
87+
88+
actual = resources.files(mod).joinpath('res.txt').read_text()
89+
assert actual == spec['res.txt']
90+
91+
92+
class ImplicitContextFilesTests(SiteDir, unittest.TestCase):
93+
def test_implicit_files(self):
94+
"""
95+
Without any parameter, files() will infer the location as the caller.
96+
"""
97+
spec = {
98+
'somepkg': {
99+
'__init__.py': textwrap.dedent(
100+
"""
101+
import importlib.resources as res
102+
val = res.files().joinpath('res.txt').read_text()
103+
"""
104+
),
105+
'res.txt': 'resources are the best',
106+
},
107+
}
108+
_path.build(spec, self.site_dir)
109+
assert importlib.import_module('somepkg').val == 'resources are the best'
110+
111+
45112
if __name__ == '__main__':
46113
unittest.main()

Lib/test/test_importlib/resources/util.py

+3-14
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import io
44
import sys
55
import types
6-
from pathlib import Path, PurePath
6+
import pathlib
77

88
from . import data01
99
from . import zipdata01
@@ -94,25 +94,14 @@ def test_string_path(self):
9494

9595
def test_pathlib_path(self):
9696
# Passing in a pathlib.PurePath object for the path should succeed.
97-
path = PurePath('utf-8.file')
97+
path = pathlib.PurePath('utf-8.file')
9898
self.execute(data01, path)
9999

100100
def test_importing_module_as_side_effect(self):
101101
# The anchor package can already be imported.
102102
del sys.modules[data01.__name__]
103103
self.execute(data01.__name__, 'utf-8.file')
104104

105-
def test_non_package_by_name(self):
106-
# The anchor package cannot be a module.
107-
with self.assertRaises(TypeError):
108-
self.execute(__name__, 'utf-8.file')
109-
110-
def test_non_package_by_package(self):
111-
# The anchor package cannot be a module.
112-
with self.assertRaises(TypeError):
113-
module = sys.modules['test.test_importlib.resources.util']
114-
self.execute(module, 'utf-8.file')
115-
116105
def test_missing_path(self):
117106
# Attempting to open or read or request the path for a
118107
# non-existent path should succeed if open_resource
@@ -144,7 +133,7 @@ class ZipSetupBase:
144133

145134
@classmethod
146135
def setUpClass(cls):
147-
data_path = Path(cls.ZIP_MODULE.__file__)
136+
data_path = pathlib.Path(cls.ZIP_MODULE.__file__)
148137
data_dir = data_path.parent
149138
cls._zip_path = str(data_dir / 'ziptestdata.zip')
150139
sys.path.append(cls._zip_path)

0 commit comments

Comments
 (0)