Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

a close reading of the pickle module #12971

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions stdlib/@tests/stubtest_allowlists/common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ multiprocessing.synchronize.SemLock.acquire
multiprocessing.synchronize.SemLock.release
numbers.Number.__hash__ # typeshed marks this as abstract but code just sets this as None
optparse.Values.__getattr__ # Some attributes are set in __init__ using setattr
pickle.Pickler.reducer_override # implemented in C pickler
pyexpat.expat_CAPI
select.poll # Depends on configuration
selectors.DevpollSelector # Depends on configuration
Expand Down Expand Up @@ -452,12 +451,8 @@ imaplib.IMAP4_SSL.ssl
ssl.PROTOCOL_SSLv2
ssl.PROTOCOL_SSLv3

pickle._Pickler\..* # Best effort typing for undocumented internals
pickle._Unpickler\..* # Best effort typing for undocumented internals
pickle.Pickler.memo # undocumented implementation detail, has different type in C/Python implementations
pickle.Pickler.persistent_id # C pickler persistent_id is an attribute
pickle.Unpickler.memo # undocumented implementation detail, has different type in C/Python implementations
pickle.Unpickler.persistent_load # C unpickler persistent_load is an attribute
pickle._Pickler\..* # Best effort typing for undocumented internals
pickle._Unpickler\..* # Best effort typing for undocumented internals
re.Pattern.scanner # Undocumented and not useful. #6405
tempfile._TemporaryFileWrapper.[\w_]+ # Dynamically specified by __getattr__, and thus don't exist on the class

Expand Down
1 change: 1 addition & 0 deletions stdlib/VERSIONS
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ _markupbase: 3.0-
_msi: 3.0-3.12
_operator: 3.4-
_osx_support: 3.0-
_pickle: 3.0-
_posixsubprocess: 3.2-
_py_abc: 3.7-
_pydecimal: 3.5-
Expand Down
108 changes: 108 additions & 0 deletions stdlib/_pickle.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import sys
from _typeshed import ReadableBuffer, SupportsWrite
from collections.abc import Callable, Iterable, Iterator, Mapping
from pickle import PickleBuffer as PickleBuffer
from typing import Any, Protocol, type_check_only
from typing_extensions import TypeAlias

class _ReadableFileobj(Protocol):
def read(self, n: int, /) -> bytes: ...
def readline(self) -> bytes: ...

_BufferCallback: TypeAlias = Callable[[PickleBuffer], Any] | None

_ReducedType: TypeAlias = (
str
| tuple[Callable[..., Any], tuple[Any, ...]]
| tuple[Callable[..., Any], tuple[Any, ...], Any]
| tuple[Callable[..., Any], tuple[Any, ...], Any, Iterator[Any] | None]
| tuple[Callable[..., Any], tuple[Any, ...], Any, Iterator[Any] | None, Iterator[Any] | None]
)

def dump(
obj: Any,
file: SupportsWrite[bytes],
protocol: int | None = None,
*,
fix_imports: bool = True,
buffer_callback: _BufferCallback = None,
) -> None: ...
def dumps(
obj: Any, protocol: int | None = None, *, fix_imports: bool = True, buffer_callback: _BufferCallback = None
) -> bytes: ...
def load(
file: _ReadableFileobj,
*,
fix_imports: bool = True,
encoding: str = "ASCII",
errors: str = "strict",
buffers: Iterable[Any] | None = (),
) -> Any: ...
def loads(
data: ReadableBuffer,
/,
*,
fix_imports: bool = True,
encoding: str = "ASCII",
errors: str = "strict",
buffers: Iterable[Any] | None = (),
) -> Any: ...

class PickleError(Exception): ...
class PicklingError(PickleError): ...
class UnpicklingError(PickleError): ...

@type_check_only
class PicklerMemoProxy:
def clear(self, /) -> None: ...
def copy(self, /) -> dict[int, tuple[int, Any]]: ...

