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:def read_text(self, encoding: Optional[str] = None) -> str: """ Read contents of self as text """ with self.open(encoding=encoding) as strm: return strm.read() -
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 anerrorsparameter that doesn't exist in Traversable's default concrete method: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) -
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