Fix/improve typing for qtutils.savefile_open

Contrary to what I thought at the time when initially writing this,
typing.AnyStr isn't just an alias for IO[str] | IO[bytes], but is actually a
TypeVar. As per the Python docs, it should be used when there are *multiple*
places where the types need to match:

    def concat(a: AnyStr, b: AnyStr) -> AnyStr:
        return a + b

What we do instead is somewhat akin to "def fun() -> T:", which mypy already
comments on:

    error: A function returning TypeVar should receive at least one argument
    containing the same TypeVar  [type-var]
        def t() -> T:

Not quite sure why it doesn't in this case, or why it now raises an additional
error (possibly the new inferrence code or something?). Either way, with this
commit the annotations are now more correctly using Union[IO[str], IO[bytes]],
including typing.Literal overloads so that mypy actually knows what specific
type will be returned by a call.
This commit is contained in:
Florian Bruhin 2023-11-20 11:18:19 +01:00
parent 3759738f52
commit 4c08a3393c
1 changed files with 24 additions and 4 deletions

View File

@ -18,8 +18,8 @@ import enum
import pathlib
import operator
import contextlib
from typing import (Any, AnyStr, TYPE_CHECKING, BinaryIO, IO, Iterator,
Optional, Union, Tuple, Protocol, cast, TypeVar)
from typing import (Any, TYPE_CHECKING, BinaryIO, IO, Iterator, Literal,
Optional, Union, Tuple, Protocol, cast, overload, TypeVar)
from qutebrowser.qt import machinery, sip
from qutebrowser.qt.core import (qVersion, QEventLoop, QDataStream, QByteArray,
@ -236,12 +236,32 @@ def deserialize_stream(stream: QDataStream, obj: _QtSerializableType) -> None:
check_qdatastream(stream)
@overload
@contextlib.contextmanager
def savefile_open(
filename: str,
binary: Literal[False] = ...,
encoding: str = 'utf-8'
) -> Iterator[IO[str]]:
...
@overload
@contextlib.contextmanager
def savefile_open(
filename: str,
binary: Literal[True] = ...,
encoding: str = 'utf-8'
) -> Iterator[IO[str]]:
...
@contextlib.contextmanager
def savefile_open(
filename: str,
binary: bool = False,
encoding: str = 'utf-8'
) -> Iterator[IO[AnyStr]]:
) -> Iterator[Union[IO[str], IO[bytes]]]:
"""Context manager to easily use a QSaveFile."""
f = QSaveFile(filename)
cancelled = False
@ -253,7 +273,7 @@ def savefile_open(
dev = cast(BinaryIO, PyQIODevice(f))
if binary:
new_f: IO[Any] = dev # FIXME:mypy Why doesn't AnyStr work?
new_f: Union[IO[str], IO[bytes]] = dev
else:
new_f = io.TextIOWrapper(dev, encoding=encoding)