class Pickler:
fast: bool
dispatch_table: Mapping[type, Callable[[Any], _ReducedType]]
reducer_override: Callable[[Any], Any]
bin: bool # undocumented
def __init__(
self,
file: SupportsWrite[bytes],
protocol: int | None = None,
*,
fix_imports: bool = True,
buffer_callback: _BufferCallback = None,
) -> None: ...
@property
def memo(self) -> PicklerMemoProxy: ...
@memo.setter
def memo(self, value: PicklerMemoProxy | dict[int, tuple[int, Any]]) -> None: ...
def dump(self, obj: Any, /) -> None: ...
def clear_memo(self) -> None: ...
if sys.version_info >= (3, 13):
def persistent_id(self, obj: Any, /) -> Any: ...
else:
persistent_id: Callable[[Any], Any]

@type_check_only
class UnpicklerMemoProxy:
def clear(self, /) -> None: ...
def copy(self, /) -> dict[int, tuple[int, Any]]: ...

class Unpickler:
def __init__(
self,
file: _ReadableFileobj,
*,
fix_imports: bool = True,
encoding: str = "ASCII",
errors: str = "strict",
buffers: Iterable[Any] | None = (),
) -> None: ...
@property
def memo(self) -> UnpicklerMemoProxy: ...
@memo.setter
def memo(self, value: UnpicklerMemoProxy | dict[int, tuple[int, Any]]) -> None: ...
def load(self) -> Any: ...
def find_class(self, module_name: str, global_name: str, /) -> Any: ...
if sys.version_info >= (3, 13):
def persistent_load(self, pid: Any, /) -> Any: ...
else:
persistent_load: Callable[[Any], Any]
2 changes: 1 addition & 1 deletion stdlib/multiprocessing/reduction.pyi
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import pickle
import sys
from _pickle import _ReducedType
from _typeshed import HasFileno, SupportsWrite, Unused
from abc import ABCMeta
from builtins import type as Type # alias to avoid name clash
from collections.abc import Callable
from copyreg import _DispatchTableType
from multiprocessing import connection
from pickle import _ReducedType
from socket import socket
from typing import Any, Final

Expand Down
137 changes: 49 additions & 88 deletions stdlib/pickle.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
from _pickle import (
PickleError as PickleError,
Pickler as Pickler,
PicklingError as PicklingError,
Unpickler as Unpickler,
UnpicklingError as UnpicklingError,
_BufferCallback,
_ReadableFileobj,
_ReducedType,
dump as dump,
dumps as dumps,
load as load,
loads as loads,
)
from _typeshed import ReadableBuffer, SupportsWrite
from collections.abc import Callable, Iterable, Iterator, Mapping
from typing import Any, ClassVar, Protocol, SupportsBytes, SupportsIndex, final
from typing_extensions import TypeAlias
from collections.abc import Callable, Iterable, Mapping
from typing import Any, ClassVar, SupportsBytes, SupportsIndex, final

