mkrieger1
mkrieger1

Reputation: 23246

How to write a file-like class that satisfies typing.TextIO?

When writing a class that implements a file-like interface we can inherit one of the abstract base classes from the io module, for example TextIOBase, as shown in Adapt an iterator to behave like a file-like object in Python.

On the other hand, in type annotations we are supposed to use the classes derived from typing.IO (e.g. TextIO) to represent such objects, as shown in Type hint for a file or file-like object? or Type-checking issue with io.TextIOBase in a Union.

However, this does not seem to work as I expected:

import io
import sys
import typing

class MyIO(io.TextIOBase):
    def write(self, text: str):
        pass

def hello(f: typing.TextIO):
    f.write('hello')

hello(sys.stdout)             # type checks
hello(open('temp.txt', 'w'))  # type checks
hello(MyIO())                 # does not type check

When running mypy on this code (using Python 3.7.3 and mypy 0.910), we get

error: Argument 1 to "hello" has incompatible type "MyIO"; expected "TextIO"

Question

How can the MyIO class be written such that it is accepted as a function argument of type typing.TextIO (without just using typing.cast(typing.TextIO, ...))?

Failed attempts

  1. Using typing.TextIO as a base class is not possible:

    When using class MyIO(typing.TextIO):

    error: Cannot instantiate abstract class "MyIO" with abstract attributes "__enter__", "__exit__", ... and "writelines" (15 methods suppressed)

    When using class MyIO(io.TextIOBase, typing.TextIO)::

    error: Definition of "readlines" in base class "IOBase" is incompatible with definition in base class "IO"

    and the same for several other methods.

  2. Overriding __new__ and annotating typing.TextIO as return type does not work:

    def __new__(cls, *args, **kwargs) -> typing.TextIO:                        
        return super().__new__(cls, *args, **kwargs)
    

    results in

    error: Incompatible return type for "__new__" (returns "TextIO", but must return a subtype of "MyIO")
    error: Incompatible return value type (got "MyIO", expected "TextIO")

Or is this already supposed to work, and I'm using too old a version of Python and/or mypy? Using --python-version 3.8 or 3.9 or 3.10 as option for mypy does not change anything, however.

Upvotes: 13

Views: 10351

Answers (2)

leberknecht
leberknecht

Reputation: 1736

I think your first attempt is actually correct, it just requires you to implement all the abstract methods (as the error says). You dont have to put actual logic there. The following class would do the trick:

import io
from types import TracebackType
from typing import Optional, Type, Iterator, AnyStr, Iterable, TextIO

import sys
import typing

class MyIO(typing.TextIO):
    def __enter__(self) -> TextIO:
        pass

    def close(self) -> None:
        pass

    def fileno(self) -> int:
        pass

    def flush(self) -> None:
        pass

    def isatty(self) -> bool:
        pass

    def read(self, n: int = ...) -> AnyStr:
        pass

    def readable(self) -> bool:
        pass

    def readline(self, limit: int = ...) -> AnyStr:
        pass

    def readlines(self, hint: int = ...) -> typing.List[AnyStr]:
        pass

    def seek(self, offset: int, whence: int = ...) -> int:
        pass

    def seekable(self) -> bool:
        pass

    def tell(self) -> int:
        pass

    def truncate(self, size: Optional[int] = ...) -> int:
        pass

    def writable(self) -> bool:
        pass

    def writelines(self, lines: Iterable[AnyStr]) -> None:
        pass

    def __next__(self) -> AnyStr:
        pass

    def __iter__(self) -> Iterator[AnyStr]:
        pass

    def __exit__(self, t: Optional[Type[BaseException]], value: Optional[BaseException],
                 traceback: Optional[TracebackType]) -> Optional[bool]:
        pass

    def write(self, text: str):
        pass

def hello(f: typing.TextIO):
    f.write('hello')

hello(sys.stdout)             # type checks
hello(open('temp.txt', 'w'))  # type checks
hello(MyIO())                 # does not type check

Upvotes: 4

itismoej
itismoej

Reputation: 1847

Use io.StringIO instead

import io
import sys
import typing


class MyIO(io.StringIO):
    def write(self, text: str):
        pass


def hello(f: typing.TextIO):
    f.write("hello")


hello(sys.stdout)             # type checks
hello(open("temp.txt", "w"))  # type checks
hello(MyIO())                 # type checks
 

Upvotes: 2

Related Questions