Skip to content

Commit 7c83799

Browse files
authored
Various improvements for the mailbox stubs (#14950)
* Use `_typeshed.SupportsItems` instead of custom `_HasItems` protocol. * Use a custom covariant protocol for mailbox messages. * Add `Mailbox._dump_message`. * Return a protocol from abstract method `Mailbox.get_file()`. * Return concrete type `BytesIO` instead of `typing.IO` from `Babyl.get_file()`. * Use our custom protocol instead of semi-protocol `typing.IO` in the remaining cases. Closes: #14935
1 parent 031d564 commit 7c83799

2 files changed

Lines changed: 96 additions & 50 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import mailbox
2+
3+
4+
def mbox1() -> mailbox.Mailbox:
5+
return mailbox.mbox("")
6+
7+
8+
def mbox2() -> mailbox.Mailbox[mailbox.mboxMessage]:
9+
return mailbox.mbox("")
10+
11+
12+
def mbox3() -> mailbox.Mailbox[mailbox.Message]:
13+
return mailbox.mbox("")

stdlib/mailbox.pyi

Lines changed: 83 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import email.message
22
import io
33
import sys
4-
from _typeshed import StrPath, SupportsNoArgReadline, SupportsRead
4+
from _typeshed import StrPath, SupportsItems, SupportsNoArgReadline, SupportsRead, SupportsWrite, Unused
55
from abc import ABCMeta, abstractmethod
66
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
7-
from email._policybase import _MessageT
87
from types import GenericAlias, TracebackType
9-
from typing import IO, Any, AnyStr, Generic, Literal, Protocol, TypeVar, overload, type_check_only
8+
from typing import Any, Generic, Literal, Protocol, TypeVar, overload, type_check_only
109
from typing_extensions import Self, TypeAlias
1110

1211
__all__ = [
@@ -34,23 +33,45 @@ _T = TypeVar("_T")
3433
@type_check_only
3534
class _SupportsReadAndReadline(SupportsRead[bytes], SupportsNoArgReadline[bytes], Protocol): ...
3635

36+
# As opposed to _MessageT_co in email._policybase, this type is bound to
37+
# mailbox.Message instead of email.message.Message.
38+
_MessageT_co = TypeVar("_MessageT_co", bound=Message, default=Message, covariant=True)
39+
3740
_MessageData: TypeAlias = email.message.Message | bytes | str | io.StringIO | _SupportsReadAndReadline
3841

3942
@type_check_only
4043
class _HasIteritems(Protocol):
4144
def iteritems(self) -> Iterator[tuple[str, _MessageData]]: ...
4245

43-
@type_check_only
44-
class _HasItems(Protocol):
45-
def items(self) -> Iterator[tuple[str, _MessageData]]: ...
46-
4746
linesep: bytes
4847

49-
class Mailbox(Generic[_MessageT]):
48+
# Common interface for get_file() return types.
49+
@type_check_only
50+
class _GetFileReturn(Protocol):
51+
def __iter__(self) -> Iterator[bytes]: ...
52+
def __enter__(self) -> Self: ...
53+
def __exit__(
54+
self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None, /
55+
) -> bool | None: ...
56+
def read(self, size: int | None = None, /) -> bytes: ...
57+
def read1(self, size: int | None = None, /) -> bytes: ...
58+
def readline(self, size: int | None = None, /) -> bytes: ...
59+
def readlines(self, sizehint: int | None = None, /) -> list[bytes]: ...
60+
def tell(self) -> int: ...
61+
def seek(self, offset: int, whence: int = 0, /) -> object: ...
62+
def close(self) -> object: ...
63+
def readable(self) -> bool: ...
64+
def writable(self) -> bool: ...
65+
def seekable(self) -> bool: ...
66+
def flush(self) -> object: ...
67+
@property
68+
def closed(self) -> bool: ...
69+
70+
class Mailbox(Generic[_MessageT_co]):
5071
_path: str # undocumented
51-
_factory: Callable[[IO[Any]], _MessageT] | None # undocumented
72+
_factory: Callable[[_GetFileReturn], _MessageT_co] | None # undocumented
5273
@overload
53-
def __init__(self, path: StrPath, factory: Callable[[IO[Any]], _MessageT], create: bool = True) -> None: ...
74+
def __init__(self, path: StrPath, factory: Callable[[_GetFileReturn], _MessageT_co], create: bool = True) -> None: ...
5475
@overload
5576
def __init__(self, path: StrPath, factory: None = None, create: bool = True) -> None: ...
5677
@abstractmethod
@@ -62,37 +83,38 @@ class Mailbox(Generic[_MessageT]):
6283
@abstractmethod
6384
def __setitem__(self, key: str, message: _MessageData) -> None: ...
6485
@overload
65-
def get(self, key: str, default: None = None) -> _MessageT | None: ...
86+
def get(self, key: str, default: None = None) -> _MessageT_co | None: ...
6687
@overload
67-
def get(self, key: str, default: _T) -> _MessageT | _T: ...
68-
def __getitem__(self, key: str) -> _MessageT: ...
88+
def get(self, key: str, default: _T) -> _MessageT_co | _T: ...
89+
def __getitem__(self, key: str) -> _MessageT_co: ...
6990
@abstractmethod
70-
def get_message(self, key: str) -> _MessageT: ...
91+
def get_message(self, key: str) -> _MessageT_co: ...
7192
def get_string(self, key: str) -> str: ...
7293
@abstractmethod
7394
def get_bytes(self, key: str) -> bytes: ...
74-
# As '_ProxyFile' doesn't implement the full IO spec, and BytesIO is incompatible with it, get_file return is Any here
7595
@abstractmethod
76-
def get_file(self, key: str) -> Any: ...
96+
def get_file(self, key: str) -> _GetFileReturn: ...
7797
@abstractmethod
7898
def iterkeys(self) -> Iterator[str]: ...
7999
def keys(self) -> list[str]: ...
80-
def itervalues(self) -> Iterator[_MessageT]: ...
81-
def __iter__(self) -> Iterator[_MessageT]: ...
82-
def values(self) -> list[_MessageT]: ...
83-
def iteritems(self) -> Iterator[tuple[str, _MessageT]]: ...
84-
def items(self) -> list[tuple[str, _MessageT]]: ...
100+
def itervalues(self) -> Iterator[_MessageT_co]: ...
101+
def __iter__(self) -> Iterator[_MessageT_co]: ...
102+
def values(self) -> list[_MessageT_co]: ...
103+
def iteritems(self) -> Iterator[tuple[str, _MessageT_co]]: ...
104+
def items(self) -> list[tuple[str, _MessageT_co]]: ...
85105
@abstractmethod
86106
def __contains__(self, key: str) -> bool: ...
87107
@abstractmethod
88108
def __len__(self) -> int: ...
89109
def clear(self) -> None: ...
90110
@overload
91-
def pop(self, key: str, default: None = None) -> _MessageT | None: ...
111+
def pop(self, key: str, default: None = None) -> _MessageT_co | None: ...
92112
@overload
93-
def pop(self, key: str, default: _T) -> _MessageT | _T: ...
94-
def popitem(self) -> tuple[str, _MessageT]: ...
95-
def update(self, arg: _HasIteritems | _HasItems | Iterable[tuple[str, _MessageData]] | None = None) -> None: ...
113+
def pop(self, key: str, default: _T) -> _MessageT_co | _T: ...
114+
def popitem(self) -> tuple[str, _MessageT_co]: ...
115+
def update(
116+
self, arg: _HasIteritems | SupportsItems[str, _MessageData] | Iterable[tuple[str, _MessageData]] | None = None
117+
) -> None: ...
96118
@abstractmethod
97119
def flush(self) -> None: ...
98120
@abstractmethod
@@ -101,19 +123,21 @@ class Mailbox(Generic[_MessageT]):
101123
def unlock(self) -> None: ...
102124
@abstractmethod
103125
def close(self) -> None: ...
126+
# Undocumented, called by subclasses to parse added messages.
127+
def _dump_message(self, message: _MessageData, target: SupportsWrite[bytes], mangle_from_: bool = False) -> None: ...
104128
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
105129

106130
class Maildir(Mailbox[MaildirMessage]):
107131
colon: str
108132
def __init__(
109-
self, dirname: StrPath, factory: Callable[[IO[Any]], MaildirMessage] | None = None, create: bool = True
133+
self, dirname: StrPath, factory: Callable[[_GetFileReturn], MaildirMessage] | None = None, create: bool = True
110134
) -> None: ...
111-
def add(self, message: _MessageData) -> str: ...
135+
def add(self, message: _MessageData | MaildirMessage) -> str: ...
112136
def remove(self, key: str) -> None: ...
113-
def __setitem__(self, key: str, message: _MessageData) -> None: ...
137+
def __setitem__(self, key: str, message: _MessageData | MaildirMessage) -> None: ...
114138
def get_message(self, key: str) -> MaildirMessage: ...
115139
def get_bytes(self, key: str) -> bytes: ...
116-
def get_file(self, key: str) -> _ProxyFile[bytes]: ...
140+
def get_file(self, key: str) -> _ProxyFile: ...
117141
if sys.version_info >= (3, 13):
118142
def get_info(self, key: str) -> str: ...
119143
def set_info(self, key: str, info: str) -> None: ...
@@ -136,7 +160,7 @@ class Maildir(Mailbox[MaildirMessage]):
136160
def clean(self) -> None: ...
137161
def next(self) -> str | None: ...
138162

139-
class _singlefileMailbox(Mailbox[_MessageT], metaclass=ABCMeta):
163+
class _singlefileMailbox(Mailbox[_MessageT_co], metaclass=ABCMeta):
140164
def add(self, message: _MessageData) -> str: ...
141165
def remove(self, key: str) -> None: ...
142166
def __setitem__(self, key: str, message: _MessageData) -> None: ...
@@ -148,26 +172,32 @@ class _singlefileMailbox(Mailbox[_MessageT], metaclass=ABCMeta):
148172
def flush(self) -> None: ...
149173
def close(self) -> None: ...
150174

151-
class _mboxMMDF(_singlefileMailbox[_MessageT]):
152-
def get_message(self, key: str) -> _MessageT: ...
153-
def get_file(self, key: str, from_: bool = False) -> _PartialFile[bytes]: ...
175+
class _mboxMMDF(_singlefileMailbox[_MessageT_co]):
176+
def get_message(self, key: str) -> _MessageT_co: ...
177+
def get_file(self, key: str, from_: bool = False) -> _PartialFile: ...
154178
def get_bytes(self, key: str, from_: bool = False) -> bytes: ...
155179
def get_string(self, key: str, from_: bool = False) -> str: ...
156180

157181
class mbox(_mboxMMDF[mboxMessage]):
158-
def __init__(self, path: StrPath, factory: Callable[[IO[Any]], mboxMessage] | None = None, create: bool = True) -> None: ...
182+
def __init__(
183+
self, path: StrPath, factory: Callable[[_GetFileReturn], mboxMessage] | None = None, create: bool = True
184+
) -> None: ...
159185

160186
class MMDF(_mboxMMDF[MMDFMessage]):
161-
def __init__(self, path: StrPath, factory: Callable[[IO[Any]], MMDFMessage] | None = None, create: bool = True) -> None: ...
187+
def __init__(
188+
self, path: StrPath, factory: Callable[[_GetFileReturn], MMDFMessage] | None = None, create: bool = True
189+
) -> None: ...
162190

163191
class MH(Mailbox[MHMessage]):
164-
def __init__(self, path: StrPath, factory: Callable[[IO[Any]], MHMessage] | None = None, create: bool = True) -> None: ...
192+
def __init__(
193+
self, path: StrPath, factory: Callable[[_GetFileReturn], MHMessage] | None = None, create: bool = True
194+
) -> None: ...
165195
def add(self, message: _MessageData) -> str: ...
166196
def remove(self, key: str) -> None: ...
167197
def __setitem__(self, key: str, message: _MessageData) -> None: ...
168198
def get_message(self, key: str) -> MHMessage: ...
169199
def get_bytes(self, key: str) -> bytes: ...
170-
def get_file(self, key: str) -> _ProxyFile[bytes]: ...
200+
def get_file(self, key: str) -> _ProxyFile: ...
171201
def iterkeys(self) -> Iterator[str]: ...
172202
def __contains__(self, key: str) -> bool: ...
173203
def __len__(self) -> int: ...
@@ -184,13 +214,15 @@ class MH(Mailbox[MHMessage]):
184214
def pack(self) -> None: ...
185215

186216
class Babyl(_singlefileMailbox[BabylMessage]):
187-
def __init__(self, path: StrPath, factory: Callable[[IO[Any]], BabylMessage] | None = None, create: bool = True) -> None: ...
217+
def __init__(
218+
self, path: StrPath, factory: Callable[[_GetFileReturn], BabylMessage] | None = None, create: bool = True
219+
) -> None: ...
188220
def get_message(self, key: str) -> BabylMessage: ...
189221
def get_bytes(self, key: str) -> bytes: ...
190-
def get_file(self, key: str) -> IO[bytes]: ...
222+
def get_file(self, key: str) -> io.BytesIO: ...
191223
def get_labels(self) -> list[str]: ...
192224

193-
class Message(email.message.Message):
225+
class Message(email.message.Message[str, str]):
194226
def __init__(self, message: _MessageData | None = None) -> None: ...
195227

196228
class MaildirMessage(Message):
@@ -232,18 +264,19 @@ class BabylMessage(Message):
232264

233265
class MMDFMessage(_mboxMMDFMessage): ...
234266

235-
class _ProxyFile(Generic[AnyStr]):
236-
def __init__(self, f: IO[AnyStr], pos: int | None = None) -> None: ...
237-
def read(self, size: int | None = None) -> AnyStr: ...
238-
def read1(self, size: int | None = None) -> AnyStr: ...
239-
def readline(self, size: int | None = None) -> AnyStr: ...
240-
def readlines(self, sizehint: int | None = None) -> list[AnyStr]: ...
241-
def __iter__(self) -> Iterator[AnyStr]: ...
267+
# Until Python 3.14, this class was technically - but unnecessarily - generic at runtime.
268+
class _ProxyFile:
269+
def __init__(self, f: _GetFileReturn, pos: int | None = None) -> None: ...
270+
def read(self, size: int | None = None) -> bytes: ...
271+
def read1(self, size: int | None = None) -> bytes: ...
272+
def readline(self, size: int | None = None) -> bytes: ...
273+
def readlines(self, sizehint: int | None = None) -> list[bytes]: ...
274+
def __iter__(self) -> Iterator[bytes]: ...
242275
def tell(self) -> int: ...
243276
def seek(self, offset: int, whence: int = 0) -> None: ...
244277
def close(self) -> None: ...
245278
def __enter__(self) -> Self: ...
246-
def __exit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None: ...
279+
def __exit__(self, *exc: Unused) -> None: ...
247280
def readable(self) -> bool: ...
248281
def writable(self) -> bool: ...
249282
def seekable(self) -> bool: ...
@@ -252,8 +285,8 @@ class _ProxyFile(Generic[AnyStr]):
252285
def closed(self) -> bool: ...
253286
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
254287

255-
class _PartialFile(_ProxyFile[AnyStr]):
256-
def __init__(self, f: IO[AnyStr], start: int | None = None, stop: int | None = None) -> None: ...
288+
class _PartialFile(_ProxyFile):
289+
def __init__(self, f: _GetFileReturn, start: int | None = None, stop: int | None = None) -> None: ...
257290

258291
class Error(Exception): ...
259292
class NoSuchMailboxError(Error): ...

0 commit comments

Comments
 (0)