Skip to content

fix: Use metaclass for safe DType attr access #2025

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

Merged
merged 13 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 11 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
8 changes: 2 additions & 6 deletions narwhals/_arrow/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from narwhals._arrow.typing import Incomplete
from narwhals._arrow.typing import StringArray
from narwhals.dtypes import DType
from narwhals.typing import TimeUnit
from narwhals.typing import _AnyDArray
from narwhals.utils import Version

Expand Down Expand Up @@ -182,12 +181,9 @@ def narwhals_to_native_dtype(dtype: DType | type[DType], version: Version) -> pa
if isinstance_or_issubclass(dtype, dtypes.Categorical):
return pa.dictionary(pa.uint32(), pa.string())
if isinstance_or_issubclass(dtype, dtypes.Datetime):
time_unit: TimeUnit = getattr(dtype, "time_unit", "us")
time_zone = getattr(dtype, "time_zone", None)
return pa.timestamp(time_unit, tz=time_zone)
return pa.timestamp(dtype.time_unit, tz=dtype.time_zone) # type: ignore[arg-type]
if isinstance_or_issubclass(dtype, dtypes.Duration):
time_unit = getattr(dtype, "time_unit", "us")
return pa.duration(time_unit)
return pa.duration(dtype.time_unit)
if isinstance_or_issubclass(dtype, dtypes.Date):
return pa.date32()
if isinstance_or_issubclass(dtype, dtypes.List):
Expand Down
6 changes: 3 additions & 3 deletions narwhals/_duckdb/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,12 @@ def narwhals_to_native_dtype(dtype: DType | type[DType], version: Version) -> st
msg = "Categorical not supported by DuckDB"
raise NotImplementedError(msg)
if isinstance_or_issubclass(dtype, dtypes.Datetime):
_time_unit = getattr(dtype, "time_unit", "us")
_time_zone = getattr(dtype, "time_zone", None)
_time_unit = dtype.time_unit
_time_zone = dtype.time_zone
msg = "todo"
raise NotImplementedError(msg)
if isinstance_or_issubclass(dtype, dtypes.Duration): # pragma: no cover
_time_unit = getattr(dtype, "time_unit", "us")
_time_unit = dtype.time_unit
msg = "todo"
raise NotImplementedError(msg)
if isinstance_or_issubclass(dtype, dtypes.Date): # pragma: no cover
Expand Down
13 changes: 6 additions & 7 deletions narwhals/_pandas_like/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,27 +613,26 @@ def narwhals_to_native_dtype( # noqa: PLR0915
# convert to it?
return "category"
if isinstance_or_issubclass(dtype, dtypes.Datetime):
dt_time_unit = getattr(dtype, "time_unit", "us")
dt_time_zone = getattr(dtype, "time_zone", None)

# Pandas does not support "ms" or "us" time units before version 2.0
# Let's overwrite with "ns"
if implementation is Implementation.PANDAS and backend_version < (
2,
): # pragma: no cover
dt_time_unit = "ns"
else:
dt_time_unit = dtype.time_unit

if dtype_backend == "pyarrow":
tz_part = f", tz={dt_time_zone}" if dt_time_zone else ""
tz_part = f", tz={tz}" if (tz := dtype.time_zone) else ""
return f"timestamp[{dt_time_unit}{tz_part}][pyarrow]"
else:
tz_part = f", {dt_time_zone}" if dt_time_zone else ""
tz_part = f", {tz}" if (tz := dtype.time_zone) else ""
return f"datetime64[{dt_time_unit}{tz_part}]"
if isinstance_or_issubclass(dtype, dtypes.Duration):
du_time_unit = getattr(dtype, "time_unit", "us")
du_time_unit = dtype.time_unit
if implementation is Implementation.PANDAS and backend_version < (
2,
): # pragma: no cover
# NOTE: I think this assignment was a typo, it never gets used (should be du_)
dt_time_unit = "ns"
return (
f"duration[{du_time_unit}][pyarrow]"
Expand Down
12 changes: 5 additions & 7 deletions narwhals/_polars/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from narwhals.exceptions import NarwhalsError
from narwhals.exceptions import ShapeError
from narwhals.utils import import_dtypes_module
from narwhals.utils import isinstance_or_issubclass

if TYPE_CHECKING:
from narwhals._polars.dataframe import PolarsDataFrame
Expand Down Expand Up @@ -190,13 +191,10 @@ def narwhals_to_native_dtype(
if dtype == dtypes.Decimal:
msg = "Casting to Decimal is not supported yet."
raise NotImplementedError(msg)
if dtype == dtypes.Datetime or isinstance(dtype, dtypes.Datetime):
dt_time_unit: TimeUnit = getattr(dtype, "time_unit", "us")
dt_time_zone = getattr(dtype, "time_zone", None)
return pl.Datetime(dt_time_unit, dt_time_zone) # type: ignore[arg-type]
if dtype == dtypes.Duration or isinstance(dtype, dtypes.Duration):
du_time_unit: TimeUnit = getattr(dtype, "time_unit", "us")
return pl.Duration(time_unit=du_time_unit) # type: ignore[arg-type]
if isinstance_or_issubclass(dtype, dtypes.Datetime):
return pl.Datetime(dtype.time_unit, dtype.time_zone) # type: ignore[arg-type]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

time_unit="s" isn't allowed for polars, but we pass it through anyway?

if isinstance_or_issubclass(dtype, dtypes.Duration):
return pl.Duration(dtype.time_unit) # type: ignore[arg-type]
if dtype == dtypes.List:
return pl.List(narwhals_to_native_dtype(dtype.inner, version, backend_version)) # type: ignore[union-attr]
if dtype == dtypes.Struct:
Expand Down
2 changes: 1 addition & 1 deletion narwhals/_spark_like/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def narwhals_to_native_dtype(
if isinstance_or_issubclass(dtype, dtypes.Date):
return spark_types.DateType()
if isinstance_or_issubclass(dtype, dtypes.Datetime):
dt_time_zone = getattr(dtype, "time_zone", None)
dt_time_zone = dtype.time_zone
if dt_time_zone is None:
return spark_types.TimestampNTZType()
if dt_time_zone != "UTC": # pragma: no cover
Expand Down
33 changes: 23 additions & 10 deletions narwhals/dtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,17 @@ class Unknown(DType):
"""


class Datetime(TemporalType):
class _DatetimeMeta(type):
@property
def time_unit(cls) -> TimeUnit:
return "us"

@property
def time_zone(cls) -> str | None:
return None


class Datetime(TemporalType, metaclass=_DatetimeMeta):
"""Data type representing a calendar date and time of day.

Arguments:
Expand Down Expand Up @@ -505,11 +515,11 @@ def __init__(
time_zone = str(time_zone)

self.time_unit: TimeUnit = time_unit
self.time_zone = time_zone
self.time_zone: str | None = time_zone

def __eq__(self: Self, other: object) -> bool:
# allow comparing object instances to class
if type(other) is type and issubclass(other, self.__class__):
if type(other) is _DatetimeMeta:
return True
elif isinstance(other, self.__class__):
return self.time_unit == other.time_unit and self.time_zone == other.time_zone
Expand All @@ -524,7 +534,13 @@ def __repr__(self: Self) -> str: # pragma: no cover
return f"{class_name}(time_unit={self.time_unit!r}, time_zone={self.time_zone!r})"


class Duration(TemporalType):
class _DurationMeta(type):
@property
def time_unit(cls) -> TimeUnit:
return "us"


class Duration(TemporalType, metaclass=_DurationMeta):
"""Data type representing a time duration.

Arguments:
Expand Down Expand Up @@ -552,22 +568,19 @@ class Duration(TemporalType):
Duration(time_unit='ms')
"""

def __init__(
self: Self,
time_unit: TimeUnit = "us",
) -> None:
def __init__(self: Self, time_unit: TimeUnit = "us") -> None:
if time_unit not in ("s", "ms", "us", "ns"):
msg = (
"invalid `time_unit`"
f"\n\nExpected one of {{'ns','us','ms', 's'}}, got {time_unit!r}."
)
raise ValueError(msg)

self.time_unit = time_unit
self.time_unit: TimeUnit = time_unit

def __eq__(self: Self, other: object) -> bool:
# allow comparing object instances to class
if type(other) is type and issubclass(other, self.__class__):
if type(other) is _DurationMeta:
return True
elif isinstance(other, self.__class__):
return self.time_unit == other.time_unit
Expand Down
Loading