Skip to content

Commit

Permalink
Apply changes from importlib_resources 5.10.
Browse files Browse the repository at this point in the history
  • Loading branch information
jaraco committed Dec 29, 2022
1 parent f10f503 commit a2524ba
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 72 deletions.
2 changes: 1 addition & 1 deletion Lib/importlib/resources/_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def _io_wrapper(file, mode='r', *args, **kwargs):
elif mode == 'rb':
return file
raise ValueError(
f"Invalid mode value '{mode}', only 'r' and 'rb' are supported"
"Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode)
)


Expand Down
86 changes: 67 additions & 19 deletions Lib/importlib/resources/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,58 @@
import contextlib
import types
import importlib
import inspect
import warnings
import itertools

from typing import Union, Optional
from typing import Union, Optional, cast
from .abc import ResourceReader, Traversable

from ._adapters import wrap_spec

Package = Union[types.ModuleType, str]
Anchor = Package


def files(package):
# type: (Package) -> Traversable
def package_to_anchor(func):
"""
Get a Traversable resource from a package
Replace 'package' parameter as 'anchor' and warn about the change.
Other errors should fall through.
>>> files('a', 'b')
Traceback (most recent call last):
TypeError: files() takes from 0 to 1 positional arguments but 2 were given
"""
undefined = object()

@functools.wraps(func)
def wrapper(anchor=undefined, package=undefined):
if package is not undefined:
if anchor is not undefined:
return func(anchor, package)
warnings.warn(
"First parameter to files is renamed to 'anchor'",
DeprecationWarning,
stacklevel=2,
)
return func(package)
elif anchor is undefined:
return func()
return func(anchor)

return wrapper


@package_to_anchor
def files(anchor: Optional[Anchor] = None) -> Traversable:
"""
Get a Traversable resource for an anchor.
"""
return from_package(get_package(package))
return from_package(resolve(anchor))


def get_resource_reader(package):
# type: (types.ModuleType) -> Optional[ResourceReader]
def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]:
"""
Return the package's loader if it's a ResourceReader.
"""
Expand All @@ -39,24 +72,39 @@ def get_resource_reader(package):
return reader(spec.name) # type: ignore


def resolve(cand):
# type: (Package) -> types.ModuleType
return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand)
@functools.singledispatch
def resolve(cand: Optional[Anchor]) -> types.ModuleType:
return cast(types.ModuleType, cand)


@resolve.register
def _(cand: str) -> types.ModuleType:
return importlib.import_module(cand)


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

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

Raise an exception if the resolved module is not a package.
def _infer_caller():
"""
resolved = resolve(package)
if wrap_spec(resolved).submodule_search_locations is None:
raise TypeError(f'{package!r} is not a package')
return resolved
Walk the stack and find the frame of the first caller not in this module.
"""

def is_this_file(frame_info):
return frame_info.filename == __file__

def is_wrapper(frame_info):
return frame_info.function == 'wrapper'

not_this_file = itertools.filterfalse(is_this_file, inspect.stack())
# also exclude 'wrapper' due to singledispatch in the call stack
callers = itertools.filterfalse(is_wrapper, not_this_file)
return next(callers).frame


