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

MAINT: Backport numpy._core stubs. Remove NumpyUnpickler #24906

Merged
merged 1 commit into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 0 additions & 6 deletions doc/release/upcoming_changes/24870.new_feature.rst

This file was deleted.

6 changes: 6 additions & 0 deletions doc/release/upcoming_changes/24906.new_feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
`numpy._core` submodules' stubs
-------------------------------

`numpy._core` submodules' stubs were added
to provide a stable way for loading pickled arrays,
created with NumPy 2.0, with Numpy 1.26.
1 change: 0 additions & 1 deletion doc/source/reference/routines.io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ NumPy binary files (NPY, NPZ)
save
savez
savez_compressed
lib.format.NumpyUnpickler

The format of these binary file types is documented in
:py:mod:`numpy.lib.format`
Expand Down
6 changes: 2 additions & 4 deletions doc/source/user/how-to-io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,8 @@ Use :func:`numpy.save` and :func:`numpy.load`. Set ``allow_pickle=False``,
unless the array dtype includes Python objects, in which case pickling is
required.

:func:`numpy.load` also supports unpickling files created with NumPy 2.0.
If you try to unpickle a 2.0 pickled array directly, you will get
an exception. Use :class:`numpy.lib.format.NumpyUnpickler` for
unpickling these files.
NumPy 1.26 also supports unpickling files created with NumPy 2.0, either
via :func:`numpy.load` or the pickle module directly.

Convert from a pandas DataFrame to a NumPy array
================================================
Expand Down
4 changes: 4 additions & 0 deletions numpy/_core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
This private module only contains stubs for interoperability with
NumPy 2.0 pickled arrays. It may not be used by the end user.
"""
Empty file added numpy/_core/__init__.pyi
Empty file.
6 changes: 6 additions & 0 deletions numpy/_core/_dtype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from numpy.core import _dtype

_globals = globals()

for item in _dtype.__dir__():
_globals[item] = getattr(_dtype, item)
6 changes: 6 additions & 0 deletions numpy/_core/_dtype_ctypes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from numpy.core import _dtype_ctypes

_globals = globals()

for item in _dtype_ctypes.__dir__():
_globals[item] = getattr(_dtype_ctypes, item)
6 changes: 6 additions & 0 deletions numpy/_core/_internal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from numpy.core import _internal

_globals = globals()

for item in _internal.__dir__():
_globals[item] = getattr(_internal, item)
6 changes: 6 additions & 0 deletions numpy/_core/_multiarray_umath.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from numpy.core import _multiarray_umath

_globals = globals()

for item in _multiarray_umath.__dir__():
_globals[item] = getattr(_multiarray_umath, item)
6 changes: 6 additions & 0 deletions numpy/_core/multiarray.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from numpy.core import multiarray

_globals = globals()

for item in multiarray.__dir__():
_globals[item] = getattr(multiarray, item)
6 changes: 6 additions & 0 deletions numpy/_core/umath.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from numpy.core import umath

_globals = globals()

for item in umath.__dir__():
_globals[item] = getattr(umath, item)
Binary file added numpy/core/tests/data/numpy_2_0_array.pkl
Binary file not shown.
48 changes: 48 additions & 0 deletions numpy/core/tests/test_numpy_2_0_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from os import path
import pickle

import numpy as np


class TestNumPy2Compatibility:

data_dir = path.join(path.dirname(__file__), "data")
filename = path.join(data_dir, "numpy_2_0_array.pkl")

def test_importable__core_stubs(self):
"""
Checks if stubs for `numpy._core` are importable.
"""
from numpy._core.multiarray import _reconstruct
from numpy._core.umath import cos
from numpy._core._multiarray_umath import exp
from numpy._core._internal import ndarray
from numpy._core._dtype import _construction_repr
from numpy._core._dtype_ctypes import dtype_from_ctypes_type

def test_unpickle_numpy_2_0_file(self):
"""
Checks that NumPy 1.26 and pickle is able to load pickles
created with NumPy 2.0 without errors/warnings.
"""
with open(self.filename, mode="rb") as file:
content = file.read()

# Let's make sure that the pickle object we're loading
# was built with NumPy 2.0.
assert b"numpy._core.multiarray" in content

arr = pickle.loads(content, encoding="latin1")

assert isinstance(arr, np.ndarray)
assert arr.shape == (73,) and arr.dtype == np.float64

def test_numpy_load_numpy_2_0_file(self):
"""
Checks that `numpy.load` for NumPy 1.26 is able to load pickles
created with NumPy 2.0 without errors/warnings.
"""
arr = np.load(self.filename, encoding="latin1", allow_pickle=True)

assert isinstance(arr, np.ndarray)
assert arr.shape == (73,) and arr.dtype == np.float64
16 changes: 2 additions & 14 deletions numpy/lib/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@
)


__all__ = ["NumpyUnpickler"]
__all__ = []


EXPECTED_KEYS = {'descr', 'fortran_order', 'shape'}
Expand Down Expand Up @@ -797,7 +797,7 @@ def read_array(fp, allow_pickle=False, pickle_kwargs=None, *,
if pickle_kwargs is None:
pickle_kwargs = {}
try:
array = NumpyUnpickler(fp, **pickle_kwargs).load()
array = pickle.load(fp, **pickle_kwargs)
except UnicodeError as err:
# Friendlier error message
raise UnicodeError("Unpickling a python object failed: %r\n"
Expand Down Expand Up @@ -974,15 +974,3 @@ def _read_bytes(fp, size, error_template="ran out of data"):
raise ValueError(msg % (error_template, size, len(data)))
else:
return data


class NumpyUnpickler(pickle.Unpickler):
"""
A thin wrapper for :py:class:`pickle.Unpickler` that
allows to load 2.0 array pickles with numpy 1.26.
"""

def find_class(self, module: str, name: str) -> object:
if module.startswith("numpy._core"):
module = module.replace("_core", "core", 1)
return pickle.Unpickler.find_class(self, module, name)
3 changes: 0 additions & 3 deletions numpy/lib/format.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import pickle
from typing import Any, Literal, Final

__all__: list[str]
Expand All @@ -21,5 +20,3 @@ def read_array_header_2_0(fp): ...
def write_array(fp, array, version=..., allow_pickle=..., pickle_kwargs=...): ...
def read_array(fp, allow_pickle=..., pickle_kwargs=...): ...
def open_memmap(filename, mode=..., dtype=..., shape=..., fortran_order=..., version=...): ...

class NumpyUnpickler(pickle.Unpickler): ...
2 changes: 1 addition & 1 deletion numpy/lib/npyio.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ def load(file, mmap_mode=None, allow_pickle=False, fix_imports=True,
raise ValueError("Cannot load file containing pickled data "
"when allow_pickle=False")
try:
return format.NumpyUnpickler(fid, **pickle_kwargs).load()
return pickle.load(fid, **pickle_kwargs)
except Exception as e:
raise pickle.UnpicklingError(
f"Failed to interpret file {file!r} as a pickle") from e
Expand Down
1 change: 1 addition & 0 deletions numpy/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ pure_subdirs = [
'_pyinstaller',
'_typing',
'_utils',
'_core',
'array_api',
'compat',
'doc',
Expand Down
1 change: 1 addition & 0 deletions numpy/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def configuration(parent_package='',top_path=None):
config.add_subpackage('array_api')
config.add_subpackage('compat')
config.add_subpackage('core')
config.add_subpackage('_core')
config.add_subpackage('distutils')
config.add_subpackage('doc')
config.add_subpackage('f2py')
Expand Down