8000 gh-97930: Apply changes from importlib_resources 5.10. by jaraco · Pull Request #100598 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-97930: Apply changes from importlib_resources 5.10. #100598

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Apply changes from importlib_resources 5.10.
  • Loading branch information
jaraco committed Dec 29, 2022
commit 726f8dd9bbb57257a41afb5867b1903bc923c64d
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