Skip to content
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
Prev Previous commit
Next Next commit
Apply changes from importlib_resources 5.0.3
  • Loading branch information
jaraco committed Feb 28, 2021
commit ecb7df1c0f8fa031da9c6bb93f8ccca40187bc75
82 changes: 82 additions & 0 deletions Lib/importlib/_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from contextlib import suppress

from . import abc


class SpecLoaderAdapter:
"""
Adapt a package spec to adapt the underlying loader.
"""

def __init__(self, spec, adapter=lambda spec: spec.loader):
self.spec = spec
self.loader = adapter(spec)

def __getattr__(self, name):
return getattr(self.spec, name)


class TraversableResourcesLoader:
"""
Adapt a loader to provide TraversableResources.
"""

def __init__(self, spec):
self.spec = spec

def get_resource_reader(self, name):
return DegenerateFiles(self.spec)._native()


class DegenerateFiles:
"""
Adapter for an existing or non-existant resource reader
to provide a degenerate .files().
"""

class Path(abc.Traversable):
def iterdir(self):
return iter(())

def is_dir(self):
return False

is_file = exists = is_dir # type: ignore

def joinpath(self, other):
return DegenerateFiles.Path()

def name(self):
return ''

def open(self):
raise ValueError()

def __init__(self, spec):
self.spec = spec

@property
def _reader(self):
with suppress(AttributeError):
return self.spec.loader.get_resource_reader(self.spec.name)

def _native(self):
"""
Return the native reader if it supports files().
"""
reader = self._reader
return reader if hasattr(reader, 'files') else self

def __getattr__(self, attr):
return getattr(self._reader, attr)

def files(self):
return DegenerateFiles.Path()


def wrap_spec(package):
"""
Construct a package spec with traversable compatibility
on the spec/loader/reader.
"""
return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)
6 changes: 4 additions & 2 deletions Lib/importlib/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from typing import Union, Any, Optional
from .abc import ResourceReader

from ._adapters import wrap_spec

Package = Union[types.ModuleType, str]


Expand Down Expand Up @@ -61,7 +63,7 @@ def get_package(package):
Raise an exception if the resolved module is not a package.
"""
resolved = resolve(package)
if resolved.__spec__.submodule_search_locations is None:
if wrap_spec(resolved).submodule_search_locations is None:
raise TypeError('{!r} is not a package'.format(package))
return resolved

Expand All @@ -71,7 +73,7 @@ def from_package(package):
Return a Traversable object for the given package.

"""
spec = package.__spec__
spec = wrap_spec(package)
reader = spec.loader.get_resource_reader(spec.name)
return reader.files()

Expand Down
19 changes: 13 additions & 6 deletions Lib/importlib/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,26 +352,28 @@ def iterdir(self):
Yield Traversable objects in self
"""

@abc.abstractmethod
def read_bytes(self):
"""
Read contents of self as bytes
"""
with self.open('rb') as strm:
return strm.read()

@abc.abstractmethod
def read_text(self, encoding=None):
"""
Read contents of self as bytes
Read contents of self as text
"""
with self.open(encoding=encoding) as strm:
return strm.read()

@abc.abstractmethod
def is_dir(self):
def is_dir(self) -> bool:
"""
Return True if self is a dir
"""

@abc.abstractmethod
def is_file(self):
def is_file(self) -> bool:
"""
Return True if self is a file
"""
Expand All @@ -382,11 +384,11 @@ def joinpath(self, child):
Return Traversable child in self
"""

@abc.abstractmethod
def __truediv__(self, child):
"""
Return Traversable child in self
"""
return self.joinpath(child)

@abc.abstractmethod
def open(self, mode='r', *args, **kwargs):
Expand All @@ -406,6 +408,11 @@ def name(self) -> str:


class TraversableResources(ResourceReader):
"""
The required interface for providing traversable
resources.
"""

@abc.abstractmethod
def files(self):
"""Return a Traversable object for the loaded package."""
Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_importlib/test_path.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io
import unittest

from importlib import resources
Expand Down Expand Up @@ -37,6 +38,17 @@ def test_natural_path(self):
assert 'data' in str(path)


class PathMemoryTests(PathTests, unittest.TestCase):
def setUp(self):
file = io.BytesIO(b'Hello, UTF-8 world!\n')
self.addCleanup(file.close)
self.data = util.create_package(
file=file, path=FileNotFoundError("package exists only in memory")
)
self.data.__spec__.origin = None
self.data.__spec__.has_location = False


class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase):
def test_remove_in_context_manager(self):
# It is not an error if the file that was temporarily stashed on the
Expand Down
53 changes: 53 additions & 0 deletions Lib/test/test_importlib/update-zips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
Generate the zip test data files.

Run to build the tests/zipdataNN/ziptestdata.zip files from
files in tests/dataNN.

Replaces the file with the working copy, but does commit anything
to the source repo.
"""

import contextlib
import os
import pathlib
import zipfile


def main():
"""
>>> from unittest import mock
>>> monkeypatch = getfixture('monkeypatch')
>>> monkeypatch.setattr(zipfile, 'ZipFile', mock.MagicMock())
>>> print(); main() # print workaround for bpo-32509
<BLANKLINE>
...data01... -> ziptestdata/...
...
...data02... -> ziptestdata/...
...
"""
suffixes = '01', '02'
tuple(map(generate, suffixes))


def generate(suffix):
root = pathlib.Path(__file__).parent.relative_to(os.getcwd())
zfpath = root / f'zipdata{suffix}/ziptestdata.zip'
with zipfile.ZipFile(zfpath, 'w') as zf:
for src, rel in walk(root / f'data{suffix}'):
dst = 'ziptestdata' / pathlib.PurePosixPath(rel.as_posix())
print(src, '->', dst)
zf.write(src, dst)


def walk(datapath):
for dirpath, dirnames, filenames in os.walk(datapath):
with contextlib.suppress(KeyError):
dirnames.remove('__pycache__')
for filename in filenames:
res = pathlib.Path(dirpath) / filename
rel = res.relative_to(datapath)
yield res, rel


__name__ == '__main__' and main()