Skip to content

Commit

Permalink
bpo-44771: Apply changes from importlib_resources 5.2.1 (pythonGH-27436)
Browse files Browse the repository at this point in the history
* bpo-44771: Apply changes from importlib_resources@3b24bd6307

* Add blurb

* Exclude namespacedata01 from eol conversion.
  • Loading branch information
jaraco committed Jul 30, 2021
1 parent 851cca8 commit aaa83cd
Show file tree
Hide file tree
Showing 19 changed files with 714 additions and 373 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Lib/test/test_email/data/*.txt -text
Lib/test/xmltestdata/* -text
Lib/test/coding20731.py -text
Lib/test/test_importlib/data01/* -text
Lib/test/test_importlib/namespacedata01/* -text

# CRLF files
*.bat text eol=crlf
Expand Down
108 changes: 98 additions & 10 deletions Lib/importlib/_adapters.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from contextlib import suppress
from io import TextIOWrapper

from . import abc

Expand All @@ -25,32 +26,119 @@ def __init__(self, spec):
self.spec = spec

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


class DegenerateFiles:
def _io_wrapper(file, mode='r', *args, **kwargs):
if mode == 'r':
return TextIOWrapper(file, *args, **kwargs)
elif mode == 'rb':
return file
raise ValueError(
"Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode)
)


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

class Path(abc.Traversable):
class SpecPath(abc.Traversable):
"""
Path tied to a module spec.
Can be read and exposes the resource reader children.
"""

def __init__(self, spec, reader):
self._spec = spec
self._reader = reader

def iterdir(self):
if not self._reader:
return iter(())
return iter(
CompatibilityFiles.ChildPath(self._reader, path)
for path in self._reader.contents()
)

def is_file(self):
return False

is_dir = is_file

def joinpath(self, other):
if not self._reader:
return CompatibilityFiles.OrphanPath(other)
return CompatibilityFiles.ChildPath(self._reader, other)

@property
def name(self):
return self._spec.name

def open(self, mode='r', *args, **kwargs):
return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs)

class ChildPath(abc.Traversable):
"""
Path tied to a resource reader child.
Can be read but doesn't expose any meaningfull children.
"""

def __init__(self, reader, name):
self._reader = reader
self._name = name

def iterdir(self):
return iter(())

def is_file(self):
return self._reader.is_resource(self.name)

def is_dir(self):
return not self.is_file()

def joinpath(self, other):
return CompatibilityFiles.OrphanPath(self.name, other)

@property
def name(self):
return self._name

def open(self, mode='r', *args, **kwargs):
return _io_wrapper(
self._reader.open_resource(self.name), mode, *args, **kwargs
)

class OrphanPath(abc.Traversable):
"""
Orphan path, not tied to a module spec or resource reader.
Can't be read and doesn't expose any meaningful children.
"""

def __init__(self, *path_parts):
if len(path_parts) < 1:
raise ValueError('Need at least one path part to construct a path')
self._path = path_parts

def iterdir(self):
return iter(())

def is_file(self):
return False

is_file = exists = is_dir # type: ignore
is_dir = is_file

def joinpath(self, other):
return DegenerateFiles.Path()
return CompatibilityFiles.OrphanPath(*self._path, other)

@property
def name(self):
return ''
return self._path[-1]

def open(self):
raise ValueError()
def open(self, mode='r', *args, **kwargs):
raise FileNotFoundError("Can't open orphan path")

def __init__(self, spec):
self.spec = spec
Expand All @@ -71,7 +159,7 @@ def __getattr__(self, attr):
return getattr(self._reader, attr)

def files(self):
return DegenerateFiles.Path()
return CompatibilityFiles.SpecPath(self.spec, self._reader)


def wrap_spec(package):
Expand Down
3 changes: 2 additions & 1 deletion Lib/importlib/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ._adapters import wrap_spec

Package = Union[types.ModuleType, str]
Resource = Union[str, os.PathLike]


def files(package):
Expand Down Expand Up @@ -93,7 +94,7 @@ def _tempfile(reader, suffix=''):
finally:
try:
os.remove(raw_path)
except FileNotFoundError:
except (FileNotFoundError, PermissionError):
pass


Expand Down
19 changes: 19 additions & 0 deletions Lib/importlib/_itertools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from itertools import filterfalse


def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in filterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
84 changes: 84 additions & 0 deletions Lib/importlib/_legacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import os
import pathlib
import types

from typing import Union, Iterable, ContextManager, BinaryIO, TextIO

from . import _common

Package = Union[types.ModuleType, str]
Resource = Union[str, os.PathLike]


def open_binary(package: Package, resource: Resource) -> BinaryIO:
"""Return a file-like object opened for binary reading of the resource."""
return (_common.files(package) / _common.normalize_path(resource)).open('rb')


def read_binary(package: Package, resource: Resource) -> bytes:
"""Return the binary contents of the resource."""
return (_common.files(package) / _common.normalize_path(resource)).read_bytes()


def open_text(
package: Package,
resource: Resource,
encoding: str = 'utf-8',
errors: str = 'strict',
) -> TextIO:
"""Return a file-like object opened for text reading of the resource."""
return (_common.files(package) / _common.normalize_path(resource)).open(
'r', encoding=encoding, errors=errors
)


def read_text(
package: Package,
resource: Resource,
encoding: str = 'utf-8',
errors: str = 'strict',
) -> str:
"""Return the decoded string of the resource.
The decoding-related arguments have the same semantics as those of
bytes.decode().
"""
with open_text(package, resource, encoding, errors) as fp:
return fp.read()


def contents(package: Package) -> Iterable[str]:
"""Return an iterable of entries in `package`.
Note that not all entries are resources. Specifically, directories are
not considered resources. Use `is_resource()` on each entry returned here
to check if it is a resource or not.
"""
return [path.name for path in _common.files(package).iterdir()]


def is_resource(package: Package, name: str) -> bool:
"""True if `name` is a resource inside `package`.
Directories are *not* resources.
"""
resource = _common.normalize_path(name)
return any(
traversable.name == resource and traversable.is_file()
for traversable in _common.files(package).iterdir()
)


def path(
package: Package,
resource: Resource,
) -> ContextManager[pathlib.Path]:
"""A context manager providing a file path object to the resource.
If the resource does not already exist on its own on the file system,
a temporary file will be created. If the file was created, the file
will be deleted upon exiting the context manager (no exception is
raised if the file was deleted prior to the context manager
exiting).
"""
return _common.as_file(_common.files(package) / _common.normalize_path(resource))
15 changes: 7 additions & 8 deletions Lib/importlib/readers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import collections
import zipfile
import operator
import pathlib
import zipfile

from . import abc

from ._itertools import unique_everseen


def remove_duplicates(items):
return iter(collections.OrderedDict.fromkeys(items))
Expand Down Expand Up @@ -63,13 +67,8 @@ def __init__(self, *paths):
raise NotADirectoryError('MultiplexedPath only supports directories')

def iterdir(self):
visited = []
for path in self._paths:
for file in path.iterdir():
if file.name in visited:
continue
visited.append(file.name)
yield file
files = (file for path in self._paths for file in path.iterdir())
return unique_everseen(files, key=operator.attrgetter('name'))

def read_bytes(self):
raise FileNotFoundError(f'{self} is not a file')
Expand Down
Loading

0 comments on commit aaa83cd

Please sign in to comment.