Description
Bug report
Bug description:
I'm writing a custom importer and discovered that the function signature for importlib.abc.Traversable.read_text()
is incompatible with the usage in importlib.resources._functional.read_text()
, specifically on Python 3.13.
-
importlib.abc.Traversable.read_text()
is a concrete method; its implementation calls the.open()
method, which is an abstract method that must be implemented. The expectation is therefore that implementing.open()
in a Traversable subclass is sufficient for.read_text()
to work.Note below that the
.read_text()
method is not marked as abstract, and includes only one parameter:encoding
:cpython/Lib/importlib/resources/abc.py
Lines 84 to 90 in 30aeb00
-
Application code that attempts to read a package resource, like
importlib.resources.read_text(module, "resource.txt")
ultimately leads to a call toimportlib.resources._functional.read_text()
, which attempts to call the.read_text()
method of a Traversable subclass, but includes anerrors
parameter that doesn't exist in Traversable's default concrete method:cpython/Lib/importlib/resources/_functional.py
Lines 28 to 32 in 30aeb00
-
Consequently, it appears to be necessary for all Traversable subclasses to not only re-implement its concrete
.read_text()
method, but also to override its signature.
I think that the Traversable .read_text()
method signature, and the call site in importlib.resources._functional.read_text()
, need to align with each other.
I'd like to submit a PR for this! However, I would like confirmation that an errors
parameter should be added to the Traversable.read_text()
method.
Note that adding an errors
parameter was previously discussed in #88368.
Demonstration of TypeError bug
import io
import sys
import typing
import pathlib
import types
import importlib.abc
import importlib.machinery
import importlib.metadata
import importlib.resources.abc
class ExampleFinder(importlib.abc.MetaPathFinder):
def find_spec(
self,
fullname: str,
path: typing.Sequence[str] | None,
target: types.ModuleType | None = None,
) -> importlib.machinery.ModuleSpec | None:
if fullname != "demonstrate_error":
return None
print(f"ExampleFinder.find_spec('{fullname}')")
spec = importlib.machinery.ModuleSpec(
name=fullname,
loader=ExampleLoader(),
is_package=True,
)
return spec
sys.meta_path.append(ExampleFinder())
class ExampleLoader(importlib.abc.Loader):
def exec_module(self, module: types.ModuleType) -> None:
print(f"ExampleLoader.exec_module({module})")
exec("", module.__dict__)
def get_resource_reader(self, fullname: str) -> "ExampleTraversableResources":
print(f"ExampleLoader.get_resource_reader('{fullname}')")
return ExampleTraversableResources(fullname)
class ExampleTraversableResources(importlib.resources.abc.TraversableResources):
def __init__(self, fullname: str) -> None:
self.fullname = fullname
def files(self) -> "ExampleTraversable":
print("ExampleTraversableResources.files()")
return ExampleTraversable(self.fullname)
# ----------------------------------------------------------------------------
# ExampleTraversable implements all five of the Traversable abstract methods.
# Specifically, it is expected that implementing `.open()` will be sufficient,
# but this will not be the case.
#
class ExampleTraversable(importlib.resources.abc.Traversable):
def __init__(self, path: str):
self._path = path
def iterdir(self) -> typing.Iterator["ExampleTraversable"]:
yield ExampleTraversable("resource.txt")
def is_dir(self) -> bool:
return False
def is_file(self) -> bool:
return True
def open(self, mode='r', *args, **kwargs) -> typing.IO[typing.AnyStr]:
return io.StringIO("Nice! The call to .read_text() succeeded!")
# Uncomment this `.read_text()` method to make `.read_text()` calls work.
# It overrides the `Traversable.read_text()` signature.
#
# def read_text(self, encoding: str | None, errors: str | None) -> str:
# print(f"ExampleTraversable.read_text('{encoding}', '{errors}')")
# return str(super().read_text(encoding))
@property
def name(self) -> str:
return pathlib.PurePosixPath(self._path).name
# -------------------------------------------------------------------------------
# Everything above allows us to import this hard-coded module
# and demonstrate a TypeError lurking in the Traversable.read_text() signature.
#
import demonstrate_error
# The next line will raise a TypeError.
# `importlib/resources/_functional.py:read_text()` calls `Traversable.read_text()`
# with an `errors` argument that is not supported by the default concrete method.
print(importlib.resources.read_text(demonstrate_error, "resource.txt"))
CPython versions tested on:
3.13
Operating systems tested on:
Linux