__all__ = [
"PickleBuffer",
Expand Down Expand Up @@ -93,10 +106,6 @@ DEFAULT_PROTOCOL: int

bytes_types: tuple[type[Any], ...] # undocumented

class _ReadableFileobj(Protocol):
def read(self, n: int, /) -> bytes: ...
def readline(self) -> bytes: ...

@final
class PickleBuffer:
def __init__(self, buffer: ReadableBuffer) -> None: ...
Expand All @@ -105,84 +114,6 @@ class PickleBuffer:
def __buffer__(self, flags: int, /) -> memoryview: ...
def __release_buffer__(self, buffer: memoryview, /) -> None: ...

_BufferCallback: TypeAlias = Callable[[PickleBuffer], Any] | None

def dump(
obj: Any,
file: SupportsWrite[bytes],
protocol: int | None = None,
*,
fix_imports: bool = True,
buffer_callback: _BufferCallback = None,
) -> None: ...
def dumps(
obj: Any, protocol: int | None = None, *, fix_imports: bool = True, buffer_callback: _BufferCallback = None
) -> bytes: ...
def load(
file: _ReadableFileobj,
*,
fix_imports: bool = True,
encoding: str = "ASCII",
errors: str = "strict",
buffers: Iterable[Any] | None = (),
) -> Any: ...
def loads(
data: ReadableBuffer,
/,
*,
fix_imports: bool = True,
encoding: str = "ASCII",
errors: str = "strict",
buffers: Iterable[Any] | None = (),
) -> Any: ...

class PickleError(Exception): ...
class PicklingError(PickleError): ...
class UnpicklingError(PickleError): ...

_ReducedType: TypeAlias = (
str
| tuple[Callable[..., Any], tuple[Any, ...]]
| tuple[Callable[..., Any], tuple[Any, ...], Any]
| tuple[Callable[..., Any], tuple[Any, ...], Any, Iterator[Any] | None]
| tuple[Callable[..., Any], tuple[Any, ...], Any, Iterator[Any] | None, Iterator[Any] | None]
)

class Pickler:
fast: bool
dispatch_table: Mapping[type, Callable[[Any], _ReducedType]]
bin: bool # undocumented
dispatch: ClassVar[dict[type, Callable[[Unpickler, Any], None]]] # undocumented, _Pickler only

def __init__(
self,
file: SupportsWrite[bytes],
protocol: int | None = None,
*,
fix_imports: bool = True,
buffer_callback: _BufferCallback = None,
) -> None: ...
def reducer_override(self, obj: Any) -> Any: ...
def dump(self, obj: Any, /) -> None: ...
def clear_memo(self) -> None: ...
def persistent_id(self, obj: Any) -> Any: ...

class Unpickler:
dispatch: ClassVar[dict[int, Callable[[Unpickler], None]]] # undocumented, _Unpickler only

def __init__(
self,
file: _ReadableFileobj,
*,
fix_imports: bool = True,
encoding: str = "ASCII",
errors: str = "strict",
buffers: Iterable[Any] | None = (),
) -> None: ...
def load(self) -> Any: ...
def find_class(self, module_name: str, global_name: str, /) -> Any: ...
def persistent_load(self, pid: Any) -> Any: ...

MARK: bytes
STOP: bytes
POP: bytes
Expand Down Expand Up @@ -266,6 +197,36 @@ READONLY_BUFFER: bytes
def encode_long(x: int) -> bytes: ... # undocumented
def decode_long(data: Iterable[SupportsIndex] | SupportsBytes | ReadableBuffer) -> int: ... # undocumented

# pure-Python implementations
_Pickler = Pickler # undocumented
_Unpickler = Unpickler # undocumented
# undocumented pure-Python implementations
class _Pickler:
fast: bool
dispatch_table: Mapping[type, Callable[[Any], _ReducedType]]
bin: bool # undocumented
dispatch: ClassVar[dict[type, Callable[[Unpickler, Any], None]]] # undocumented, _Pickler only
reducer_override: Callable[[Any], Any]
def __init__(
self,
file: SupportsWrite[bytes],
protocol: int | None = None,
*,
fix_imports: bool = True,
buffer_callback: _BufferCallback = None,
) -> None: ...
def dump(self, obj: Any) -> None: ...
def clear_memo(self) -> None: ...
def persistent_id(self, obj: Any) -> Any: ...

class _Unpickler:
dispatch: ClassVar[dict[int, Callable[[Unpickler], None]]] # undocumented, _Unpickler only
def __init__(
self,
file: _ReadableFileobj,
*,
fix_imports: bool = True,
encoding: str = "ASCII",
errors: str = "strict",
buffers: Iterable[Any] | None = None,
) -> None: ...
def load(self) -> Any: ...
def find_class(self, module: str, name: str) -> Any: ...
def persistent_load(self, pid: Any) -> Any: ...