Skip to content

Commit

Permalink
Apply CPython PR, sans docs and changelogs
Browse files Browse the repository at this point in the history
  • Loading branch information
encukou committed Mar 21, 2024
1 parent 0db550c commit 189d15f
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 0 deletions.
17 changes: 17 additions & 0 deletions importlib_resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
Anchor,
)

from .functional import (
contents,
is_resource,
open_binary,
open_text,
path,
read_binary,
read_text,
)

from .abc import ResourceReader


Expand All @@ -16,4 +26,11 @@
'ResourceReader',
'as_file',
'files',
'contents',
'is_resource',
'open_binary',
'open_text',
'path',
'read_binary',
'read_text',
]
88 changes: 88 additions & 0 deletions importlib_resources/functional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Simplified function-based API for importlib.resources
"""

import os
import warnings

from ._common import files, as_file


_MISSING = object()

def open_binary(anchor, *path_names):
"""Open for binary reading the *resource* within *package*."""
return _get_resource(anchor, path_names).open('rb')


def open_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
"""Open for text reading the *resource* within *package*."""
encoding = _get_encoding_arg(path_names, encoding)
resource = _get_resource(anchor, path_names)
return resource.open('r', encoding=encoding, errors=errors)


def read_binary(anchor, *path_names):
"""Read and return contents of *resource* within *package* as bytes."""
return _get_resource(anchor, path_names).read_bytes()


def read_text(anchor, *path_names, encoding=_MISSING, errors='strict'):
"""Read and return contents of *resource* within *package* as str."""
encoding = _get_encoding_arg(path_names, encoding)
resource = _get_resource(anchor, path_names)
return resource.read_text(encoding=encoding, errors=errors)


def path(anchor, *path_names):
"""Return the path to the *resource* as an actual file system path."""
return as_file(_get_resource(anchor, path_names))


def is_resource(anchor, *path_names):
"""Return ``True`` if there is a resource named *name* in the package,
Otherwise returns ``False``.
"""
return _get_resource(anchor, path_names).is_file()


def contents(anchor, *path_names):
"""Return an iterable over the named resources within the package.
The iterable returns :class:`str` resources (e.g. files).
The iterable does not recurse into subdirectories.
"""
warnings.warn(
"importlib.resources.contents is deprecated. "
"Use files(anchor).iterdir() instead.",
DeprecationWarning,
stacklevel=1,
)
return (
resource.name
for resource
in _get_resource(anchor, path_names).iterdir()
)


def _get_encoding_arg(path_names, encoding):
# For compatibility with versions where *encoding* was a positional
# argument, it needs to be given explicitly when there are multiple
# *path_names*.
# This limitation can be removed in Python 3.15.
if encoding is _MISSING:
if len(path_names) > 1:
raise TypeError(
"'encoding' argument required with multiple path names",
)
else:
return 'utf-8'
return encoding


def _get_resource(anchor, path_names):
if anchor is None:
raise TypeError("anchor must be module or string, got None")
return files(anchor).joinpath(*path_names)
217 changes: 217 additions & 0 deletions importlib_resources/tests/test_functional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import unittest
import os

from test.support.warnings_helper import ignore_warnings, check_warnings

import importlib.resources

# Since the functional API forwards to Traversable, we only test
# filesystem resources here -- not zip files, namespace packages etc.
# We do test for two kinds of Anchor, though.


class StringAnchorMixin:
anchor01 = 'test.test_importlib.resources.data01'
anchor02 = 'test.test_importlib.resources.data02'


class ModuleAnchorMixin:
from test.test_importlib.resources import data01 as anchor01
from test.test_importlib.resources import data02 as anchor02


class FunctionalAPIBase():
def _gen_resourcetxt_path_parts(self):
"""Yield various names of a text file in anchor02, each in a subTest
"""
for path_parts in (
('subdirectory', 'subsubdir', 'resource.txt'),
('subdirectory/subsubdir/resource.txt',),
('subdirectory/subsubdir', 'resource.txt'),
):
with self.subTest(path_parts=path_parts):
yield path_parts

def test_read_text(self):
self.assertEqual(
importlib.resources.read_text(self.anchor01, 'utf-8.file'),
'Hello, UTF-8 world!\n',
)
self.assertEqual(
importlib.resources.read_text(
self.anchor02, 'subdirectory', 'subsubdir', 'resource.txt',
encoding='utf-8',
),
'a resource',
)
for path_parts in self._gen_resourcetxt_path_parts():
self.assertEqual(
importlib.resources.read_text(
self.anchor02, *path_parts, encoding='utf-8',
),
'a resource',
)
# Use generic OSError, since e.g. attempting to read a directory can
# fail with PermissionError rather than IsADirectoryError
with self.assertRaises(OSError):
importlib.resources.read_text(self.anchor01)
with self.assertRaises(OSError):
importlib.resources.read_text(self.anchor01, 'no-such-file')
with self.assertRaises(UnicodeDecodeError):
importlib.resources.read_text(self.anchor01, 'utf-16.file')
self.assertEqual(
importlib.resources.read_text(
self.anchor01, 'binary.file', encoding='latin1',
),
'\x00\x01\x02\x03',
)
self.assertEqual(
importlib.resources.read_text(
self.anchor01, 'utf-16.file',
errors='backslashreplace',
),
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
errors='backslashreplace',
)
)

def test_read_binary(self):
self.assertEqual(
importlib.resources.read_binary(self.anchor01, 'utf-8.file'),
b'Hello, UTF-8 world!\n',
)
for path_parts in self._gen_resourcetxt_path_parts():
self.assertEqual(
importlib.resources.read_binary(self.anchor02, *path_parts),
b'a resource',
)

def test_open_text(self):
with importlib.resources.open_text(self.anchor01, 'utf-8.file') as f:
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
for path_parts in self._gen_resourcetxt_path_parts():
with importlib.resources.open_text(
self.anchor02, *path_parts,
encoding='utf-8',
) as f:
self.assertEqual(f.read(), 'a resource')
# Use generic OSError, since e.g. attempting to read a directory can
# fail with PermissionError rather than IsADirectoryError
with self.assertRaises(OSError):
importlib.resources.open_text(self.anchor01)
with self.assertRaises(OSError):
importlib.resources.open_text(self.anchor01, 'no-such-file')
with importlib.resources.open_text(self.anchor01, 'utf-16.file') as f:
with self.assertRaises(UnicodeDecodeError):
f.read()
with importlib.resources.open_text(
self.anchor01, 'binary.file', encoding='latin1',
) as f:
self.assertEqual(f.read(), '\x00\x01\x02\x03')
with importlib.resources.open_text(
self.anchor01, 'utf-16.file',
errors='backslashreplace',
) as f:
self.assertEqual(
f.read(),
'Hello, UTF-16 world!\n'.encode('utf-16').decode(
errors='backslashreplace',
)
)

def test_open_binary(self):
with importlib.resources.open_binary(self.anchor01, 'utf-8.file') as f:
self.assertEqual(f.read(), b'Hello, UTF-8 world!\n')
for path_parts in self._gen_resourcetxt_path_parts():
with importlib.resources.open_binary(
self.anchor02, *path_parts,
) as f:
self.assertEqual(f.read(), b'a resource')

def test_path(self):
with importlib.resources.path(self.anchor01, 'utf-8.file') as path:
with open(str(path)) as f:
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')
with importlib.resources.path(self.anchor01) as path:
with open(os.path.join(path, 'utf-8.file')) as f:
self.assertEqual(f.read(), 'Hello, UTF-8 world!\n')

def test_is_resource(self):
is_resource = importlib.resources.is_resource
self.assertTrue(is_resource(self.anchor01, 'utf-8.file'))
self.assertFalse(is_resource(self.anchor01, 'no_such_file'))
self.assertFalse(is_resource(self.anchor01))
self.assertFalse(is_resource(self.anchor01, 'subdirectory'))
for path_parts in self._gen_resourcetxt_path_parts():
self.assertTrue(is_resource(self.anchor02, *path_parts))

def test_contents(self):
is_resource = importlib.resources.is_resource
with check_warnings((".*contents.*", DeprecationWarning)):
c = importlib.resources.contents(self.anchor01)
self.assertGreaterEqual(
set(c),
{'utf-8.file', 'utf-16.file', 'binary.file', 'subdirectory'},
)
with (self.assertRaises(OSError),
check_warnings((".*contents.*", DeprecationWarning)),
):
importlib.resources.contents(self.anchor01, 'utf-8.file')
for path_parts in self._gen_resourcetxt_path_parts():
with (self.assertRaises(OSError),
check_warnings((".*contents.*", DeprecationWarning)),
):
importlib.resources.contents(self.anchor01, *path_parts)
with check_warnings((".*contents.*", DeprecationWarning)):
c = importlib.resources.contents(self.anchor01, 'subdirectory')
self.assertGreaterEqual(
set(c),
{'binary.file'},
)

@ignore_warnings(category=DeprecationWarning)
def test_common_errors(self):
for func in (
importlib.resources.read_text,
importlib.resources.read_binary,
importlib.resources.open_text,
importlib.resources.open_binary,
importlib.resources.path,
importlib.resources.is_resource,
importlib.resources.contents,
):
with self.subTest(func=func):
# Rejecting None anchor
with self.assertRaises(TypeError):
func(None)
# Rejecting invalid anchor type
with self.assertRaises((TypeError, AttributeError)):
func(1234)
# Unknown module
with self.assertRaises(ModuleNotFoundError):
func('$missing module$')

def test_text_errors(self):
for func in (
importlib.resources.read_text,
importlib.resources.open_text,
):
with self.subTest(func=func):
# Multiple path arguments need explicit encoding argument.
with self.assertRaises(TypeError):
func(
self.anchor02, 'subdirectory',
'subsubdir', 'resource.txt',
)


class FunctionalAPITest_StringAnchor(
unittest.TestCase, FunctionalAPIBase, StringAnchorMixin,
):
pass


class FunctionalAPITest_ModuleAnchor(
unittest.TestCase, FunctionalAPIBase, ModuleAnchorMixin,
):
pass

0 comments on commit 189d15f

Please sign in to comment.