Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Bug fixes
^^^^^^^^^
- Fixed a bug in the :class:`DataFrame` constructor when passed a :class:`Series` or
:class:`Index` correctly handling Copy-on-Write (:issue:`63899`)
- Allow :class:`.ExtensionArray` to have dtypes involving :class:`numpy.void` (:issue:`54810`)

.. ---------------------------------------------------------------------------
.. _whatsnew_301.contributors:
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ def _validate_dtype(cls, dtype) -> DtypeObj | None:
dtype = pandas_dtype(dtype)

# a compound dtype
if dtype.kind == "V":
if dtype.kind == "V" and not isinstance(dtype, ExtensionDtype):
raise NotImplementedError(
"compound dtypes are not implemented "
f"in the {cls.__name__} constructor"
Expand Down
Empty file.
93 changes: 93 additions & 0 deletions pandas/tests/extension/uuid/test_uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from __future__ import annotations

from typing import (
TYPE_CHECKING,
ClassVar,
Self,
)
from uuid import UUID

import numpy as np

from pandas.core.dtypes.dtypes import ExtensionDtype

import pandas as pd
from pandas.core.arrays.base import ExtensionArray

if TYPE_CHECKING:
import builtins
from collections.abc import Iterable

from numpy.typing import NDArray

from pandas._typing import (
Dtype,
ScalarIndexer,
)


# 16 void bytes: 128 bit, every pattern valid, no funky behavior like 0 stripping.
_UuidNumpyDtype = np.dtype("V16")


class UuidDtype(ExtensionDtype):
# ExtensionDtype essential API (3 class attrs and methods)

name: ClassVar[str] = "uuid"
type: ClassVar[builtins.type[UUID]] = UUID

@classmethod
def construct_array_type(cls) -> builtins.type[UuidExtensionArray]:
return UuidExtensionArray

# ExtensionDtype overrides
kind: ClassVar[str] = _UuidNumpyDtype.kind


class UuidExtensionArray(ExtensionArray):
# Implementation details and convenience

_data: NDArray[np.void]

def __init__(self, values: Iterable[UUID], *, copy: bool = False) -> None:
self._data = np.array([x.bytes for x in values], dtype=_UuidNumpyDtype)

# Parts of ExtensionArray's essential API required for tests:

dtype: ClassVar[UuidDtype] = UuidDtype()

@classmethod
def _from_sequence(
cls,
scalars: Iterable[UUID],
*,
dtype: Dtype | None = None,
copy: bool = False,
) -> Self:
if dtype is None:
dtype = UuidDtype()
return cls(scalars, copy=copy)

def __getitem__(self, index: ScalarIndexer) -> UUID: # type: ignore[override]
assert isinstance(index, int | np.integer)
return UUID(bytes=self._data[index].tobytes())

def __len__(self) -> int:
return len(self._data)


def test_construct() -> None:
"""Tests that we can construct UuidExtensionArray from a list of valid values."""
from uuid import uuid4

a = UuidExtensionArray([UUID(int=0), u := uuid4()])
assert a[0].int == 0
assert a[1] == u


def test_series() -> None:
"""Tests that Series accepts (unstructured) void ExtensionDtypes."""
from uuid import uuid4

s = pd.Series([u := uuid4()], dtype=UuidDtype(), name="s")
assert str(u) in str(s)
Loading