Repository: astropenguin/pandas-dataclasses

Issue:
Update named fields
- [ ] Remove `NamedData` and `NamedIndex`
- [ ] Add `Named` type hint for general named fields (e.g. `Named[Attr[int], "name"]`)

Comments:

Patch:
diff --git a/pandas_dataclasses/typing.py b/pandas_dataclasses/typing.py
index dad9a08..1c98367 100644
--- a/pandas_dataclasses/typing.py
+++ b/pandas_dataclasses/typing.py
@@ -1,4 +1,4 @@
-__all__ = ["Attr", "Data", "Index", "Name", "NamedData", "NamedIndex"]
+__all__ = ["Attr", "Data", "Index", "Name", "Named"]
 
 
 # standard library
@@ -7,10 +7,10 @@ from typing import Any, Collection, Hashable, Optional, TypeVar, Union
 
 
 # dependencies
+import numpy as np
 from typing_extensions import (
     Annotated,
     Literal,
-    Protocol,
     get_args,
     get_origin,
     get_type_hints,
@@ -23,18 +23,6 @@ TDtype = TypeVar("TDtype", covariant=True)
 TName = TypeVar("TName", bound=Hashable, covariant=True)
 
 
-class Named(Protocol[TName]):
-    """Type hint for named objects."""
-
-    pass
-
-
-class Collection(Named[TName], Collection[TDtype], Protocol):
-    """Type hint for named collection objects."""
-
-    pass
-
-
 # type hints (public)
 class FieldType(Enum):
     """Annotations for pandas-related type hints."""
@@ -59,39 +47,35 @@ class FieldType(Enum):
 Attr = Annotated[TAttr, FieldType.ATTR]
 """Type hint for attribute fields (``Attr[TAttr]``)."""
 
-Data = Annotated[Union[Collection[None, TDtype], TDtype], FieldType.DATA]
+Data = Annotated[Union[Collection[TDtype], TDtype], FieldType.DATA]
 """Type hint for data fields (``Data[TDtype]``)."""
 
-Index = Annotated[Union[Collection[None, TDtype], TDtype], FieldType.INDEX]
+Index = Annotated[Union[Collection[TDtype], TDtype], FieldType.INDEX]
 """Type hint for index fields (``Index[TDtype]``)."""
 
 Name = Annotated[TName, FieldType.NAME]
 """Type hint for name fields (``Name[TName]``)."""
 
-NamedData = Annotated[Union[Collection[TName, TDtype], TDtype], FieldType.DATA]
-"""Type hint for named data fields (``NamedData[TName, TDtype]``)."""
-
-NamedIndex = Annotated[Union[Collection[TName, TDtype], TDtype], FieldType.INDEX]
-"""Type hint for named index fields (``NamedIndex[TName, TDtype]``)."""
+Named = Annotated
+"""Type hint for named fields (alias of Annotated)."""
 
 
 # runtime functions
-def get_dtype(type_: Any) -> Optional[str]:
-    """Parse a type and return a dtype."""
-    args = get_args(type_)
-    origin = get_origin(type_)
-
-    if origin is Collection:
-        return get_dtype(args[1])
-
-    if origin is Literal:
-        return args[0]
+def get_dtype(type_: Any) -> Optional["np.dtype[Any]"]:
+    """Parse a type and return a data type (dtype)."""
+    try:
+        t_dtype = get_args(unannotate(type_))[1]
+    except (IndexError, NameError):
+        raise ValueError(f"Could not convert {type_!r} to dtype.")
 
-    if type_ is Any or type_ is type(None):
+    if t_dtype is Any or t_dtype is type(None):
         return None
 
-    if isinstance(type_, type):
-        return type_.__name__
+    if isinstance(t_dtype, type):
+        return np.dtype(t_dtype)
+
+    if get_origin(t_dtype) is Literal:
+        return np.dtype(get_args(t_dtype)[0])
 
     raise ValueError(f"Could not convert {type_!r} to dtype.")
 
@@ -115,33 +99,21 @@ def get_ftype(type_: Any) -> FieldType:
 
 def get_name(type_: Any) -> Optional[Hashable]:
     """Parse a type and return a name."""
-    args = get_args(type_)
-    origin = get_origin(type_)
+    if get_origin(type_) is not Annotated:
+        return
 
-    if origin is Collection:
-        return get_dtype(args[0])
+    for arg in reversed(get_args(type_)[1:]):
+        if isinstance(arg, FieldType):
+            continue
 
-    if origin is Literal:
-        return args[0]
+        if isinstance(arg, Hashable):
+            return arg
 
-    if type_ is type(None):
-        return None
 
-    raise ValueError(f"Could not convert {type_!r} to name.")
-
-
-def get_rtype(type_: Any) -> Any:
-    """Parse a type and return a representative type (rtype)."""
+def unannotate(type_: Any) -> Any:
+    """Recursively remove annotations from a type."""
 
     class Temporary:
         __annotations__ = dict(type=type_)
 
-    try:
-        unannotated = get_type_hints(Temporary)["type"]
-    except NameError:
-        raise ValueError(f"Could not convert {type_!r} to rtype.")
-
-    if get_origin(unannotated) is Union:
-        return get_args(unannotated)[0]
-    else:
-        return unannotated
+    return get_type_hints(Temporary)["type"]


Test patch:
diff --git a/tests/test_typing.py b/tests/test_typing.py
index ad20503..dc5aedf 100644
--- a/tests/test_typing.py
+++ b/tests/test_typing.py
@@ -1,42 +1,36 @@
 # standard library
-from typing import Any, Optional, Union
+from typing import Any
 
 
 # dependencies
+import numpy as np
 from pytest import mark
-from typing_extensions import Annotated, Literal
+from typing_extensions import Literal
 
 
 # submodules
 from pandas_dataclasses.typing import (
-    Collection,
     Attr,
     Data,
     Index,
     Name,
+    Named,
     get_dtype,
     get_ftype,
     get_name,
-    get_rtype,
 )
 
 
-# type hints
-Int64 = Literal["int64"]
-Label = Literal["label"]
-NoneType = type(None)
-
-
 # test datasets
 testdata_dtype = [
-    (Any, None),
-    (NoneType, None),
-    (Int64, "int64"),
-    (int, "int"),
-    (Collection[Any, Any], None),
-    (Collection[Any, None], None),
-    (Collection[Any, Int64], "int64"),
-    (Collection[Any, int], "int"),
+    (Data[Any], None),
+    (Data[None], None),
+    (Data[int], np.dtype("int64")),
+    (Data[Literal["i8"]], np.dtype("int64")),
+    (Index[Any], None),
+    (Index[None], None),
+    (Index[int], np.dtype("int64")),
+    (Index[Literal["i8"]], np.dtype("int64")),
 ]
 
 testdata_ftype = [
@@ -47,20 +41,16 @@ testdata_ftype = [
 ]
 
 testdata_name = [
-    (NoneType, None),
-    (Label, "label"),
-    (Collection[None, Any], None),
-    (Collection[Label, Any], "label"),
-]
-
-testdata_rtype = [
-    (int, int),
-    (Annotated[int, "annotation"], int),
-    (Union[int, float], int),
-    (Optional[int], int),
+    (Attr[Any], None),
+    (Data[Any], None),
+    (Index[Any], None),
+    (Name[Any], None),
+    (Named[Attr[Any], "attr"], "attr"),
+    (Named[Data[Any], "data"], "data"),
+    (Named[Index[Any], "index"], "index"),
+    (Named[Name[Any], "name"], "name"),
 ]
 
-
 # test functions
 @mark.parametrize("type_, dtype", testdata_dtype)
 def test_get_dtype(type_: Any, dtype: Any) -> None:
@@ -68,15 +58,10 @@ def test_get_dtype(type_: Any, dtype: Any) -> None:
 
 
 @mark.parametrize("type_, ftype", testdata_ftype)
-def test_get_field_type(type_: Any, ftype: Any) -> None:
+def test_get_ftype(type_: Any, ftype: Any) -> None:
     assert get_ftype(type_).value == ftype
 
 
 @mark.parametrize("type_, name", testdata_name)
 def test_get_name(type_: Any, name: Any) -> None:
     assert get_name(type_) == name
-
-
-@mark.parametrize("type_, rtype", testdata_rtype)
-def test_get_rtype(type_: Any, rtype: Any) -> None:
-    assert get_rtype(type_) == rtype