Skip to content

feat(DRAFT): DType.__eq__ as a TypeIs[...]#2050

Closed
dangotbanned wants to merge 12 commits intomainfrom
dtype-eq-typeis
Closed

feat(DRAFT): DType.__eq__ as a TypeIs[...]#2050
dangotbanned wants to merge 12 commits intomainfrom
dtype-eq-typeis

Conversation

@dangotbanned
Copy link
Member

@dangotbanned dangotbanned commented Feb 19, 2025

What type of PR is this? (check all applicable)

  • 💾 Refactor
  • ✨ Feature
  • 🐛 Bug Fix
  • 🔧 Optimization
  • 📝 Documentation
  • ✅ Test
  • 🐳 Other

Related issues

Checklist

  • Code follows style guide (ruff)
  • Tests added
  • Documented the changes

If you have comments or can explain your changes, please do so below

Important

Seems that we can't represent this well in the current typing spec AFAICT.
See (dbadb09) message

https://typing.readthedocs.io/en/latest/spec/narrowing.html

Type checkers should assume that type narrowing should be applied to the expression that is passed as the first positional argument to a user-defined type guard.
If the type guard function accepts more than one argument, no type narrowing is applied to those additional argument expressions.

If a type guard function is implemented as an instance method or class method, the first positional argument maps to the second parameter (after “self” or “cls”).

So __eq__ currently describes the opposite of the spec:

import narwhals as nw

dtype = nw.Boolean()
second_parameter = nw.Int64

dtype == second_parameter
dtype.__eq__(second_parameter)

# Equivalent to
isinstance_or_issubclass(second_parameter, type(dtype))
isinstance_or_issubclass(nw.Int64, nw.Boolean)
issubclass(nw.Int64, nw.Boolean)

@dangotbanned dangotbanned added enhancement New feature or request typing labels Feb 19, 2025
# DTypes
dtypes = [
i for i in nw.dtypes.__dir__() if i[0].isupper() and not i.isupper() and i[0] != "_"
i for i in nw.dtypes.__all__ if i[0].isupper() and not i.isupper() and i[0] != "_"
Copy link
Member Author

Choose a reason for hiding this comment

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

FIXME

Need some help getting this work again
https://results.pre-commit.ci/run/github/760058710/1739990897.pkejAgUjQ3CdKYXvgIDqPw

I don't understand what the two checks are doing https://narwhals-dev.github.io/narwhals/api-reference/dtypes/#narwhals.dtypes

Or why this seems to include Literal?

BASE_DTYPES = {
"NumericType",
"DType",
"TemporalType",
"Literal",
"OrderedDict",
"Mapping",
}

`pyright` was yelling in `dtypes_test`
- I've played around with this for quite a while now
- I don't think there's a satisfying way to get `__eq__` working the same as `isinstance_or_issubclass`
- Generic `Datetime` & `Duration` are achievable
@dangotbanned
Copy link
Member Author

@MarcoGorelli I've added a little write-up for what I believe is the blocking issue.

Maybe this is a naive question, why was __eq__ chosen for this instead of __(instance|subclass)check__?
https://docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks

@dangotbanned
Copy link
Member Author

Closing as not planned

@MarcoGorelli
Copy link
Member

@MarcoGorelli I've added a little write-up for what I believe is the blocking issue.

Maybe this is a naive question, why was __eq__ chosen for this instead of __(instance|subclass)check__? https://docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks

thanks @dangotbanned for looking into this, sorry i missed it

we just mirrored what Polars does with regards to __eq__

@dangotbanned
Copy link
Member Author

@MarcoGorelli I've added a little write-up for what I believe is the blocking issue.
Maybe this is a naive question, why was __eq__ chosen for this instead of __(instance|subclass)check__? docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks

thanks @dangotbanned for looking into this, sorry i missed it

we just mirrored what Polars does with regards to __eq__

@MarcoGorelli
Oh wow, can't believe I didn't realise that 🤦‍♂️

All of this is near-identical to polars, including a comment 😉

https://github.com/pola-rs/polars/blob/0aa889bef845ed902c634f900d4ed703c2eb55aa/py-polars/polars/datatypes/classes.py#L528-L562

narwhals/narwhals/dtypes.py

Lines 335 to 367 in 2bcc6bb

def __init__(
self: Self,
time_unit: TimeUnit = "us",
time_zone: str | timezone | None = None,
) -> 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)
if isinstance(time_zone, timezone):
time_zone = str(time_zone)
self.time_unit: TimeUnit = time_unit
self.time_zone: str | None = time_zone
def __eq__(self: Self, other: object) -> bool:
# allow comparing object instances to 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
else: # pragma: no cover
return False
def __hash__(self: Self) -> int: # pragma: no cover
return hash((self.__class__, self.time_unit, self.time_zone))
def __repr__(self: Self) -> str: # pragma: no cover
class_name = self.__class__.__name__
return f"{class_name}(time_unit={self.time_unit!r}, time_zone={self.time_zone!r})"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked enhancement New feature or request typing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants