Skip to content

Commit 4ff0b1b

Browse files
authored
Merge pull request #260 from python/feature/implicit-here
Allow a package to resolve its own resources simply
2 parents 19ca481 + d34eeac commit 4ff0b1b

File tree

3 files changed

+56
-7
lines changed

3 files changed

+56
-7
lines changed

CHANGES.rst

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ v5.10.0
1010
files was renamed from 'package' to 'anchor', with a
1111
compatibility shim for those passing by keyword.
1212

13+
* #259: ``files`` no longer requires the anchor to be
14+
specified and can infer the anchor from the caller's scope
15+
(defaults to the caller's module).
16+
1317
v5.9.0
1418
======
1519

importlib_resources/_common.py

+27-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import contextlib
66
import types
77
import importlib
8+
import inspect
89
import warnings
10+
import itertools
911

1012
from typing import Union, Optional, cast
1113
from .abc import ResourceReader, Traversable
@@ -22,12 +24,9 @@ def package_to_anchor(func):
2224
2325
Other errors should fall through.
2426
25-
>>> files()
26-
Traceback (most recent call last):
27-
TypeError: files() missing 1 required positional argument: 'anchor'
2827
>>> files('a', 'b')
2928
Traceback (most recent call last):
30-
TypeError: files() takes 1 positional argument but 2 were given
29+
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
3130
"""
3231
undefined = object()
3332

@@ -50,7 +49,7 @@ def wrapper(anchor=undefined, package=undefined):
5049

5150

5251
@package_to_anchor
53-
def files(anchor: Anchor) -> Traversable:
52+
def files(anchor: Optional[Anchor] = None) -> Traversable:
5453
"""
5554
Get a Traversable resource for an anchor.
5655
"""
@@ -74,7 +73,7 @@ def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
7473

7574

7675
@functools.singledispatch
77-
def resolve(cand: Anchor) -> types.ModuleType:
76+
def resolve(cand: Optional[Anchor]) -> types.ModuleType:
7877
return cast(types.ModuleType, cand)
7978

8079

@@ -83,6 +82,28 @@ def _(cand: str) -> types.ModuleType:
8382
return importlib.import_module(cand)
8483

8584

85+
@resolve.register
86+
def _(cand: None) -> types.ModuleType:
87+
return resolve(_infer_caller().f_globals['__name__'])
88+
89+
90+
def _infer_caller():
91+
"""
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
105+
106+
86107
def from_package(package: types.ModuleType):
87108
"""
88109
Return a Traversable object for the given package.

importlib_resources/tests/test_files.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import typing
2+
import textwrap
23
import unittest
34
import warnings
5+
import importlib
46
import contextlib
57

68
import importlib_resources as resources
@@ -61,14 +63,16 @@ def setUp(self):
6163
self.data = namespacedata01
6264

6365

64-
class ModulesFilesTests(unittest.TestCase):
66+
class SiteDir:
6567
def setUp(self):
6668
self.fixtures = contextlib.ExitStack()
6769
self.addCleanup(self.fixtures.close)
6870
self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())
6971
self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir))
7072
self.fixtures.enter_context(import_helper.CleanImport())
7173

74+
75+
class ModulesFilesTests(SiteDir, unittest.TestCase):
7276
def test_module_resources(self):
7377
"""
7478
A module can have resources found adjacent to the module.
@@ -84,5 +88,25 @@ def test_module_resources(self):
8488
assert actual == spec['res.txt']
8589

8690

91+
class ImplicitContextFilesTests(SiteDir, unittest.TestCase):
92+
def test_implicit_files(self):
93+
"""
94+
Without any parameter, files() will infer the location as the caller.
95+
"""
96+
spec = {
97+
'somepkg': {
98+
'__init__.py': textwrap.dedent(
99+
"""
100+
import importlib_resources as res
101+
val = res.files().joinpath('res.txt').read_text()
102+
"""
103+
),
104+
'res.txt': 'resources are the best',
105+
},
106+
}
107+
_path.build(spec, self.site_dir)
108+
assert importlib.import_module('somepkg').val == 'resources are the best'
109+
110+
87111
if __name__ == '__main__':
88112
unittest.main()

0 commit comments

Comments
 (0)