def from_package(package):
def from_package(package: types.ModuleType):
"""
Return a Traversable object for the given package.
Expand Down
3 changes: 1 addition & 2 deletions Lib/importlib/resources/_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ def wrapper(*args, **kwargs):
return wrapper


def normalize_path(path):
# type: (Any) -> str
def normalize_path(path: Any) -> str:
"""Normalize a path by ensuring it is a string.
If the resulting string contains path separators, an exception is raised.
Expand Down
3 changes: 2 additions & 1 deletion Lib/importlib/resources/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ def open(self, mode='r', *args, **kwargs):
accepted by io.TextIOWrapper.
"""

@abc.abstractproperty
@property
@abc.abstractmethod
def name(self) -> str:
"""
The base name of this object without any parent references.
Expand Down
65 changes: 30 additions & 35 deletions Lib/importlib/resources/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,28 @@ class SimpleReader(abc.ABC):
provider.
"""

@abc.abstractproperty
def package(self):
# type: () -> str
@property
@abc.abstractmethod
def package(self) -> str:
"""
The name of the package for which this reader loads resources.
"""

@abc.abstractmethod
def children(self):
# type: () -> List['SimpleReader']
def children(self) -> List['SimpleReader']:
"""
Obtain an iterable of SimpleReader for available
child containers (e.g. directories).
"""

@abc.abstractmethod
def resources(self):
# type: () -> List[str]
def resources(self) -> List[str]:
"""
Obtain available named resources for this virtual package.
"""

@abc.abstractmethod
def open_binary(self, resource):
# type: (str) -> BinaryIO
def open_binary(self, resource: str) -> BinaryIO:
"""
Obtain a File-like for a named resource.
"""
Expand All @@ -50,13 +47,35 @@ def name(self):
return self.package.split('.')[-1]


class ResourceContainer(Traversable):
"""
Traversable container for a package's resources via its reader.
"""

def __init__(self, reader: SimpleReader):
self.reader = reader

def is_dir(self):
return True

def is_file(self):
return False

def iterdir(self):
files = (ResourceHandle(self, name) for name in self.reader.resources)
dirs = map(ResourceContainer, self.reader.children())
return itertools.chain(files, dirs)

def open(self, *args, **kwargs):
raise IsADirectoryError()


class ResourceHandle(Traversable):
"""
Handle to a named resource in a ResourceReader.
"""

def __init__(self, parent, name):
# type: (ResourceContainer, str) -> None
def __init__(self, parent: ResourceContainer, name: str):
self.parent = parent
self.name = name # type: ignore

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


class ResourceContainer(Traversable):
"""
Traversable container for a package's resources via its reader.
"""

def __init__(self, reader):
# type: (SimpleReader) -> None
self.reader = reader

def is_dir(self):
return True

def is_file(self):
return False

def iterdir(self):
files = (ResourceHandle(self, name) for name in self.reader.resources)
dirs = map(ResourceContainer, self.reader.children())
return itertools.chain(files, dirs)

def open(self, *args, **kwargs):
raise IsADirectoryError()


class TraversableReader(TraversableResources, SimpleReader):
"""
A TraversableResources based on SimpleReader. Resource providers
Expand Down
50 changes: 50 additions & 0 deletions Lib/test/test_importlib/resources/_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pathlib
import functools


####
# from jaraco.path 3.4


def build(spec, prefix=pathlib.Path()):
"""
Build a set of files/directories, as described by the spec.
Each key represents a pathname, and the value represents
the content. Content may be a nested directory.
>>> spec = {
... 'README.txt': "A README file",
... "foo": {
... "__init__.py": "",
... "bar": {
... "__init__.py": "",
... },
... "baz.py": "# Some code",
... }
... }
>>> tmpdir = getfixture('tmpdir')
>>> build(spec, tmpdir)
"""
for name, contents in spec.items():
create(contents, pathlib.Path(prefix) / name)


@functools.singledispatch
def create(content, path):
path.mkdir(exist_ok=True)
build(content, prefix=path) # type: ignore


@create.register
def _(content: bytes, path):
path.write_bytes(content)


@create.register
def _(content: str, path):
path.write_text(content)


# end from jaraco.path
####
67 changes: 67 additions & 0 deletions Lib/test/test_importlib/resources/test_files.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import typing
import textwrap
import unittest
import warnings
import importlib
import contextlib

from importlib import resources
from importlib.resources.abc import Traversable
from . import data01
from . import util
from . import _path
from test.support import os_helper
from test.support import import_helper


@contextlib.contextmanager
def suppress_known_deprecation():
with warnings.catch_warnings(record=True) as ctx:
warnings.simplefilter('default', category=DeprecationWarning)
yield ctx


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

def test_old_parameter(self):
"""
Files used to take a 'package' parameter. Make sure anyone
passing by name is still supported.
"""
with suppress_known_deprecation():
resources.files(package=self.data)


class OpenDiskTests(FilesTests, unittest.TestCase):
def setUp(self):
Expand All @@ -42,5 +64,50 @@ def setUp(self):
self.data = namespacedata01


class SiteDir:
def setUp(self):
self.fixtures = contextlib.ExitStack()
self.addCleanup(self.fixtures.close)
self.site_dir = self.fixtures.enter_context(os_helper.temp_dir())
self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir))
self.fixtures.enter_context(import_helper.CleanImport())


class ModulesFilesTests(SiteDir, unittest.TestCase):
def test_module_resources(self):
"""
A module can have resources found adjacent to the module.
"""
spec = {
'mod.py': '',
'res.txt': 'resources are the best',
}
_path.build(spec, self.site_dir)
import mod

actual = resources.files(mod).joinpath('res.txt').read_text()
assert actual == spec['res.txt']


class ImplicitContextFilesTests(SiteDir, unittest.TestCase):
def test_implicit_files(self):
"""
Without any parameter, files() will infer the location as the caller.
"""
spec = {
'somepkg': {
'__init__.py': textwrap.dedent(
"""
import importlib.resources as res
val = res.files().joinpath('res.txt').read_text()
"""
),
'res.txt': 'resources are the best',
},
}
_path.build(spec, self.site_dir)
assert importlib.import_module('somepkg').val == 'resources are the best'


if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit a2524ba

Please sign in to comment.