Skip to content

importlib.abc.Traversable.read_text() incompatible with importlib.resources._functional.read_text() usage (Python 3.13)#127012

@kurtmckee

Description

@kurtmckee

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.

  1. 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:

    defread_text(self, encoding: Optional[str] =None) ->str:
    """
    Read contents of self as text
    """
    withself.open(encoding=encoding) asstrm:
    returnstrm.read()

  2. Application code that attempts to read a package resource, like importlib.resources.read_text(module, "resource.txt") ultimately leads to a call to importlib.resources._functional.read_text(), which attempts to call the .read_text() method of a Traversable subclass, but includes an errors parameter that doesn't exist in Traversable's default concrete method:

    defread_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)
    returnresource.read_text(encoding=encoding, errors=errors)

  3. 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

importioimportsysimporttypingimportpathlibimporttypesimportimportlib.abcimportimportlib.machineryimportimportlib.metadataimportimportlib.resources.abcclassExampleFinder(importlib.abc.MetaPathFinder): deffind_spec( self, fullname: str, path: typing.Sequence[str] |None, target: types.ModuleType|None=None, ) ->importlib.machinery.ModuleSpec|None: iffullname!="demonstrate_error": returnNoneprint(f"ExampleFinder.find_spec('{fullname}')") spec=importlib.machinery.ModuleSpec( name=fullname, loader=ExampleLoader(), is_package=True, ) returnspecsys.meta_path.append(ExampleFinder()) classExampleLoader(importlib.abc.Loader): defexec_module(self, module: types.ModuleType) ->None: print(f"ExampleLoader.exec_module({module})") exec("", module.__dict__) defget_resource_reader(self, fullname: str) ->"ExampleTraversableResources": print(f"ExampleLoader.get_resource_reader('{fullname}')") returnExampleTraversableResources(fullname) classExampleTraversableResources(importlib.resources.abc.TraversableResources): def__init__(self, fullname: str) ->None: self.fullname=fullnamedeffiles(self) ->"ExampleTraversable": print("ExampleTraversableResources.files()") returnExampleTraversable(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.#classExampleTraversable(importlib.resources.abc.Traversable): def__init__(self, path: str): self._path=pathdefiterdir(self) ->typing.Iterator["ExampleTraversable"]: yieldExampleTraversable("resource.txt") defis_dir(self) ->bool: returnFalsedefis_file(self) ->bool: returnTruedefopen(self, mode='r', *args, **kwargs) ->typing.IO[typing.AnyStr]: returnio.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))@propertydefname(self) ->str: returnpathlib.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.#importdemonstrate_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

Metadata

Metadata

Labels

stdlibStandard Library Python modules in the Lib/ directorytopic-importlibtype-bugAn unexpected behavior, bug, or error

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions