Repository: astropenguin/xarray-dataclasses (python)

Issue:
Fix missing default values for default DataArray and Dataset factories
This hotfix adds missing default values to the following factories and releases v0.5.1.

- `dataarray.AsDataArray.__dataarray_factory__`
- `dataset.AsDataset.__dataset_factory__`

Pull request description:
Fix missing default values for default DataArray and Dataset factories
This PR closes #86.

Patch:
diff --git a/pyproject.toml b/pyproject.toml
index 2c929b9..085a7d7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "xarray-dataclasses"
-version = "0.5.0"
+version = "0.5.1"
 description = "xarray extension for typed DataArray and Dataset creation"
 authors = ["Akio Taniguchi <taniguchi@a.phys.nagoya-u.ac.jp>"]
 license = "MIT"
diff --git a/xarray_dataclasses/__init__.py b/xarray_dataclasses/__init__.py
index fdf3fee..7909ba9 100644
--- a/xarray_dataclasses/__init__.py
+++ b/xarray_dataclasses/__init__.py
@@ -1,7 +1,7 @@
 # flake8: noqa
 # type: ignore
 __author__ = "Akio Taniguchi"
-__version__ = "0.5.0"
+__version__ = "0.5.1"
 
 
 # for Python 3.7 - 3.8
diff --git a/xarray_dataclasses/dataarray.py b/xarray_dataclasses/dataarray.py
index 685d424..9cf07f9 100644
--- a/xarray_dataclasses/dataarray.py
+++ b/xarray_dataclasses/dataarray.py
@@ -114,7 +114,7 @@ class classproperty:
 class AsDataArray:
     """Mix-in class that provides shorthand methods."""
 
-    def __dataarray_factory__(self, data: Any) -> xr.DataArray:
+    def __dataarray_factory__(self, data: Any = None) -> xr.DataArray:
         """Default DataArray factory (xarray.DataArray)."""
         return xr.DataArray(data)
 
diff --git a/xarray_dataclasses/datamodel.py b/xarray_dataclasses/datamodel.py
index ac65410..70e08e7 100644
--- a/xarray_dataclasses/datamodel.py
+++ b/xarray_dataclasses/datamodel.py
@@ -9,18 +9,16 @@ from typing import Any, Callable, cast, Generic, List, TypeVar, Union
 # dependencies
 import numpy as np
 import xarray as xr
-from typing_extensions import TypedDict
+from typing_extensions import get_args, TypedDict
 
 
 # submodules
-from .deprecated import get_type_hints
 from .typing import (
     ArrayLike,
     DataClass,
     Dims,
     Dtype,
     FieldType,
-    get_class,
     get_dims,
     get_dtype,
     Reference,
@@ -82,8 +80,10 @@ class Data(FieldModel[xr.DataArray]):
     @classmethod
     def from_field(cls, field: Field[Any], value: Any) -> "Data":
         """Create a field model from a dataclass field and a value."""
-        dims = get_dims(field.type)
-        dtype = get_dtype(field.type)
+        args = get_args(get_args(unannotate(field.type))[0])
+
+        dims = get_dims(args[0])
+        dtype = get_dtype(args[1])
         type: DataArrayDict = {"dims": dims, "dtype": dtype}
 
         def factory(value: Any, reference: Reference) -> xr.DataArray:
@@ -102,7 +102,7 @@ class Dataof(FieldModel[xr.DataArray]):
     @classmethod
     def from_field(cls, field: Field[Any], value: Any) -> "Dataof":
         """Create a field model from a dataclass field and a value."""
-        dataclass = get_class(field.type)
+        dataclass = get_args(unannotate(field.type))[0]
         type = resolve_class(dataclass)
 
         def factory(value: Any, reference: Reference) -> xr.DataArray:
@@ -151,7 +151,6 @@ class DataModel:
     def from_dataclass(cls, dataclass: DataClass) -> "DataModel":
         """Create a data model from a dataclass or its object."""
         model = cls()
-        eval_field_types(dataclass)
 
         for field_ in dataclass.__dataclass_fields__.values():
             value = getattr(dataclass, field_.name, field_.default)
@@ -173,15 +172,6 @@ class DataModel:
 
 
 # runtime functions
-def eval_field_types(dataclass: DataClass) -> None:
-    """Evaluate field types of a dataclass or its object."""
-    hints = get_type_hints(dataclass, include_extras=True)  # type: ignore
-
-    for field_ in dataclass.__dataclass_fields__.values():
-        if isinstance(field_.type, str):
-            field_.type = hints[field_.name]
-
-
 def typedarray(
     data: Any,
     dims: Dims,
diff --git a/xarray_dataclasses/dataset.py b/xarray_dataclasses/dataset.py
index e6309b3..e577226 100644
--- a/xarray_dataclasses/dataset.py
+++ b/xarray_dataclasses/dataset.py
@@ -111,7 +111,7 @@ class classproperty:
 class AsDataset:
     """Mix-in class that provides shorthand methods."""
 
-    def __dataset_factory__(self, data_vars: Any) -> xr.Dataset:
+    def __dataset_factory__(self, data_vars: Any = None) -> xr.Dataset:
         """Default Dataset factory (xarray.Dataset)."""
         return xr.Dataset(data_vars)
 
diff --git a/xarray_dataclasses/deprecated.py b/xarray_dataclasses/deprecated.py
index aef8069..59d0248 100644
--- a/xarray_dataclasses/deprecated.py
+++ b/xarray_dataclasses/deprecated.py
@@ -1,22 +1,20 @@
 """Submodule for functions that will be deprecated in v1.0.0."""
-__all__ = ["dataarrayclass", "datasetclass", "get_type_hints"]
+__all__ = ["dataarrayclass", "datasetclass"]
 
 
 # standard library
-import re
 from dataclasses import dataclass, Field
-from typing import Any, Callable, Dict, ForwardRef, Optional, Type, TypeVar, Union
-from typing import _eval_type  # type: ignore
+from typing import Any, Callable, Dict, Optional, Type, TypeVar, Union
 from warnings import warn
 
 
 # dependencies
-from typing_extensions import Literal, Protocol
-from typing_extensions import get_type_hints as _get_type_hints
+from typing_extensions import Protocol
 
 
-# constants
-GENERIC_NAME = re.compile(r"\[.+\]")
+# submodules
+from .dataarray import AsDataArray
+from .dataset import AsDataset
 
 
 # type hints
@@ -43,13 +41,13 @@ def dataarrayclass(
     shorthands: bool = True,
 ) -> Union[Type[DataClass], Callable[[type], Type[DataClass]]]:
     """Class decorator to create a DataArray class."""
-    from .dataarray import AsDataArray
 
     warn(
-        "This decorator will be removed in v1.0.0. "
-        "Please consider to use the Python's dataclass "
-        "and the mix-in class (AsDataArray) instead.",
-        category=FutureWarning,
+        DeprecationWarning(
+            "This decorator will be removed in v1.0.0. ",
+            "Please consider to use the Python's dataclass ",
+            "and the mix-in class (AsDataArray) instead.",
+        )
     )
 
     def to_dataclass(cls: Type[Any]) -> Type[DataClass]:
@@ -83,13 +81,13 @@ def datasetclass(
     shorthands: bool = True,
 ) -> Union[Type[DataClass], Callable[[type], Type[DataClass]]]:
     """Class decorator to create a Dataset class."""
-    from .dataset import AsDataset
 
     warn(
-        "This decorator will be removed in v1.0.0. "
-        "Please consider to use the Python's dataclass "
-        "and the mix-in class (AsDataset) instead.",
-        category=FutureWarning,
+        DeprecationWarning(
+            "This decorator will be removed in v1.0.0. ",
+            "Please consider to use the Python's dataclass ",
+            "and the mix-in class (AsDataset) instead.",
+        )
     )
 
     def to_dataclass(cls: Type[Any]) -> Type[DataClass]:
@@ -111,27 +109,6 @@ def datasetclass(
         return to_dataclass(cls)
 
 
-def eval_type(type: Any, *args: Any, **kwargs: Any) -> Any:
-    if not isinstance(type, ForwardRef):
-        return _eval_type(type, *args, **kwargs)
-
-    name = type.__forward_arg__
-
-    if GENERIC_NAME.search(name):
-        return _eval_type(type, *args, **kwargs)
-
-    warn(
-        f"For backward compatibility, forward reference {name!r} "
-        f"was not evaluated but converted to Literal[{name!r}]. "
-        "From v1.0.0, it will be evaluated by default, which may "
-        f"raise NameError or unexpectedly assign an object to {name}. "
-        f"Please consider to replace {name!r} with Literal[{name!r}].",
-        category=FutureWarning,
-    )
-
-    return Literal[name]
-
-
 def extend_class(cls: Type[T], mixin: Type[Any]) -> Type[T]:
     """Extend a class with a mix-in class."""
     if cls.__bases__ == (object,):
@@ -140,25 +117,3 @@ def extend_class(cls: Type[T], mixin: Type[Any]) -> Type[T]:
         bases = (*cls.__bases__, mixin)
 
     return type(cls.__name__, bases, cls.__dict__.copy())
-
-
-def get_type_hints(
-    obj: Callable[..., Any],
-    globalns: Optional[Dict[str, Any]] = None,
-    localns: Optional[Dict[str, Any]] = None,
-    include_extras: bool = False,
-) -> Dict[str, Any]:
-    """Return type hints for an object.
-
-    Unlike the original (``typing.get_type_hints``), it does NOT handle
-    forward references encoded as string but converts them to literal types.
-    Other behavior is the same as the original.
-
-    """
-    import typing
-
-    try:
-        typing._eval_type = eval_type  # type: ignore
-        return _get_type_hints(obj, globalns, localns, include_extras)
-    finally:
-        typing._eval_type = _eval_type  # type: ignore
diff --git a/xarray_dataclasses/typing.py b/xarray_dataclasses/typing.py
index ad80865..9a40890 100644
--- a/xarray_dataclasses/typing.py
+++ b/xarray_dataclasses/typing.py
@@ -4,7 +4,16 @@ __all__ = ["Attr", "Coord", "Coordof", "Data", "Dataof", "Name"]
 # standard library
 from dataclasses import Field
 from enum import auto, Enum
-from typing import Any, Dict, Optional, Tuple, TypeVar, Union
+from itertools import chain
+from typing import (
+    Any,
+    Dict,
+    ForwardRef,
+    Optional,
+    Tuple,
+    TypeVar,
+    Union,
+)
 
 
 # dependencies
@@ -13,7 +22,6 @@ from typing_extensions import (
     Annotated,
     get_args,
     get_origin,
-    get_type_hints,
     Literal,
     Protocol,
     runtime_checkable,
@@ -269,71 +277,80 @@ Examples:
 
 
 # runtime functions
-def get_class(hint: Any) -> Any:
-    """Return a class parsed from a type hint."""
-    return get_first(unannotate(hint))
+def get_dims(obj: Any) -> Dims:
+    """Parse an object to get dims."""
+    obj = unannotate(obj)
+
+    if obj == ():
+        return ()
 
+    if obj is NoneType:
+        return ()
 
-def get_dims(hint: Any) -> Dims:
-    """Return dims parsed from a type hint."""
-    t_dims = get_args(get_class(hint))[0]
+    if isinstance(obj, str):
+        return (obj,)
 
-    if is_str_literal(t_dims):
-        return (get_first(t_dims),)
+    if isinstance(obj, ForwardRef):
+        return (obj.__forward_arg__,)
 
-    args = get_args(t_dims)
+    args = get_args(obj)
+    origin = get_origin(obj)
 
-    if args == () or args == ((),):
-        return ()
+    if origin is tuple:
+        return tuple(chain(*map(get_dims, args)))
 
-    if all(map(is_str_literal, args)):
-        return tuple(map(get_first, args))
+    if origin is Literal and len(args) == 1:
+        return (str(args[0]),)
 
-    raise ValueError(f"Could not parse dims from {hint!r}.")
+    raise ValueError(f"Could not parse {obj} as dims.")
 
 
-def get_dtype(hint: Any) -> Dtype:
-    """Return dtype parsed from a type hint."""
-    t_dtype = get_args(get_class(hint))[1]
+def get_dtype(obj: Any) -> Dtype:
+    """Parse an object to get dtype."""
+    obj = unannotate(obj)
 
-    if t_dtype is Any:
+    if obj is Any:
         return None
 
-    if t_dtype is NoneType:
+    if obj is NoneType:
         return None
 
-    if isinstance(t_dtype, type):
-        return t_dtype.__name__
-
-    if is_str_literal(t_dtype):
-        return get_first(t_dtype)
+    if isinstance(obj, str):
+        return obj
 
-    raise ValueError(f"Could not parse dtype from {hint!r}.")
+    if isinstance(obj, type):
+        return obj.__name__
 
+    if isinstance(obj, ForwardRef):
+        return obj.__forward_arg__
 
-def get_first(hint: Any) -> Any:
-    """Return the first argument in a type hint."""
-    return get_args(hint)[0]
+    args = get_args(obj)
+    origin = get_origin(obj)
 
+    if origin is Literal and len(args) == 1:
+        return str(args[0])
 
-def is_str_literal(hint: Any) -> bool:
-    """Check if a type hint is Literal[str]."""
-    args = get_args(hint)
-    origin = get_origin(hint)
+    raise ValueError(f"Could not parse {obj} as dtype.")
 
-    if origin is not Literal:
-        return False
 
-    if not len(args) == 1:
-        return False
+def unannotate(obj: T) -> T:
+    """Recursively remove Annotated types."""
+    import typing
 
-    return isinstance(args[0], str)
+    args = get_args(obj)
+    origin = get_origin(obj)
 
+    if origin is None:
+        return obj
 
-def unannotate(hint: Any) -> Any:
-    """Recursively remove Annotated type hints."""
+    if origin is Annotated:
+        return unannotate(args[0])
 
-    class Temp:
-        __annotations__ = dict(hint=hint)
+    args = map(unannotate, args)
+    args = tuple(filter(None, args))
 
-    return get_type_hints(Temp)["hint"]
+    try:
+        return origin[args]
+    except TypeError:
+        name = origin.__name__.capitalize()
+        return getattr(typing, name)[args]


Test patch:
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index 1a0f89c..7e6efe9 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -8,4 +8,4 @@ def test_author():
 
 
 def test_version():
-    assert xarray_dataclasses.__version__ == "0.5.0"
+    assert xarray_dataclasses.__version__ == "0.5.1"
diff --git a/tests/test_typing.py b/tests/test_typing.py
index 53f2e88..497a2ce 100644
--- a/tests/test_typing.py
+++ b/tests/test_typing.py
@@ -1,44 +1,60 @@
 # standard library
-from typing import Any, Tuple
+from typing import Any, ForwardRef, Tuple, Union
 
 
 # third-party packages
 from pytest import mark
-from typing_extensions import Literal
+from typing_extensions import Annotated, Literal
 
 
 # submodules
-from xarray_dataclasses.typing import Data, get_dims, get_dtype
+from xarray_dataclasses.typing import get_dims, get_dtype, unannotate
 
 
 # type hints
-Int = Literal["int"]
+x = ForwardRef("x")
+y = ForwardRef("y")
 X = Literal["x"]
 Y = Literal["y"]
 
 
 # test datasets
 testdata_dims = [
-    (Data[X, Any], ("x",)),
-    (Data[Tuple[()], Any], ()),
-    (Data[Tuple[X], Any], ("x",)),
-    (Data[Tuple[X, Y], Any], ("x", "y")),
+    (Tuple[()], ()),
+    (X, ("x",)),
+    (x, ("x",)),
+    (Tuple[X], ("x",)),
+    (Tuple[x], ("x",)),  # type: ignore
+    (Tuple[X, Y], ("x", "y")),
+    (Tuple[x, y], ("x", "y")),  # type: ignore
 ]
 
 testdata_dtype = [
-    (Data[X, Any], None),
-    (Data[X, None], None),
-    (Data[X, int], "int"),
-    (Data[X, Int], "int"),
+    (Any, None),
+    (int, "int"),
+    ("int", "int"),
+    (Literal["int"], "int"),
+]
+
+testdata_unannotate = [
+    (x, x),
+    (X, X),
+    (Annotated[X, 0], X),
+    (Union[Annotated[X, 0], Y], Union[X, Y]),
 ]
 
 
 # test functions
-@mark.parametrize("hint, dims", testdata_dims)
-def test_get_dims(hint: Any, dims: Any) -> None:
-    assert get_dims(hint) == dims
+@mark.parametrize("t_dims, dims", testdata_dims)
+def test_get_dims(t_dims: Any, dims: Any) -> None:
+    assert get_dims(t_dims) == dims
+
+
+@mark.parametrize("t_dtype, dtype", testdata_dtype)
+def test_get_dtype(t_dtype: Any, dtype: Any) -> None:
+    assert get_dtype(t_dtype) == dtype
 
 
-@mark.parametrize("hint, dtype", testdata_dtype)
-def test_get_dtype(hint: Any, dtype: Any) -> None:
-    assert get_dtype(hint) == dtype
+@mark.parametrize("t_before, t_after", testdata_unannotate)
+def test_unannotate(t_before: Any, t_after: Any) -> None:
+    assert unannotate(t_before) == t_after