From 9b28d5a54b94e84660be6bd454086ee6375b148a Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 14 Jan 2025 13:58:35 +0100 Subject: [PATCH 01/23] docs: normalize examples --- src/safeds/data/tabular/containers/_cell.py | 605 ++++++++++-------- src/safeds/data/tabular/containers/_column.py | 11 +- 2 files changed, 328 insertions(+), 288 deletions(-) diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 877159940..1cc569e0b 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -21,6 +21,9 @@ _PythonLiteral: TypeAlias = _NumericLiteral | bool | str | bytes | _TemporalLiteral | None +# TODO: Rethink whether T_co should include None, also affects Cell operations ('<' return Cell[bool | None] etc.) + + class Cell(ABC, Generic[T_co]): """ A single value in a table. @@ -188,9 +191,15 @@ def __rtruediv__(self, other: Any) -> Cell[R_co]: ... @abstractmethod def __hash__(self) -> int: ... + @abstractmethod + def __repr__(self) -> str: ... + @abstractmethod def __sizeof__(self) -> int: ... + @abstractmethod + def __str__(self) -> str: ... + # ------------------------------------------------------------------------------------------------------------------ # Properties # ------------------------------------------------------------------------------------------------------------------ @@ -198,12 +207,44 @@ def __sizeof__(self) -> int: ... @property @abstractmethod def str(self) -> StringCell: - """Namespace for operations on strings.""" + """ + Namespace for operations on strings. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", ["hi", "hello"]) + >>> column.transform(lambda cell: cell.str.length()) + +-----+ + | a | + | --- | + | u32 | + +=====+ + | 2 | + | 5 | + +-----+ + """ @property @abstractmethod def dt(self) -> TemporalCell: - """Namespace for operations on date time values.""" + """ + Namespace for operations on temporal values. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [datetime.datetime(2025, 1, 1), datetime.datetime(2024, 1, 1)]) + >>> column.transform(lambda cell: cell.dt.year()) + +------+ + | a | + | --- | + | i32 | + +======+ + | 2025 | + | 2024 | + +------+ + """ # ------------------------------------------------------------------------------------------------------------------ # Boolean operations @@ -216,26 +257,26 @@ def not_(self) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [True, False]) + >>> column = Column("a", [True, False]) >>> column.transform(lambda cell: cell.not_()) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | true | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | true | + +-------+ >>> column.transform(lambda cell: ~cell) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | true | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | true | + +-------+ """ return self.__invert__() @@ -246,26 +287,26 @@ def and_(self, other: bool | Cell[bool]) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [True, False]) + >>> column = Column("a", [True, False]) >>> column.transform(lambda cell: cell.and_(False)) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | false | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | false | + +-------+ >>> column.transform(lambda cell: cell & False) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | false | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | false | + +-------+ """ return self.__and__(other) @@ -276,26 +317,26 @@ def or_(self, other: bool | Cell[bool]) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [True, False]) + >>> column = Column("a", [True, False]) >>> column.transform(lambda cell: cell.or_(True)) - +---------+ - | example | - | --- | - | bool | - +=========+ - | true | - | true | - +---------+ + +------+ + | a | + | --- | + | bool | + +======+ + | true | + | true | + +------+ >>> column.transform(lambda cell: cell | True) - +---------+ - | example | - | --- | - | bool | - +=========+ - | true | - | true | - +---------+ + +------+ + | a | + | --- | + | bool | + +======+ + | true | + | true | + +------+ """ return self.__or__(other) @@ -306,26 +347,26 @@ def xor(self, other: bool | Cell[bool]) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [True, False]) + >>> column = Column("a", [True, False]) >>> column.transform(lambda cell: cell.xor(True)) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | true | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | true | + +-------+ >>> column.transform(lambda cell: cell ^ True) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | true | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | true | + +-------+ """ return self.__xor__(other) @@ -340,16 +381,16 @@ def abs(self) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1, -2]) + >>> column = Column("a", [1, -2]) >>> column.transform(lambda cell: cell.abs()) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 1 | - | 2 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 1 | + | 2 | + +-----+ """ return self.__abs__() @@ -360,10 +401,10 @@ def ceil(self) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1.1, 2.9]) + >>> column = Column("a", [1.1, 2.9]) >>> column.transform(lambda cell: cell.ceil()) +---------+ - | example | + | a | | --- | | f64 | +=========+ @@ -380,10 +421,10 @@ def floor(self) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1.1, 2.9]) + >>> column = Column("a", [1.1, 2.9]) >>> column.transform(lambda cell: cell.floor()) +---------+ - | example | + | a | | --- | | f64 | +=========+ @@ -400,16 +441,16 @@ def neg(self) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1, -2]) + >>> column = Column("a", [1, -2]) >>> column.transform(lambda cell: cell.neg()) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | -1 | - | 2 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | -1 | + | 2 | + +-----+ """ return self.__neg__() @@ -420,26 +461,26 @@ def add(self, other: Any) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1, 2]) + >>> column = Column("a", [1, 2]) >>> column.transform(lambda cell: cell.add(3)) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 4 | - | 5 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 4 | + | 5 | + +-----+ >>> column.transform(lambda cell: cell + 3) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 4 | - | 5 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 4 | + | 5 | + +-----+ """ return self.__add__(other) @@ -450,10 +491,10 @@ def div(self, other: Any) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [6, 8]) + >>> column = Column("a", [6, 8]) >>> column.transform(lambda cell: cell.div(2)) +---------+ - | example | + | a | | --- | | f64 | +=========+ @@ -463,7 +504,7 @@ def div(self, other: Any) -> Cell[R_co]: >>> column.transform(lambda cell: cell / 2) +---------+ - | example | + | a | | --- | | f64 | +=========+ @@ -480,26 +521,26 @@ def mod(self, other: Any) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [5, 6]) + >>> column = Column("a", [5, 6]) >>> column.transform(lambda cell: cell.mod(3)) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 2 | - | 0 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 2 | + | 0 | + +-----+ >>> column.transform(lambda cell: cell % 3) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 2 | - | 0 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 2 | + | 0 | + +-----+ """ return self.__mod__(other) @@ -510,26 +551,26 @@ def mul(self, other: Any) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [2, 3]) + >>> column = Column("a", [2, 3]) >>> column.transform(lambda cell: cell.mul(4)) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 8 | - | 12 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 8 | + | 12 | + +-----+ >>> column.transform(lambda cell: cell * 4) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 8 | - | 12 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 8 | + | 12 | + +-----+ """ return self.__mul__(other) @@ -540,26 +581,26 @@ def pow(self, other: float | Cell[P_contra]) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [2, 3]) + >>> column = Column("a", [2, 3]) >>> column.transform(lambda cell: cell.pow(3)) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 8 | - | 27 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 8 | + | 27 | + +-----+ >>> column.transform(lambda cell: cell ** 3) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 8 | - | 27 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 8 | + | 27 | + +-----+ """ return self.__pow__(other) @@ -570,26 +611,26 @@ def sub(self, other: Any) -> Cell[R_co]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [5, 6]) + >>> column = Column("a", [5, 6]) >>> column.transform(lambda cell: cell.sub(3)) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 2 | - | 3 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 2 | + | 3 | + +-----+ >>> column.transform(lambda cell: cell - 3) - +---------+ - | example | - | --- | - | i64 | - +=========+ - | 2 | - | 3 | - +---------+ + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 2 | + | 3 | + +-----+ """ return self.__sub__(other) @@ -604,26 +645,26 @@ def eq(self, other: Any) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1, 2]) + >>> column = Column("a", [1, 2]) >>> column.transform(lambda cell: cell.eq(2)) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | true | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | true | + +-------+ >>> column.transform(lambda cell: cell == 2) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | true | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | true | + +-------+ """ return self.__eq__(other) @@ -634,26 +675,26 @@ def neq(self, other: Any) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1, 2]) + >>> column = Column("a", [1, 2]) >>> column.transform(lambda cell: cell.neq(2)) - +---------+ - | example | - | --- | - | bool | - +=========+ - | true | - | false | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | true | + | false | + +-------+ >>> column.transform(lambda cell: cell != 2) - +---------+ - | example | - | --- | - | bool | - +=========+ - | true | - | false | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | true | + | false | + +-------+ """ return self.__ne__(other) @@ -664,26 +705,26 @@ def ge(self, other: Any) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1, 2]) + >>> column = Column("a", [1, 2]) >>> column.transform(lambda cell: cell.ge(2)) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | true | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | true | + +-------+ >>> column.transform(lambda cell: cell >= 2) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | true | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | true | + +-------+ """ return self.__ge__(other) @@ -694,26 +735,26 @@ def gt(self, other: Any) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1, 2]) + >>> column = Column("a", [1, 2]) >>> column.transform(lambda cell: cell.gt(2)) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | false | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | false | + +-------+ >>> column.transform(lambda cell: cell > 2) - +---------+ - | example | - | --- | - | bool | - +=========+ - | false | - | false | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | false | + +-------+ """ return self.__gt__(other) @@ -724,26 +765,26 @@ def le(self, other: Any) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1, 2]) + >>> column = Column("a", [1, 2]) >>> column.transform(lambda cell: cell.le(2)) - +---------+ - | example | - | --- | - | bool | - +=========+ - | true | - | true | - +---------+ + +------+ + | a | + | --- | + | bool | + +======+ + | true | + | true | + +------+ >>> column.transform(lambda cell: cell <= 2) - +---------+ - | example | - | --- | - | bool | - +=========+ - | true | - | true | - +---------+ + +------+ + | a | + | --- | + | bool | + +======+ + | true | + | true | + +------+ """ return self.__le__(other) @@ -754,26 +795,26 @@ def lt(self, other: Any) -> Cell[bool]: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("example", [1, 2]) + >>> column = Column("a", [1, 2]) >>> column.transform(lambda cell: cell.lt(2)) - +---------+ - | example | - | --- | - | bool | - +=========+ - | true | - | false | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | true | + | false | + +-------+ >>> column.transform(lambda cell: cell < 2) - +---------+ - | example | - | --- | - | bool | - +=========+ - | true | - | false | - +---------+ + +-------+ + | a | + | --- | + | bool | + +=======+ + | true | + | false | + +-------+ """ return self.__lt__(other) diff --git a/src/safeds/data/tabular/containers/_column.py b/src/safeds/data/tabular/containers/_column.py index f26e7763e..0aaa7af33 100644 --- a/src/safeds/data/tabular/containers/_column.py +++ b/src/safeds/data/tabular/containers/_column.py @@ -19,9 +19,11 @@ from polars import Series from safeds.data.tabular.typing import ColumnType - from safeds.exceptions import ( - ColumnTypeError, # noqa: F401 - IndexOutOfBoundsError, # noqa: F401 + from safeds.exceptions import ( # noqa: F401 + ColumnTypeError, + IndexOutOfBoundsError, + LengthMismatchError, + MissingValuesError, ) from ._cell import Cell @@ -32,9 +34,6 @@ R_co = TypeVar("R_co", covariant=True) -# TODO: Rethink whether T_co should include None, also affects Cell operations ('<' return Cell[bool | None] etc.) - - class Column(Sequence[T_co]): """ A named, one-dimensional collection of homogeneous values. From ad84a510e6403e5730ee523a057911490001cbbd Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 14 Jan 2025 14:46:55 +0100 Subject: [PATCH 02/23] feat: create cell from date/time/datetime/duration --- src/safeds/_typing/__init__.py | 19 ++ src/safeds/data/tabular/containers/_cell.py | 218 +++++++++++++++++- .../tabular/containers/_column/test_eq.py | 2 +- .../containers/_column/test_transform.py | 4 +- .../_table/test_add_computed_column.py | 6 +- .../containers/_table/test_filter_rows.py | 4 +- .../_table/test_filter_rows_by_column.py | 2 +- .../containers/_table/test_remove_rows.py | 4 +- .../_table/test_remove_rows_by_column.py | 2 +- 9 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 src/safeds/_typing/__init__.py diff --git a/src/safeds/_typing/__init__.py b/src/safeds/_typing/__init__.py new file mode 100644 index 000000000..9d6e4fd4a --- /dev/null +++ b/src/safeds/_typing/__init__.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import datetime +from decimal import Decimal +from typing import TypeAlias + +from safeds.data.tabular.containers import Cell + +_NumericLiteral: TypeAlias = int | float | Decimal +_TemporalLiteral: TypeAlias = datetime.date | datetime.time | datetime.datetime | datetime.timedelta +_PythonLiteral: TypeAlias = _NumericLiteral | bool | str | bytes | _TemporalLiteral +_ConvertibleToCell: TypeAlias = _PythonLiteral | Cell | None + +__all__ = [ + "_ConvertibleToCell", + "_NumericLiteral", + "_PythonLiteral", + "_TemporalLiteral", +] diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 1cc569e0b..592ddaf32 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -1,13 +1,15 @@ from __future__ import annotations -import datetime from abc import ABC, abstractmethod -from decimal import Decimal -from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar if TYPE_CHECKING: + import datetime as python_datetime + import polars as pl + from safeds._typing import _ConvertibleToCell, _PythonLiteral + from ._string_cell import StringCell from ._temporal_cell import TemporalCell @@ -16,11 +18,6 @@ R_co = TypeVar("R_co", covariant=True) -_NumericLiteral: TypeAlias = int | float | Decimal -_TemporalLiteral: TypeAlias = datetime.date | datetime.time | datetime.datetime | datetime.timedelta -_PythonLiteral: TypeAlias = _NumericLiteral | bool | str | bytes | _TemporalLiteral | None - - # TODO: Rethink whether T_co should include None, also affects Cell operations ('<' return Cell[bool | None] etc.) @@ -36,9 +33,9 @@ class Cell(ABC, Generic[T_co]): # ------------------------------------------------------------------------------------------------------------------ @staticmethod - def from_literal(value: _PythonLiteral) -> Cell: + def constant(value: _PythonLiteral | None) -> Cell: """ - Create a new cell from a literal value. + Create a cell with a constant value. Parameters ---------- @@ -56,6 +53,192 @@ def from_literal(value: _PythonLiteral) -> Cell: return _LazyCell(pl.lit(value)) + @staticmethod + def date( + year: int | Cell[int], + month: int | Cell[int], + day: int | Cell[int], + ) -> Cell[python_datetime.date]: + """ + Create a cell with a date. + + Parameters + ---------- + year: + The year. + month: + The month. Must be between 1 and 12. + day: + The day. Must be between 1 and 31. + + Returns + ------- + cell: + The created cell. + """ + import polars as pl + + from ._lazy_cell import _LazyCell # circular import + + year = _to_polars_expression(year) + month = _to_polars_expression(month) + day = _to_polars_expression(day) + + return _LazyCell(pl.date(year, month, day)) + + @staticmethod + def datetime( + year: int | Cell[int], + month: int | Cell[int], + day: int | Cell[int], + *, + hour: int | Cell[int] = 0, + minute: int | Cell[int] = 0, + second: int | Cell[int] = 0, + microsecond: int | Cell[int] = 0, + ) -> Cell[python_datetime.datetime]: + """ + Create a cell with a datetime. + + Parameters + ---------- + year: + The year. + month: + The month. Must be between 1 and 12. + day: + The day. Must be between 1 and 31. + hour: + The hour. Must be between 0 and 23. + minute: + The minute. Must be between 0 and 59. + second: + The second. Must be between 0 and 59. + microsecond: + The microsecond. Must be between 0 and 999,999. + + Returns + ------- + cell: + The created cell. + """ + import polars as pl + + from ._lazy_cell import _LazyCell # circular import + + year = _to_polars_expression(year) + month = _to_polars_expression(month) + day = _to_polars_expression(day) + hour = _to_polars_expression(hour) + minute = _to_polars_expression(minute) + second = _to_polars_expression(second) + microsecond = _to_polars_expression(microsecond) + + return _LazyCell(pl.datetime(year, month, day, hour, minute, second, microsecond)) + + @staticmethod + def duration( + *, + weeks: int | Cell[int] = 0, + days: int | Cell[int] = 0, + hours: int | Cell[int] = 0, + minutes: int | Cell[int] = 0, + seconds: int | Cell[int] = 0, + milliseconds: int | Cell[int] = 0, + microseconds: int | Cell[int] = 0, + nanoseconds: int | Cell[int] = 0, + ) -> Cell[python_datetime.timedelta]: + """ + Create a cell with a duration. + + Parameters + ---------- + weeks: + The number of weeks. + days: + The number of days. + hours: + The number of hours. + minutes: + The number of minutes. + seconds: + The number of seconds. + milliseconds: + The number of milliseconds. + microseconds: + The number of microseconds. + nanoseconds: + The number of nanoseconds. + + Returns + ------- + cell: + The created cell. + """ + import polars as pl + + from ._lazy_cell import _LazyCell # circular import + + weeks = _to_polars_expression(weeks) + days = _to_polars_expression(days) + hours = _to_polars_expression(hours) + minutes = _to_polars_expression(minutes) + seconds = _to_polars_expression(seconds) + milliseconds = _to_polars_expression(milliseconds) + microseconds = _to_polars_expression(microseconds) + nanoseconds = _to_polars_expression(nanoseconds) + + return _LazyCell( + pl.duration( + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + milliseconds=milliseconds, + microseconds=microseconds, + nanoseconds=nanoseconds, + ), + ) + + @staticmethod + def time( + hour: int | Cell[int], + minute: int | Cell[int], + second: int | Cell[int], + *, + microsecond: int | Cell[int] = 0, + ) -> Cell[python_datetime.time]: + """ + Create a cell with a time. + + Parameters + ---------- + hour: + The hour. Must be between 0 and 23. + minute: + The minute. Must be between 0 and 59. + second: + The second. Must be between 0 and 59. + microsecond: + The microsecond. Must be between 0 and 999,999. + + Returns + ------- + cell: + The created cell. + """ + import polars as pl + + from ._lazy_cell import _LazyCell # circular import + + hour = _to_polars_expression(hour) + minute = _to_polars_expression(minute) + second = _to_polars_expression(second) + microsecond = _to_polars_expression(microsecond) + + return _LazyCell(pl.time(hour, minute, second, microsecond)) + @staticmethod def first_not_none(cells: list[Cell]) -> Cell: """ @@ -788,14 +971,14 @@ def le(self, other: Any) -> Cell[bool]: """ return self.__le__(other) - def lt(self, other: Any) -> Cell[bool]: + def lt(self, other: _ConvertibleToCell) -> Cell[bool | None]: """ Check if less than a value. This is equivalent to the `<` operator. Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1, 2]) + >>> column = Column("a", [1, 2, None]) >>> column.transform(lambda cell: cell.lt(2)) +-------+ | a | @@ -804,6 +987,7 @@ def lt(self, other: Any) -> Cell[bool]: +=======+ | true | | false | + | null | +-------+ >>> column.transform(lambda cell: cell < 2) @@ -814,6 +998,7 @@ def lt(self, other: Any) -> Cell[bool]: +=======+ | true | | false | + | null | +-------+ """ return self.__lt__(other) @@ -834,3 +1019,12 @@ def _equals(self, other: object) -> bool: This method is needed because the `__eq__` method is used for element-wise comparisons. """ + + +def _to_polars_expression(cell: _ConvertibleToCell) -> pl.Expr: + import polars as pl + + if isinstance(cell, Cell): + return cell._polars_expression + else: + return pl.lit(cell) diff --git a/tests/safeds/data/tabular/containers/_column/test_eq.py b/tests/safeds/data/tabular/containers/_column/test_eq.py index 1136a2c81..566d72a52 100644 --- a/tests/safeds/data/tabular/containers/_column/test_eq.py +++ b/tests/safeds/data/tabular/containers/_column/test_eq.py @@ -93,7 +93,7 @@ def test_should_return_true_if_objects_are_identical(column: Column) -> None: ("column", "other"), [ (Column("col1", []), None), - (Column("col1", []), Cell.from_literal(1)), + (Column("col1", []), Cell.constant(1)), ], ids=[ "Column vs. None", diff --git a/tests/safeds/data/tabular/containers/_column/test_transform.py b/tests/safeds/data/tabular/containers/_column/test_transform.py index 85821c849..e17ceb4c9 100644 --- a/tests/safeds/data/tabular/containers/_column/test_transform.py +++ b/tests/safeds/data/tabular/containers/_column/test_transform.py @@ -10,7 +10,7 @@ [ ( lambda: Column("col1", []), - lambda _: Cell.from_literal(None), + lambda _: Cell.constant(None), Column("col1", []), ), ( @@ -20,7 +20,7 @@ ), ( lambda: Column("col1", [1, 2, 3]), - lambda _: Cell.from_literal(None), + lambda _: Cell.constant(None), Column("col1", [None, None, None]), ), ( diff --git a/tests/safeds/data/tabular/containers/_table/test_add_computed_column.py b/tests/safeds/data/tabular/containers/_table/test_add_computed_column.py index c0920d5de..69e8a2373 100644 --- a/tests/safeds/data/tabular/containers/_table/test_add_computed_column.py +++ b/tests/safeds/data/tabular/containers/_table/test_add_computed_column.py @@ -12,19 +12,19 @@ ( lambda: Table({}), "col1", - lambda _: Cell.from_literal(None), + lambda _: Cell.constant(None), Table({"col1": []}), ), ( lambda: Table({"col1": []}), "col2", - lambda _: Cell.from_literal(None), + lambda _: Cell.constant(None), Table({"col1": [], "col2": []}), ), ( lambda: Table({"col1": [1, 2, 3]}), "col2", - lambda _: Cell.from_literal(None), + lambda _: Cell.constant(None), Table({"col1": [1, 2, 3], "col2": [None, None, None]}), ), ( diff --git a/tests/safeds/data/tabular/containers/_table/test_filter_rows.py b/tests/safeds/data/tabular/containers/_table/test_filter_rows.py index a53034e7b..61d474b02 100644 --- a/tests/safeds/data/tabular/containers/_table/test_filter_rows.py +++ b/tests/safeds/data/tabular/containers/_table/test_filter_rows.py @@ -10,12 +10,12 @@ [ ( lambda: Table({}), - lambda _: Cell.from_literal(False), # noqa: FBT003 + lambda _: Cell.constant(False), # noqa: FBT003 Table({}), ), ( lambda: Table({"col1": []}), - lambda _: Cell.from_literal(False), # noqa: FBT003 + lambda _: Cell.constant(False), # noqa: FBT003 Table({"col1": []}), ), ( diff --git a/tests/safeds/data/tabular/containers/_table/test_filter_rows_by_column.py b/tests/safeds/data/tabular/containers/_table/test_filter_rows_by_column.py index 56a7f4c5e..58fe61989 100644 --- a/tests/safeds/data/tabular/containers/_table/test_filter_rows_by_column.py +++ b/tests/safeds/data/tabular/containers/_table/test_filter_rows_by_column.py @@ -12,7 +12,7 @@ ( lambda: Table({"col1": [], "col2": []}), "col1", - lambda _: Cell.from_literal(False), # noqa: FBT003 + lambda _: Cell.constant(False), # noqa: FBT003 Table({"col1": [], "col2": []}), ), ( diff --git a/tests/safeds/data/tabular/containers/_table/test_remove_rows.py b/tests/safeds/data/tabular/containers/_table/test_remove_rows.py index 3bd524981..8979bb22f 100644 --- a/tests/safeds/data/tabular/containers/_table/test_remove_rows.py +++ b/tests/safeds/data/tabular/containers/_table/test_remove_rows.py @@ -10,12 +10,12 @@ [ ( lambda: Table({}), - lambda _: Cell.from_literal(False), # noqa: FBT003 + lambda _: Cell.constant(False), # noqa: FBT003 Table({}), ), ( lambda: Table({"col1": []}), - lambda _: Cell.from_literal(False), # noqa: FBT003 + lambda _: Cell.constant(False), # noqa: FBT003 Table({"col1": []}), ), ( diff --git a/tests/safeds/data/tabular/containers/_table/test_remove_rows_by_column.py b/tests/safeds/data/tabular/containers/_table/test_remove_rows_by_column.py index 6b3061bb9..c757cc7de 100644 --- a/tests/safeds/data/tabular/containers/_table/test_remove_rows_by_column.py +++ b/tests/safeds/data/tabular/containers/_table/test_remove_rows_by_column.py @@ -12,7 +12,7 @@ ( lambda: Table({"col1": [], "col2": []}), "col1", - lambda _: Cell.from_literal(False), # noqa: FBT003 + lambda _: Cell.constant(False), # noqa: FBT003 Table({"col1": [], "col2": []}), ), ( From e3abb1aaaf403ab047ed92fda975d700c5649fcd Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 14 Jan 2025 19:09:43 +0100 Subject: [PATCH 03/23] feat: cast cells --- src/safeds/data/tabular/containers/_cell.py | 22 +++++++++++++++++++ .../data/tabular/containers/_lazy_cell.py | 8 +++++++ 2 files changed, 30 insertions(+) diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 592ddaf32..0ce018ae9 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -3,6 +3,8 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Generic, TypeVar +from ..typing import ColumnType + if TYPE_CHECKING: import datetime as python_datetime @@ -1003,6 +1005,26 @@ def lt(self, other: _ConvertibleToCell) -> Cell[bool | None]: """ return self.__lt__(other) + # ------------------------------------------------------------------------------------------------------------------ + # Other + # ------------------------------------------------------------------------------------------------------------------ + + @abstractmethod + def cast(self, type_: ColumnType) -> Cell: + """ + Cast the cell to a different type. + + Parameters + ---------- + type_: + The type to cast to. + + Returns + ------- + cell: + The cast cell. + """ + # ------------------------------------------------------------------------------------------------------------------ # Internal # ------------------------------------------------------------------------------------------------------------------ diff --git a/src/safeds/data/tabular/containers/_lazy_cell.py b/src/safeds/data/tabular/containers/_lazy_cell.py index 0e1703c73..6af744edb 100644 --- a/src/safeds/data/tabular/containers/_lazy_cell.py +++ b/src/safeds/data/tabular/containers/_lazy_cell.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, TypeVar from safeds._utils import _structural_hash +from safeds.data.tabular.typing import ColumnType from ._cell import Cell @@ -199,6 +200,13 @@ def dt(self) -> TemporalCell: return _LazyTemporalCell(self._expression) + # ------------------------------------------------------------------------------------------------------------------ + # Other + # ------------------------------------------------------------------------------------------------------------------ + + def cast(self, type_: ColumnType) -> Cell: + return _wrap(self._expression.cast(type_._polars_data_type)) + # ------------------------------------------------------------------------------------------------------------------ # Internal # ------------------------------------------------------------------------------------------------------------------ From a79c0ea8b69291f8566990bf0a8b7e712e03cd49 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 14 Jan 2025 19:24:22 +0100 Subject: [PATCH 04/23] feat: catch errors when collecting polars lazy frames --- src/safeds/_utils/__init__.py | 5 ++ src/safeds/_utils/_lazy.py | 62 +++++++++++++++++++ src/safeds/data/tabular/containers/_table.py | 32 ++++++---- .../data/tabular/plotting/_table_plotter.py | 9 +-- .../tabular/transformation/_range_scaler.py | 6 +- .../tabular/transformation/_robust_scaler.py | 7 ++- .../tabular/transformation/_simple_imputer.py | 6 +- .../transformation/_standard_scaler.py | 5 +- src/safeds/exceptions/__init__.py | 5 ++ tests/safeds/_utils/test_lazy.py | 17 +++++ 10 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 src/safeds/_utils/_lazy.py create mode 100644 tests/safeds/_utils/test_lazy.py diff --git a/src/safeds/_utils/__init__.py b/src/safeds/_utils/__init__.py index 83e31d841..fc3d501d4 100644 --- a/src/safeds/_utils/__init__.py +++ b/src/safeds/_utils/__init__.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from ._collections import _compute_duplicates from ._hashing import _structural_hash + from ._lazy import _safe_collect_lazy_frame, _safe_collect_lazy_frame_schema from ._plotting import _figure_to_image from ._random import _get_random_seed @@ -15,6 +16,8 @@ { "_compute_duplicates": "._collections:_compute_duplicates", "_structural_hash": "._hashing:_structural_hash", + "_safe_collect_lazy_frame": "._lazy:_safe_collect_lazy_frame", + "_safe_collect_lazy_frame_schema": "._lazy:_safe_collect_lazy_frame_schema", "_figure_to_image": "._plotting:_figure_to_image", "_get_random_seed": "._random:_get_random_seed", }, @@ -24,5 +27,7 @@ "_compute_duplicates", "_figure_to_image", "_get_random_seed", + "_safe_collect_lazy_frame", + "_safe_collect_lazy_frame_schema", "_structural_hash", ] diff --git a/src/safeds/_utils/_lazy.py b/src/safeds/_utils/_lazy.py new file mode 100644 index 000000000..92e443508 --- /dev/null +++ b/src/safeds/_utils/_lazy.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from safeds.exceptions import LazyComputationError + +if TYPE_CHECKING: + import polars as pl + + +def _safe_collect_lazy_frame(frame: pl.LazyFrame) -> pl.DataFrame: + """ + Collect a LazyFrame into a DataFrame and raise a custom error if an error occurs. + + Parameters + ---------- + frame: + The LazyFrame to collect. + + Returns + ------- + frame: + The collected DataFrame. + + Raises + ------ + LazyComputationError + If an error occurs during the computation. + """ + from polars.exceptions import PolarsError + + try: + return frame.collect() + except PolarsError as e: + raise LazyComputationError(str(e)) from None + + +def _safe_collect_lazy_frame_schema(frame: pl.LazyFrame) -> pl.Schema: + """ + Collect the schema of a LazyFrame. + + Parameters + ---------- + frame: + The LazyFrame to collect the schema of. + + Returns + ------- + schema: + The collected schema. + + Raises + ------ + LazyComputationError + If an error occurs during the computation. + """ + from polars.exceptions import PolarsError + + try: + return frame.collect_schema() + except PolarsError as e: + raise LazyComputationError(str(e)) from None diff --git a/src/safeds/data/tabular/containers/_table.py b/src/safeds/data/tabular/containers/_table.py index 0e8df2ef7..34eb8fa0c 100644 --- a/src/safeds/data/tabular/containers/_table.py +++ b/src/safeds/data/tabular/containers/_table.py @@ -4,7 +4,12 @@ from safeds._config import _get_device, _init_default_device from safeds._config._polars import _get_polars_config -from safeds._utils import _compute_duplicates, _structural_hash +from safeds._utils import ( + _compute_duplicates, + _safe_collect_lazy_frame, + _safe_collect_lazy_frame_schema, + _structural_hash, +) from safeds._validation import ( _check_bounds, _check_columns_dont_exist, @@ -388,7 +393,7 @@ def __str__(self) -> str: @property def _data_frame(self) -> pl.DataFrame: if self.__data_frame_cache is None: - self.__data_frame_cache = self._lazy_frame.collect() + self.__data_frame_cache = _safe_collect_lazy_frame(self._lazy_frame) return self.__data_frame_cache @@ -470,7 +475,9 @@ def schema(self) -> Schema: 'b': int64 }) """ - return Schema._from_polars_schema(self._lazy_frame.collect_schema()) + return Schema._from_polars_schema( + _safe_collect_lazy_frame_schema(self._lazy_frame), + ) # ------------------------------------------------------------------------------------------------------------------ # Column operations @@ -716,7 +723,7 @@ def get_column(self, name: str) -> Column: """ _check_columns_exist(self, name) return Column._from_polars_series( - self._lazy_frame.select(name).collect().get_column(name), + _safe_collect_lazy_frame(self._lazy_frame.select(name)).get_column(name), ) def get_column_type(self, name: str) -> ColumnType: @@ -1329,7 +1336,7 @@ def count_rows_if( None """ expression = predicate(_LazyVectorizedRow(self))._polars_expression - series = self._lazy_frame.select(expression.alias("count")).collect().get_column("count") + series = _safe_collect_lazy_frame(self._lazy_frame.select(expression.alias("count"))).get_column("count") if ignore_unknown or series.null_count() == 0: return series.sum() @@ -1747,14 +1754,17 @@ def remove_rows_with_outliers( # polar's `all_horizontal` raises a `ComputeError` if there are no columns selected = self._lazy_frame.select(cs.numeric() & cs.by_name(selector)) - if not selected.collect_schema().names(): + selected_names = _safe_collect_lazy_frame_schema(selected).names() + if not selected_names: return self # Multiply z-score by standard deviation instead of dividing the distance by it, to avoid division by zero non_outlier_mask = pl.all_horizontal( - selected.select( - pl.all().is_null() | ((pl.all() - pl.all().mean()).abs() <= (z_score_threshold * pl.all().std())), - ).collect(), + _safe_collect_lazy_frame( + selected.select( + pl.all().is_null() | ((pl.all() - pl.all().mean()).abs() <= (z_score_threshold * pl.all().std())), + ), + ), ) return Table._from_polars_lazy_frame( @@ -2360,7 +2370,7 @@ def join( # Can be removed once https://github.com/pola-rs/polars/issues/20670 is fixed if mode == "right" and len(left_names) > 1: # We must collect because of https://github.com/pola-rs/polars/issues/20671 - result = result.collect().drop(left_names).lazy() + result = _safe_collect_lazy_frame(result).drop(left_names).lazy() return self._from_polars_lazy_frame( result, @@ -2495,7 +2505,7 @@ def summarize_statistics(self) -> Table: # Compute suitable types for the output columns frame = self._lazy_frame - schema = frame.collect_schema() + schema = _safe_collect_lazy_frame_schema(frame) for name, type_ in schema.items(): # polars fails to determine supertype of temporal types and u32 if not type_.is_numeric() and not type_.is_(pl.Null): diff --git a/src/safeds/data/tabular/plotting/_table_plotter.py b/src/safeds/data/tabular/plotting/_table_plotter.py index ba9f53825..3eee20bbf 100644 --- a/src/safeds/data/tabular/plotting/_table_plotter.py +++ b/src/safeds/data/tabular/plotting/_table_plotter.py @@ -3,7 +3,7 @@ import warnings from typing import TYPE_CHECKING -from safeds._utils import _figure_to_image +from safeds._utils import _figure_to_image, _safe_collect_lazy_frame from safeds._validation import _check_bounds, _check_columns_are_numeric, _check_columns_exist, _ClosedBound from safeds.exceptions import ColumnTypeError, NonNumericColumnError @@ -380,7 +380,8 @@ def line_plot( agg_list.append(pl.col(name).mean().alias(f"{name}_mean")) agg_list.append(pl.count(name).alias(f"{name}_count")) agg_list.append(pl.std(name, ddof=0).alias(f"{name}_std")) - grouped = self._table._lazy_frame.sort(x_name).group_by(x_name, maintain_order=True).agg(agg_list).collect() + grouped_lazy = self._table._lazy_frame.sort(x_name).group_by(x_name, maintain_order=True).agg(agg_list) + grouped = _safe_collect_lazy_frame(grouped_lazy) x = grouped.get_column(x_name) y_s = [] @@ -575,8 +576,8 @@ def moving_average_plot( # Calculate the moving average mean_col = pl.col(y_name).mean().alias(y_name) - grouped = self._table._lazy_frame.sort(x_name).group_by(x_name, maintain_order=True).agg(mean_col).collect() - data = grouped + grouped_lazy = self._table._lazy_frame.sort(x_name).group_by(x_name, maintain_order=True).agg(mean_col) + data = _safe_collect_lazy_frame(grouped_lazy) moving_average = data.select([pl.col(y_name).rolling_mean(window_size).alias("moving_average")]) # set up the arrays for plotting y_data_with_nan = moving_average["moving_average"].to_numpy() diff --git a/src/safeds/data/tabular/transformation/_range_scaler.py b/src/safeds/data/tabular/transformation/_range_scaler.py index b025939ba..54def0379 100644 --- a/src/safeds/data/tabular/transformation/_range_scaler.py +++ b/src/safeds/data/tabular/transformation/_range_scaler.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from safeds._utils import _structural_hash +from safeds._utils import _safe_collect_lazy_frame, _structural_hash from safeds._validation import _check_columns_are_numeric, _check_columns_exist from safeds.data.tabular.containers import Table from safeds.exceptions import NotFittedError @@ -117,8 +117,8 @@ def fit(self, table: Table) -> RangeScaler: raise ValueError("The RangeScaler cannot be fitted because the table contains 0 rows") # Learn the transformation - _data_min = table._lazy_frame.select(column_names).min().collect() - _data_max = table._lazy_frame.select(column_names).max().collect() + _data_min = _safe_collect_lazy_frame(table._lazy_frame.select(column_names).min()) + _data_max = _safe_collect_lazy_frame(table._lazy_frame.select(column_names).max()) # Create a copy with the learned transformation result = RangeScaler(min_=self._min, max_=self._max, selector=column_names) diff --git a/src/safeds/data/tabular/transformation/_robust_scaler.py b/src/safeds/data/tabular/transformation/_robust_scaler.py index a0565c72f..9d390872b 100644 --- a/src/safeds/data/tabular/transformation/_robust_scaler.py +++ b/src/safeds/data/tabular/transformation/_robust_scaler.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING +from safeds._utils import _safe_collect_lazy_frame from safeds._validation import _check_columns_are_numeric, _check_columns_exist from safeds.data.tabular.containers import Table from safeds.exceptions import NotFittedError @@ -90,9 +91,9 @@ def fit(self, table: Table) -> RobustScaler: if table.row_count == 0: raise ValueError("The RobustScaler cannot be fitted because the table contains 0 rows") - _data_median = table._lazy_frame.select(column_names).median().collect() - q1 = table._lazy_frame.select(column_names).quantile(0.25).collect() - q3 = table._lazy_frame.select(column_names).quantile(0.75).collect() + _data_median = _safe_collect_lazy_frame(table._lazy_frame.select(column_names).median()) + q1 = _safe_collect_lazy_frame(table._lazy_frame.select(column_names).quantile(0.25)) + q3 = _safe_collect_lazy_frame(table._lazy_frame.select(column_names).quantile(0.75)) _data_scale = q3 - q1 # To make sure there is no division by zero diff --git a/src/safeds/data/tabular/transformation/_simple_imputer.py b/src/safeds/data/tabular/transformation/_simple_imputer.py index 41639677d..c152c2d40 100644 --- a/src/safeds/data/tabular/transformation/_simple_imputer.py +++ b/src/safeds/data/tabular/transformation/_simple_imputer.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from typing import Any -from safeds._utils import _structural_hash +from safeds._utils import _safe_collect_lazy_frame, _structural_hash from safeds._validation import _check_columns_are_numeric, _check_columns_exist from safeds.data.tabular.containers import Table from safeds.exceptions import NotFittedError @@ -276,7 +276,7 @@ def __str__(self) -> str: return "Mean" def _get_replacement(self, table: Table) -> dict[str, Any]: - return table._lazy_frame.mean().collect().to_dict() + return _safe_collect_lazy_frame(table._lazy_frame.mean()).to_dict() class _Median(SimpleImputer.Strategy): @@ -292,7 +292,7 @@ def __str__(self) -> str: return "Median" def _get_replacement(self, table: Table) -> dict[str, Any]: - return table._lazy_frame.median().collect().to_dict() + return _safe_collect_lazy_frame(table._lazy_frame.median()).to_dict() class _Mode(SimpleImputer.Strategy): diff --git a/src/safeds/data/tabular/transformation/_standard_scaler.py b/src/safeds/data/tabular/transformation/_standard_scaler.py index 5db98dade..8e71cf0ce 100644 --- a/src/safeds/data/tabular/transformation/_standard_scaler.py +++ b/src/safeds/data/tabular/transformation/_standard_scaler.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING +from safeds._utils import _safe_collect_lazy_frame from safeds._validation import _check_columns_are_numeric, _check_columns_exist from safeds.data.tabular.containers import Table from safeds.exceptions import NotFittedError @@ -86,8 +87,8 @@ def fit(self, table: Table) -> StandardScaler: raise ValueError("The StandardScaler cannot be fitted because the table contains 0 rows") # Learn the transformation (ddof=0 is used to match the behavior of scikit-learn) - _data_mean = table._lazy_frame.select(column_names).mean().collect() - _data_standard_deviation = table._lazy_frame.select(column_names).std(ddof=0).collect() + _data_mean = _safe_collect_lazy_frame(table._lazy_frame.select(column_names).mean()) + _data_standard_deviation = _safe_collect_lazy_frame(table._lazy_frame.select(column_names).std(ddof=0)) # Create a copy with the learned transformation result = StandardScaler(selector=column_names) diff --git a/src/safeds/exceptions/__init__.py b/src/safeds/exceptions/__init__.py index f6dfaf87c..578cc4ddc 100644 --- a/src/safeds/exceptions/__init__.py +++ b/src/safeds/exceptions/__init__.py @@ -56,6 +56,10 @@ class IndexOutOfBoundsError(IndexError): """Raised when trying to access an invalid index.""" +class LazyComputationError(SafeDsError, RuntimeError): + """Raised when a lazy computation fails.""" + + class LengthMismatchError(SafeDsError, ValueError): """Raised when objects have different lengths.""" @@ -93,6 +97,7 @@ class SchemaError(SafeDsError, TypeError): "DuplicateColumnError", "FileExtensionError", "IndexOutOfBoundsError", + "LazyComputationError", "LengthMismatchError", "MissingValuesError", "NotFittedError", diff --git a/tests/safeds/_utils/test_lazy.py b/tests/safeds/_utils/test_lazy.py new file mode 100644 index 000000000..0cba7d88f --- /dev/null +++ b/tests/safeds/_utils/test_lazy.py @@ -0,0 +1,17 @@ +import polars as pl +import pytest + +from safeds._utils import _safe_collect_lazy_frame, _safe_collect_lazy_frame_schema +from safeds.exceptions import LazyComputationError + + +def test_safe_collect_lazy_frame() -> None: + frame = pl.LazyFrame().select("a") + with pytest.raises(LazyComputationError): + _safe_collect_lazy_frame(frame) + + +def test_safe_collect_lazy_frame_schema() -> None: + frame = pl.LazyFrame().select("a") + with pytest.raises(LazyComputationError): + _safe_collect_lazy_frame_schema(frame) From 5fa05aa9c91648fcb792503870596328f12abdee Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 14 Jan 2025 20:19:16 +0100 Subject: [PATCH 05/23] chore: improve type hints --- src/safeds/_typing/__init__.py | 6 + src/safeds/data/tabular/containers/_cell.py | 133 +++++++++--------- src/safeds/data/tabular/containers/_column.py | 25 ++-- .../data/tabular/containers/_lazy_cell.py | 91 ++++++------ src/safeds/data/tabular/containers/_table.py | 15 +- .../tabular/containers/_lazy_cell/test_lt.py | 4 + 6 files changed, 140 insertions(+), 134 deletions(-) diff --git a/src/safeds/_typing/__init__.py b/src/safeds/_typing/__init__.py index 9d6e4fd4a..2ae84e575 100644 --- a/src/safeds/_typing/__init__.py +++ b/src/safeds/_typing/__init__.py @@ -10,8 +10,14 @@ _TemporalLiteral: TypeAlias = datetime.date | datetime.time | datetime.datetime | datetime.timedelta _PythonLiteral: TypeAlias = _NumericLiteral | bool | str | bytes | _TemporalLiteral _ConvertibleToCell: TypeAlias = _PythonLiteral | Cell | None +_BooleanCell: TypeAlias = Cell[bool | None] +# We cannot restrict `Cell`, because `Row.get_cell` returns a `Cell[Any]`. +_ConvertibleToBooleanCell: TypeAlias = bool | Cell | None + __all__ = [ + "_BooleanCell", + "_ConvertibleToBooleanCell", "_ConvertibleToCell", "_NumericLiteral", "_PythonLiteral", diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 0ce018ae9..1c28af2b4 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -1,26 +1,21 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Generic, TypeVar - -from ..typing import ColumnType +from typing import TYPE_CHECKING, Generic, TypeVar if TYPE_CHECKING: import datetime as python_datetime import polars as pl - from safeds._typing import _ConvertibleToCell, _PythonLiteral + from safeds._typing import _BooleanCell, _ConvertibleToBooleanCell, _ConvertibleToCell, _PythonLiteral + from safeds.data.tabular.typing import ColumnType from ._string_cell import StringCell from ._temporal_cell import TemporalCell T_co = TypeVar("T_co", covariant=True) -P_contra = TypeVar("P_contra", contravariant=True) -R_co = TypeVar("R_co", covariant=True) - - -# TODO: Rethink whether T_co should include None, also affects Cell operations ('<' return Cell[bool | None] etc.) +P = TypeVar("P") class Cell(ABC, Generic[T_co]): @@ -60,7 +55,7 @@ def date( year: int | Cell[int], month: int | Cell[int], day: int | Cell[int], - ) -> Cell[python_datetime.date]: + ) -> Cell[python_datetime.date | None]: """ Create a cell with a date. @@ -98,7 +93,7 @@ def datetime( minute: int | Cell[int] = 0, second: int | Cell[int] = 0, microsecond: int | Cell[int] = 0, - ) -> Cell[python_datetime.datetime]: + ) -> Cell[python_datetime.datetime | None]: """ Create a cell with a datetime. @@ -149,7 +144,7 @@ def duration( milliseconds: int | Cell[int] = 0, microseconds: int | Cell[int] = 0, nanoseconds: int | Cell[int] = 0, - ) -> Cell[python_datetime.timedelta]: + ) -> Cell[python_datetime.timedelta | None]: """ Create a cell with a duration. @@ -210,7 +205,7 @@ def time( second: int | Cell[int], *, microsecond: int | Cell[int] = 0, - ) -> Cell[python_datetime.time]: + ) -> Cell[python_datetime.time | None]: """ Create a cell with a time. @@ -242,9 +237,9 @@ def time( return _LazyCell(pl.time(hour, minute, second, microsecond)) @staticmethod - def first_not_none(cells: list[Cell]) -> Cell: + def first_not_none(cells: list[Cell[P]]) -> Cell[P | None]: """ - Return the first cell from the given list that is not None. + Return the first cell that is not None or None if all cells are None. Parameters ---------- @@ -254,8 +249,7 @@ def first_not_none(cells: list[Cell]) -> Cell: Returns ------- cell: - Returns the contents of the first cell that is not None. If all cells in the list are None or the list is - empty returns None. + The first cell that is not None or None if all cells are None. """ import polars as pl @@ -270,106 +264,106 @@ def first_not_none(cells: list[Cell]) -> Cell: # "Boolean" operators (actually bitwise) ----------------------------------- @abstractmethod - def __invert__(self) -> Cell[bool]: ... + def __invert__(self) -> _BooleanCell: ... @abstractmethod - def __and__(self, other: bool | Cell[bool]) -> Cell[bool]: ... + def __and__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: ... @abstractmethod - def __rand__(self, other: bool | Cell[bool]) -> Cell[bool]: ... + def __rand__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: ... @abstractmethod - def __or__(self, other: bool | Cell[bool]) -> Cell[bool]: ... + def __or__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: ... @abstractmethod - def __ror__(self, other: bool | Cell[bool]) -> Cell[bool]: ... + def __ror__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: ... @abstractmethod - def __xor__(self, other: bool | Cell[bool]) -> Cell[bool]: ... + def __xor__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: ... @abstractmethod - def __rxor__(self, other: bool | Cell[bool]) -> Cell[bool]: ... + def __rxor__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: ... # Comparison --------------------------------------------------------------- @abstractmethod - def __eq__(self, other: object) -> Cell[bool]: # type: ignore[override] + def __eq__(self, other: _ConvertibleToCell) -> _BooleanCell: # type: ignore[override] ... @abstractmethod - def __ge__(self, other: Any) -> Cell[bool]: ... + def __ge__(self, other: _ConvertibleToCell) -> _BooleanCell: ... @abstractmethod - def __gt__(self, other: Any) -> Cell[bool]: ... + def __gt__(self, other: _ConvertibleToCell) -> _BooleanCell: ... @abstractmethod - def __le__(self, other: Any) -> Cell[bool]: ... + def __le__(self, other: _ConvertibleToCell) -> _BooleanCell: ... @abstractmethod - def __lt__(self, other: Any) -> Cell[bool]: ... + def __lt__(self, other: _ConvertibleToCell) -> _BooleanCell: ... @abstractmethod - def __ne__(self, other: object) -> Cell[bool]: # type: ignore[override] + def __ne__(self, other: _ConvertibleToCell) -> _BooleanCell: # type: ignore[override] ... # Numeric operators -------------------------------------------------------- @abstractmethod - def __abs__(self) -> Cell[R_co]: ... + def __abs__(self) -> Cell: ... @abstractmethod - def __ceil__(self) -> Cell[R_co]: ... + def __ceil__(self) -> Cell: ... @abstractmethod - def __floor__(self) -> Cell[R_co]: ... + def __floor__(self) -> Cell: ... @abstractmethod - def __neg__(self) -> Cell[R_co]: ... + def __neg__(self) -> Cell: ... @abstractmethod - def __pos__(self) -> Cell[R_co]: ... + def __pos__(self) -> Cell: ... @abstractmethod - def __add__(self, other: Any) -> Cell[R_co]: ... + def __add__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __radd__(self, other: Any) -> Cell[R_co]: ... + def __radd__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __floordiv__(self, other: Any) -> Cell[R_co]: ... + def __floordiv__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __rfloordiv__(self, other: Any) -> Cell[R_co]: ... + def __rfloordiv__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __mod__(self, other: Any) -> Cell[R_co]: ... + def __mod__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __rmod__(self, other: Any) -> Cell[R_co]: ... + def __rmod__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __mul__(self, other: Any) -> Cell[R_co]: ... + def __mul__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __rmul__(self, other: Any) -> Cell[R_co]: ... + def __rmul__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __pow__(self, other: float | Cell[P_contra]) -> Cell[R_co]: ... + def __pow__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __rpow__(self, other: float | Cell[P_contra]) -> Cell[R_co]: ... + def __rpow__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __sub__(self, other: Any) -> Cell[R_co]: ... + def __sub__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __rsub__(self, other: Any) -> Cell[R_co]: ... + def __rsub__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __truediv__(self, other: Any) -> Cell[R_co]: ... + def __truediv__(self, other: _ConvertibleToCell) -> Cell: ... @abstractmethod - def __rtruediv__(self, other: Any) -> Cell[R_co]: ... + def __rtruediv__(self, other: _ConvertibleToCell) -> Cell: ... # Other -------------------------------------------------------------------- @@ -418,6 +412,7 @@ def dt(self) -> TemporalCell: Examples -------- + >>> import datetime >>> from safeds.data.tabular.containers import Column >>> column = Column("a", [datetime.datetime(2025, 1, 1), datetime.datetime(2024, 1, 1)]) >>> column.transform(lambda cell: cell.dt.year()) @@ -435,7 +430,7 @@ def dt(self) -> TemporalCell: # Boolean operations # ------------------------------------------------------------------------------------------------------------------ - def not_(self) -> Cell[bool]: + def not_(self) -> _BooleanCell: """ Negate a boolean. This is equivalent to the `~` operator. @@ -465,7 +460,7 @@ def not_(self) -> Cell[bool]: """ return self.__invert__() - def and_(self, other: bool | Cell[bool]) -> Cell[bool]: + def and_(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: """ Perform a boolean AND operation. This is equivalent to the `&` operator. @@ -495,7 +490,7 @@ def and_(self, other: bool | Cell[bool]) -> Cell[bool]: """ return self.__and__(other) - def or_(self, other: bool | Cell[bool]) -> Cell[bool]: + def or_(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: """ Perform a boolean OR operation. This is equivalent to the `|` operator. @@ -525,7 +520,7 @@ def or_(self, other: bool | Cell[bool]) -> Cell[bool]: """ return self.__or__(other) - def xor(self, other: bool | Cell[bool]) -> Cell[bool]: + def xor(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: """ Perform a boolean XOR operation. This is equivalent to the `^` operator. @@ -559,7 +554,7 @@ def xor(self, other: bool | Cell[bool]) -> Cell[bool]: # Numeric operations # ------------------------------------------------------------------------------------------------------------------ - def abs(self) -> Cell[R_co]: + def abs(self) -> Cell: """ Get the absolute value. @@ -579,7 +574,7 @@ def abs(self) -> Cell[R_co]: """ return self.__abs__() - def ceil(self) -> Cell[R_co]: + def ceil(self) -> Cell: """ Round up to the nearest integer. @@ -599,7 +594,7 @@ def ceil(self) -> Cell[R_co]: """ return self.__ceil__() - def floor(self) -> Cell[R_co]: + def floor(self) -> Cell: """ Round down to the nearest integer. @@ -619,7 +614,7 @@ def floor(self) -> Cell[R_co]: """ return self.__floor__() - def neg(self) -> Cell[R_co]: + def neg(self) -> Cell: """ Negate the value. @@ -639,7 +634,7 @@ def neg(self) -> Cell[R_co]: """ return self.__neg__() - def add(self, other: Any) -> Cell[R_co]: + def add(self, other: _ConvertibleToCell) -> Cell: """ Add a value. This is equivalent to the `+` operator. @@ -669,7 +664,7 @@ def add(self, other: Any) -> Cell[R_co]: """ return self.__add__(other) - def div(self, other: Any) -> Cell[R_co]: + def div(self, other: _ConvertibleToCell) -> Cell: """ Divide by a value. This is equivalent to the `/` operator. @@ -699,7 +694,7 @@ def div(self, other: Any) -> Cell[R_co]: """ return self.__truediv__(other) - def mod(self, other: Any) -> Cell[R_co]: + def mod(self, other: _ConvertibleToCell) -> Cell: """ Perform a modulo operation. This is equivalent to the `%` operator. @@ -729,7 +724,7 @@ def mod(self, other: Any) -> Cell[R_co]: """ return self.__mod__(other) - def mul(self, other: Any) -> Cell[R_co]: + def mul(self, other: _ConvertibleToCell) -> Cell: """ Multiply by a value. This is equivalent to the `*` operator. @@ -759,7 +754,7 @@ def mul(self, other: Any) -> Cell[R_co]: """ return self.__mul__(other) - def pow(self, other: float | Cell[P_contra]) -> Cell[R_co]: + def pow(self, other: _ConvertibleToCell) -> Cell: """ Raise to a power. This is equivalent to the `**` operator. @@ -789,7 +784,7 @@ def pow(self, other: float | Cell[P_contra]) -> Cell[R_co]: """ return self.__pow__(other) - def sub(self, other: Any) -> Cell[R_co]: + def sub(self, other: _ConvertibleToCell) -> Cell: """ Subtract a value. This is equivalent to the `-` operator. @@ -823,7 +818,7 @@ def sub(self, other: Any) -> Cell[R_co]: # Comparison operations # ------------------------------------------------------------------------------------------------------------------ - def eq(self, other: Any) -> Cell[bool]: + def eq(self, other: _ConvertibleToCell) -> _BooleanCell: """ Check if equal to a value. This is equivalent to the `==` operator. @@ -853,7 +848,7 @@ def eq(self, other: Any) -> Cell[bool]: """ return self.__eq__(other) - def neq(self, other: Any) -> Cell[bool]: + def neq(self, other: _ConvertibleToCell) -> _BooleanCell: """ Check if not equal to a value. This is equivalent to the `!=` operator. @@ -883,7 +878,7 @@ def neq(self, other: Any) -> Cell[bool]: """ return self.__ne__(other) - def ge(self, other: Any) -> Cell[bool]: + def ge(self, other: _ConvertibleToCell) -> _BooleanCell: """ Check if greater than or equal to a value. This is equivalent to the `>=` operator. @@ -913,7 +908,7 @@ def ge(self, other: Any) -> Cell[bool]: """ return self.__ge__(other) - def gt(self, other: Any) -> Cell[bool]: + def gt(self, other: _ConvertibleToCell) -> _BooleanCell: """ Check if greater than a value. This is equivalent to the `>` operator. @@ -943,7 +938,7 @@ def gt(self, other: Any) -> Cell[bool]: """ return self.__gt__(other) - def le(self, other: Any) -> Cell[bool]: + def le(self, other: _ConvertibleToCell) -> _BooleanCell: """ Check if less than or equal to a value. This is equivalent to the `<=` operator. @@ -973,7 +968,7 @@ def le(self, other: Any) -> Cell[bool]: """ return self.__le__(other) - def lt(self, other: _ConvertibleToCell) -> Cell[bool | None]: + def lt(self, other: _ConvertibleToCell) -> _BooleanCell: """ Check if less than a value. This is equivalent to the `<` operator. diff --git a/src/safeds/data/tabular/containers/_column.py b/src/safeds/data/tabular/containers/_column.py index 0aaa7af33..6e4f86fc1 100644 --- a/src/safeds/data/tabular/containers/_column.py +++ b/src/safeds/data/tabular/containers/_column.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from polars import Series + from safeds._typing import _BooleanCell from safeds.data.tabular.typing import ColumnType from safeds.exceptions import ( # noqa: F401 ColumnTypeError, @@ -324,7 +325,7 @@ def get_value(self, index: int) -> T_co: @overload def all( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: Literal[True] = ..., ) -> bool: ... @@ -332,14 +333,14 @@ def all( @overload def all( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: bool, ) -> bool | None: ... def all( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: bool = True, ) -> bool | None: @@ -400,7 +401,7 @@ def all( @overload def any( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: Literal[True] = ..., ) -> bool: ... @@ -408,14 +409,14 @@ def any( @overload def any( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: bool, ) -> bool | None: ... def any( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: bool = True, ) -> bool | None: @@ -476,7 +477,7 @@ def any( @overload def count_if( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: Literal[True] = ..., ) -> int: ... @@ -484,14 +485,14 @@ def count_if( @overload def count_if( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: bool, ) -> int | None: ... def count_if( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: bool = True, ) -> int | None: @@ -546,7 +547,7 @@ def count_if( @overload def none( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: Literal[True] = ..., ) -> bool: ... @@ -554,14 +555,14 @@ def none( @overload def none( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: bool, ) -> bool | None: ... def none( self, - predicate: Callable[[Cell[T_co]], Cell[bool]], + predicate: Callable[[Cell[T_co]], _BooleanCell], *, ignore_unknown: bool = True, ) -> bool | None: diff --git a/src/safeds/data/tabular/containers/_lazy_cell.py b/src/safeds/data/tabular/containers/_lazy_cell.py index 6af744edb..8e1b48219 100644 --- a/src/safeds/data/tabular/containers/_lazy_cell.py +++ b/src/safeds/data/tabular/containers/_lazy_cell.py @@ -3,19 +3,19 @@ from typing import TYPE_CHECKING, Any, TypeVar from safeds._utils import _structural_hash -from safeds.data.tabular.typing import ColumnType from ._cell import Cell if TYPE_CHECKING: import polars as pl + from safeds._typing import _BooleanCell, _ConvertibleToBooleanCell, _ConvertibleToCell + from safeds.data.tabular.typing import ColumnType + from ._string_cell import StringCell from ._temporal_cell import TemporalCell T = TypeVar("T") -P = TypeVar("P") -R = TypeVar("R") class _LazyCell(Cell[T]): @@ -34,156 +34,154 @@ def __init__(self, expression: pl.Expr) -> None: # "Boolean" operators (actually bitwise) ----------------------------------- - def __invert__(self) -> Cell[bool]: + def __invert__(self) -> _BooleanCell: import polars as pl return _wrap(self._expression.cast(pl.Boolean).__invert__()) - def __and__(self, other: bool | Cell[bool]) -> Cell[bool]: + def __and__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__and__(other)) - def __rand__(self, other: bool | Cell[bool]) -> Cell[bool]: + def __rand__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__rand__(other)) - def __or__(self, other: bool | Cell[bool]) -> Cell[bool]: + def __or__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__or__(other)) - def __ror__(self, other: bool | Cell[bool]) -> Cell[bool]: + def __ror__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__ror__(other)) - def __xor__(self, other: bool | Cell[bool]) -> Cell[bool]: + def __xor__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__xor__(other)) - def __rxor__(self, other: bool | Cell[bool]) -> Cell[bool]: + def __rxor__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__rxor__(other)) # Comparison --------------------------------------------------------------- - def __eq__(self, other: object) -> Cell[bool]: # type: ignore[override] + def __eq__(self, other: _ConvertibleToCell) -> _BooleanCell: # type: ignore[override] other = _unwrap(other) return _wrap(self._expression.eq_missing(other)) - def __ge__(self, other: Any) -> Cell[bool]: + def __ge__(self, other: _ConvertibleToCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__ge__(other)) - def __gt__(self, other: Any) -> Cell[bool]: + def __gt__(self, other: _ConvertibleToCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__gt__(other)) - def __le__(self, other: Any) -> Cell[bool]: + def __le__(self, other: _ConvertibleToCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__le__(other)) - def __lt__(self, other: Any) -> Cell[bool]: + def __lt__(self, other: _ConvertibleToCell) -> _BooleanCell: other = _unwrap(other) return _wrap(self._expression.__lt__(other)) - def __ne__(self, other: object) -> Cell[bool]: # type: ignore[override] + def __ne__(self, other: _ConvertibleToCell) -> _BooleanCell: # type: ignore[override] other = _unwrap(other) return _wrap(self._expression.ne_missing(other)) # Numeric operators -------------------------------------------------------- - def __abs__(self) -> Cell[R]: + def __abs__(self) -> Cell: return _wrap(self._expression.__abs__()) - def __ceil__(self) -> Cell[R]: + def __ceil__(self) -> Cell: import polars as pl # polars does not yet implement floor for integers return _wrap(self._expression.cast(pl.Float64).ceil()) - def __floor__(self) -> Cell[R]: + def __floor__(self) -> Cell: import polars as pl # polars does not yet implement floor for integers return _wrap(self._expression.cast(pl.Float64).floor()) - def __neg__(self) -> Cell[R]: + def __neg__(self) -> Cell: return _wrap(self._expression.__neg__()) - def __pos__(self) -> Cell[R]: + def __pos__(self) -> Cell: return _wrap(self._expression.__pos__()) - def __add__(self, other: Any) -> Cell[R]: + def __add__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__add__(other)) - def __radd__(self, other: Any) -> Cell[R]: + def __radd__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__radd__(other)) - def __floordiv__(self, other: Any) -> Cell[R]: + def __floordiv__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__floordiv__(other)) - def __rfloordiv__(self, other: Any) -> Cell[R]: + def __rfloordiv__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__rfloordiv__(other)) - def __mod__(self, other: Any) -> Cell[R]: + def __mod__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__mod__(other)) - def __rmod__(self, other: Any) -> Cell[R]: + def __rmod__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__rmod__(other)) - def __mul__(self, other: Any) -> Cell[R]: + def __mul__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__mul__(other)) - def __rmul__(self, other: Any) -> Cell[R]: + def __rmul__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__rmul__(other)) - def __pow__(self, other: float | Cell[P]) -> Cell[R]: + def __pow__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__pow__(other)) - def __rpow__(self, other: float | Cell[P]) -> Cell[R]: + def __rpow__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__rpow__(other)) - def __sub__(self, other: Any) -> Cell[R]: + def __sub__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__sub__(other)) - def __rsub__(self, other: Any) -> Cell[R]: + def __rsub__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__rsub__(other)) - def __truediv__(self, other: Any) -> Cell[R]: + def __truediv__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__truediv__(other)) - def __rtruediv__(self, other: Any) -> Cell[R]: + def __rtruediv__(self, other: _ConvertibleToCell) -> Cell: other = _unwrap(other) return _wrap(self._expression.__rtruediv__(other)) - # String representation ---------------------------------------------------- - - def __repr__(self) -> str: - return self._expression.__repr__() - - def __str__(self) -> str: - return self._expression.__str__() - # Other -------------------------------------------------------------------- def __hash__(self) -> int: return _structural_hash(self._expression.meta.serialize()) + def __repr__(self) -> str: + return self._expression.__repr__() + def __sizeof__(self) -> int: return self._expression.__sizeof__() + def __str__(self) -> str: + return self._expression.__str__() + # ------------------------------------------------------------------------------------------------------------------ # Properties # ------------------------------------------------------------------------------------------------------------------ @@ -227,7 +225,8 @@ def _wrap(other: pl.Expr) -> Any: return _LazyCell(other) -def _unwrap(other: Any) -> Any: - if isinstance(other, _LazyCell): - return other._expression +def _unwrap(other: _ConvertibleToCell) -> Any: + if isinstance(other, Cell): + return other._polars_expression + return other diff --git a/src/safeds/data/tabular/containers/_table.py b/src/safeds/data/tabular/containers/_table.py index 34eb8fa0c..e3c23f0f9 100644 --- a/src/safeds/data/tabular/containers/_table.py +++ b/src/safeds/data/tabular/containers/_table.py @@ -40,6 +40,7 @@ from torch import Tensor from torch.utils.data import DataLoader, Dataset + from safeds._typing import _BooleanCell from safeds.data.labeled.containers import TabularDataset from safeds.data.tabular.transformation import ( InvertibleTableTransformer, @@ -1279,7 +1280,7 @@ def transform_columns( @overload def count_rows_if( self, - predicate: Callable[[Row], Cell[bool]], + predicate: Callable[[Row], _BooleanCell], *, ignore_unknown: Literal[True] = ..., ) -> int: ... @@ -1287,14 +1288,14 @@ def count_rows_if( @overload def count_rows_if( self, - predicate: Callable[[Row], Cell[bool]], + predicate: Callable[[Row], _BooleanCell], *, ignore_unknown: bool, ) -> int | None: ... def count_rows_if( self, - predicate: Callable[[Row], Cell[bool]], + predicate: Callable[[Row], _BooleanCell], *, ignore_unknown: bool = True, ) -> int | None: @@ -1345,7 +1346,7 @@ def count_rows_if( def filter_rows( self, - predicate: Callable[[Row], Cell[bool]], + predicate: Callable[[Row], _BooleanCell], ) -> Table: """ Keep only rows that satisfy a condition and return the result as a new table. @@ -1392,7 +1393,7 @@ def filter_rows( def filter_rows_by_column( self, name: str, - predicate: Callable[[Cell], Cell[bool]], + predicate: Callable[[Cell], _BooleanCell], ) -> Table: """ Keep only rows that satisfy a condition on a specific column and return the result as a new table. @@ -1487,7 +1488,7 @@ def remove_duplicate_rows(self) -> Table: def remove_rows( self, - predicate: Callable[[Row], Cell[bool]], + predicate: Callable[[Row], _BooleanCell], ) -> Table: """ Remove rows that satisfy a condition and return the result as a new table. @@ -1539,7 +1540,7 @@ def remove_rows( def remove_rows_by_column( self, name: str, - predicate: Callable[[Cell], Cell[bool]], + predicate: Callable[[Cell], _BooleanCell], ) -> Table: """ Remove rows that satisfy a condition on a specific column and return the result as a new table. diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py index 45280e2cd..7ff979437 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py @@ -12,12 +12,16 @@ (3, 1.5, False), (1.5, 3, True), (1.5, 1.5, False), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeLessThan: From 9cb8a193450ef8794250f3193960eb37eb9f3cda Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 14 Jan 2025 20:43:52 +0100 Subject: [PATCH 06/23] feat: by default, `None` propagates now in `eq` and `neq` --- src/safeds/data/tabular/containers/_cell.py | 24 +++++++++++++------ .../data/tabular/containers/_lazy_cell.py | 4 ++-- .../tabular/containers/_lazy_cell/test_eq.py | 21 +++++++++++----- .../tabular/containers/_lazy_cell/test_ge.py | 21 +++++++++++----- .../tabular/containers/_lazy_cell/test_gt.py | 21 +++++++++++----- .../tabular/containers/_lazy_cell/test_le.py | 21 +++++++++++----- .../tabular/containers/_lazy_cell/test_lt.py | 17 ++++++++----- .../_lazy_cell/{test_ne.py => test_neq.py} | 21 +++++++++++----- 8 files changed, 105 insertions(+), 45 deletions(-) rename tests/safeds/data/tabular/containers/_lazy_cell/{test_ne.py => test_neq.py} (75%) diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 1c28af2b4..b63d27870 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -820,12 +820,12 @@ def sub(self, other: _ConvertibleToCell) -> Cell: def eq(self, other: _ConvertibleToCell) -> _BooleanCell: """ - Check if equal to a value. This is equivalent to the `==` operator. + Check if equal to a value. The default behavior is equivalent to the `==` operator. Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1, 2]) + >>> column = Column("a", [1, 2, None]) >>> column.transform(lambda cell: cell.eq(2)) +-------+ | a | @@ -834,6 +834,7 @@ def eq(self, other: _ConvertibleToCell) -> _BooleanCell: +=======+ | false | | true | + | null | +-------+ >>> column.transform(lambda cell: cell == 2) @@ -844,18 +845,19 @@ def eq(self, other: _ConvertibleToCell) -> _BooleanCell: +=======+ | false | | true | + | null | +-------+ """ return self.__eq__(other) def neq(self, other: _ConvertibleToCell) -> _BooleanCell: """ - Check if not equal to a value. This is equivalent to the `!=` operator. + Check if not equal to a value. The default behavior is equivalent to the `!=` operator. Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1, 2]) + >>> column = Column("a", [1, 2, None]) >>> column.transform(lambda cell: cell.neq(2)) +-------+ | a | @@ -864,6 +866,7 @@ def neq(self, other: _ConvertibleToCell) -> _BooleanCell: +=======+ | true | | false | + | null | +-------+ >>> column.transform(lambda cell: cell != 2) @@ -874,6 +877,7 @@ def neq(self, other: _ConvertibleToCell) -> _BooleanCell: +=======+ | true | | false | + | null | +-------+ """ return self.__ne__(other) @@ -885,7 +889,7 @@ def ge(self, other: _ConvertibleToCell) -> _BooleanCell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1, 2]) + >>> column = Column("a", [1, 2, None]) >>> column.transform(lambda cell: cell.ge(2)) +-------+ | a | @@ -894,6 +898,7 @@ def ge(self, other: _ConvertibleToCell) -> _BooleanCell: +=======+ | false | | true | + | null | +-------+ >>> column.transform(lambda cell: cell >= 2) @@ -904,6 +909,7 @@ def ge(self, other: _ConvertibleToCell) -> _BooleanCell: +=======+ | false | | true | + | null | +-------+ """ return self.__ge__(other) @@ -915,7 +921,7 @@ def gt(self, other: _ConvertibleToCell) -> _BooleanCell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1, 2]) + >>> column = Column("a", [1, 2, None]) >>> column.transform(lambda cell: cell.gt(2)) +-------+ | a | @@ -924,6 +930,7 @@ def gt(self, other: _ConvertibleToCell) -> _BooleanCell: +=======+ | false | | false | + | null | +-------+ >>> column.transform(lambda cell: cell > 2) @@ -934,6 +941,7 @@ def gt(self, other: _ConvertibleToCell) -> _BooleanCell: +=======+ | false | | false | + | null | +-------+ """ return self.__gt__(other) @@ -945,7 +953,7 @@ def le(self, other: _ConvertibleToCell) -> _BooleanCell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1, 2]) + >>> column = Column("a", [1, 2, None]) >>> column.transform(lambda cell: cell.le(2)) +------+ | a | @@ -954,6 +962,7 @@ def le(self, other: _ConvertibleToCell) -> _BooleanCell: +======+ | true | | true | + | null | +------+ >>> column.transform(lambda cell: cell <= 2) @@ -964,6 +973,7 @@ def le(self, other: _ConvertibleToCell) -> _BooleanCell: +======+ | true | | true | + | null | +------+ """ return self.__le__(other) diff --git a/src/safeds/data/tabular/containers/_lazy_cell.py b/src/safeds/data/tabular/containers/_lazy_cell.py index 8e1b48219..e76daa65f 100644 --- a/src/safeds/data/tabular/containers/_lazy_cell.py +++ b/src/safeds/data/tabular/containers/_lazy_cell.py @@ -67,7 +67,7 @@ def __rxor__(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: def __eq__(self, other: _ConvertibleToCell) -> _BooleanCell: # type: ignore[override] other = _unwrap(other) - return _wrap(self._expression.eq_missing(other)) + return _wrap(self._expression.__eq__(other)) def __ge__(self, other: _ConvertibleToCell) -> _BooleanCell: other = _unwrap(other) @@ -87,7 +87,7 @@ def __lt__(self, other: _ConvertibleToCell) -> _BooleanCell: def __ne__(self, other: _ConvertibleToCell) -> _BooleanCell: # type: ignore[override] other = _unwrap(other) - return _wrap(self._expression.ne_missing(other)) + return _wrap(self._expression.__ne__(other)) # Numeric operators -------------------------------------------------------- diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py index 0cd8e0b02..25656a89e 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py @@ -12,29 +12,38 @@ (3, 1.5, False), (1.5, 3, False), (1.5, 1.5, True), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeEquality: - def test_dunder_method(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell == value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell == _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value2, lambda cell: value1 == cell, expected) # type: ignore[arg-type,return-value] - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) == cell, expected) # type: ignore[arg-type,return-value] - def test_named_method(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.eq(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.eq(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_ge.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_ge.py index 2269066c7..a4b488cff 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_ge.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_ge.py @@ -12,29 +12,38 @@ (3, 1.5, True), (1.5, 3, False), (1.5, 1.5, True), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeGreaterThanOrEqual: - def test_dunder_method(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell >= value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell >= _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value2, lambda cell: value1 >= cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) >= cell, expected) - def test_named_method(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.ge(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.ge(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_gt.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_gt.py index 4c39978d3..80bd44630 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_gt.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_gt.py @@ -12,29 +12,38 @@ (3, 1.5, True), (1.5, 3, False), (1.5, 1.5, False), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeGreaterThan: - def test_dunder_method(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell > value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell > _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value2, lambda cell: value1 > cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) > cell, expected) - def test_named_method(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.gt(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.gt(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_le.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_le.py index 2f4e43806..e1fcc5ca6 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_le.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_le.py @@ -12,29 +12,38 @@ (3, 1.5, False), (1.5, 3, True), (1.5, 1.5, True), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeLessThanOrEqual: - def test_dunder_method(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell <= value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell <= _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value2, lambda cell: value1 <= cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) <= cell, expected) - def test_named_method(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.le(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.le(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py index 7ff979437..1daef2437 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py @@ -25,20 +25,25 @@ ], ) class TestShouldComputeLessThan: - def test_dunder_method(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell < value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell < _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value2, lambda cell: value1 < cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) < cell, expected) - def test_named_method(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.lt(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.lt(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_ne.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py similarity index 75% rename from tests/safeds/data/tabular/containers/_lazy_cell/test_ne.py rename to tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py index be55a9c41..b537d404c 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_ne.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py @@ -12,29 +12,38 @@ (3, 1.5, True), (1.5, 3, True), (1.5, 1.5, False), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeNegatedEquality: - def test_dunder_method(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell != value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell != _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: value2 != cell, expected) # type: ignore[arg-type,return-value] - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: _LazyCell(pl.lit(value2)) != cell, expected) # type: ignore[arg-type,return-value] - def test_named_method(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.neq(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool) -> None: + def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.neq(_LazyCell(pl.lit(value2))), expected) From 9f57ce07980f8473da344b7e190a9cbdb9ec75b6 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 14 Jan 2025 21:16:59 +0100 Subject: [PATCH 07/23] feat: add parameter to control whether missing values should be propagated --- src/safeds/data/tabular/containers/_cell.py | 63 +++++++++++++++++-- .../data/tabular/containers/_lazy_cell.py | 19 ++++++ .../tabular/containers/_lazy_cell/test_eq.py | 35 +++++++++++ .../tabular/containers/_lazy_cell/test_neq.py | 35 +++++++++++ 4 files changed, 148 insertions(+), 4 deletions(-) diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index b63d27870..769c5cf26 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -818,10 +818,24 @@ def sub(self, other: _ConvertibleToCell) -> Cell: # Comparison operations # ------------------------------------------------------------------------------------------------------------------ - def eq(self, other: _ConvertibleToCell) -> _BooleanCell: + @abstractmethod + def eq( + self, + other: _ConvertibleToCell, + *, + propagate_missing_values: bool = True, + ) -> _BooleanCell: """ Check if equal to a value. The default behavior is equivalent to the `==` operator. + Missing values (indicated by `None`) are handled as follows: + + - If `propagate_missing_values` is `True` (default), the result will be a missing value if either the cell or + the other value is a missing value. Here, `None == None` is `None`. The intuition is that we do not know the + result of the comparison if we do not know the values, which is consistent with the other cell operations. + - If `propagate_missing_values` is `False`, `None` will be treated as a regular value. Here, `None == None` + is `True`. This behavior is useful, if you want to work with missing values, e.g. to filter them out. + Examples -------- >>> from safeds.data.tabular.containers import Column @@ -847,13 +861,44 @@ def eq(self, other: _ConvertibleToCell) -> _BooleanCell: | true | | null | +-------+ + + >>> column.transform(lambda cell: cell.eq(2, propagate_missing_values=False)) + +-------+ + | a | + | --- | + | bool | + +=======+ + | false | + | true | + | false | + +-------+ """ - return self.__eq__(other) - def neq(self, other: _ConvertibleToCell) -> _BooleanCell: + @abstractmethod + def neq( + self, + other: _ConvertibleToCell, + *, + propagate_missing_values: bool = True, + ) -> _BooleanCell: """ Check if not equal to a value. The default behavior is equivalent to the `!=` operator. + Missing values (indicated by `None`) are handled as follows: + + - If `propagate_missing_values` is `True` (default), the result will be a missing value if either the cell or + the other value is a missing value. Here, `None != None` is `None`. The intuition is that we do not know the + result of the comparison if we do not know the values, which is consistent with the other cell operations. + - If `propagate_missing_values` is `False`, `None` will be treated as a regular value. Here, `None != None` + is `False`. This behavior is useful, if you want to work with missing values, e.g. to filter them out. + + Parameters + ---------- + other: + The value to compare to. + propagate_missing_values: + Whether to propagate missing values. + Examples -------- >>> from safeds.data.tabular.containers import Column @@ -879,8 +924,18 @@ def neq(self, other: _ConvertibleToCell) -> _BooleanCell: | false | | null | +-------+ + + >>> column.transform(lambda cell: cell.neq(2, propagate_missing_values=False)) + +-------+ + | a | + | --- | + | bool | + +=======+ + | true | + | false | + | true | + +-------+ """ - return self.__ne__(other) def ge(self, other: _ConvertibleToCell) -> _BooleanCell: """ diff --git a/src/safeds/data/tabular/containers/_lazy_cell.py b/src/safeds/data/tabular/containers/_lazy_cell.py index e76daa65f..aaf9a0a9e 100644 --- a/src/safeds/data/tabular/containers/_lazy_cell.py +++ b/src/safeds/data/tabular/containers/_lazy_cell.py @@ -198,6 +198,25 @@ def dt(self) -> TemporalCell: return _LazyTemporalCell(self._expression) + # ------------------------------------------------------------------------------------------------------------------ + # Comparison operations + # ------------------------------------------------------------------------------------------------------------------ + def eq(self, other: _ConvertibleToCell, *, propagate_missing_values: bool = True) -> _BooleanCell: + other = _unwrap(other) + + if propagate_missing_values: + return _wrap(self._expression.eq(other)) + else: + return _wrap(self._expression.eq_missing(other)) + + def neq(self, other: _ConvertibleToCell, *, propagate_missing_values: bool = True) -> _BooleanCell: + other = _unwrap(other) + + if propagate_missing_values: + return _wrap(self._expression.ne(other)) + else: + return _wrap(self._expression.ne_missing(other)) + # ------------------------------------------------------------------------------------------------------------------ # Other # ------------------------------------------------------------------------------------------------------------------ diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py index 25656a89e..219b4e97f 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py @@ -47,3 +47,38 @@ def test_named_method(self, value1: float, value2: float, expected: bool | None) def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.eq(_LazyCell(pl.lit(value2))), expected) + + +@pytest.mark.parametrize( + ("value1", "value2", "expected"), + [ + (None, 3, False), + (3, None, False), + (None, None, True), + ], + ids=[ + "left is None", + "right is None", + "both are None", + ], +) +class TestShouldComputeEqualityWithoutPropagatingMissingValues: + def test_named_method( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: + assert_cell_operation_works(value1, lambda cell: cell.eq(value2, propagate_missing_values=False), expected) + + def test_named_method_wrapped_in_cell( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell.eq(_LazyCell(pl.lit(value2)), propagate_missing_values=False), + expected, + ) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py index b537d404c..36cde010f 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py @@ -47,3 +47,38 @@ def test_named_method(self, value1: float, value2: float, expected: bool | None) def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.neq(_LazyCell(pl.lit(value2))), expected) + + +@pytest.mark.parametrize( + ("value1", "value2", "expected"), + [ + (None, 3, True), + (3, None, True), + (None, None, False), + ], + ids=[ + "left is None", + "right is None", + "both are None", + ], +) +class TestShouldComputeNegatedEqualityWithoutPropagatingMissingValues: + def test_named_method( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: + assert_cell_operation_works(value1, lambda cell: cell.neq(value2, propagate_missing_values=False), expected) + + def test_named_method_wrapped_in_cell( + self, + value1: float, + value2: float, + expected: bool | None, + ) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell.neq(_LazyCell(pl.lit(value2)), propagate_missing_values=False), + expected, + ) From bd4b009f38643325f1c11a48965fec14cae3c63b Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 13:25:05 +0100 Subject: [PATCH 08/23] chore: check boolean operations --- src/safeds/data/tabular/containers/_cell.py | 68 +++++++++++------- tests/helpers/_assertions.py | 7 +- .../tabular/containers/_lazy_cell/test_and.py | 71 +++++++++++++------ .../tabular/containers/_lazy_cell/test_not.py | 11 +-- .../tabular/containers/_lazy_cell/test_or.py | 71 +++++++++++++------ .../tabular/containers/_lazy_cell/test_xor.py | 71 +++++++++++++------ 6 files changed, 208 insertions(+), 91 deletions(-) diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 769c5cf26..97ac3cb02 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -434,10 +434,13 @@ def not_(self) -> _BooleanCell: """ Negate a boolean. This is equivalent to the `~` operator. + Do **not** use the `not` operator. Its behavior cannot be overwritten in Python, so it will not work as + expected. + Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [True, False]) + >>> column = Column("a", [True, False, None]) >>> column.transform(lambda cell: cell.not_()) +-------+ | a | @@ -446,6 +449,7 @@ def not_(self) -> _BooleanCell: +=======+ | false | | true | + | null | +-------+ >>> column.transform(lambda cell: ~cell) @@ -456,6 +460,7 @@ def not_(self) -> _BooleanCell: +=======+ | false | | true | + | null | +-------+ """ return self.__invert__() @@ -464,28 +469,33 @@ def and_(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: """ Perform a boolean AND operation. This is equivalent to the `&` operator. + Do **not** use the `and` operator. Its behavior cannot be overwritten in Python, so it will not work as + expected. + Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [True, False]) - >>> column.transform(lambda cell: cell.and_(False)) + >>> column = Column("a", [True, False, None]) + >>> column.transform(lambda cell: cell.and_(True)) +-------+ | a | | --- | | bool | +=======+ + | true | | false | - | false | + | null | +-------+ - >>> column.transform(lambda cell: cell & False) + >>> column.transform(lambda cell: cell & True) +-------+ | a | | --- | | bool | +=======+ + | true | | false | - | false | + | null | +-------+ """ return self.__and__(other) @@ -494,29 +504,33 @@ def or_(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: """ Perform a boolean OR operation. This is equivalent to the `|` operator. + Do **not** use the `or` operator. Its behavior cannot be overwritten in Python, so it will not work as expected. + Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [True, False]) - >>> column.transform(lambda cell: cell.or_(True)) - +------+ - | a | - | --- | - | bool | - +======+ - | true | - | true | - +------+ + >>> column = Column("a", [True, False, None]) + >>> column.transform(lambda cell: cell.or_(False)) + +-------+ + | a | + | --- | + | bool | + +=======+ + | true | + | false | + | null | + +-------+ - >>> column.transform(lambda cell: cell | True) - +------+ - | a | - | --- | - | bool | - +======+ - | true | - | true | - +------+ + >>> column.transform(lambda cell: cell | False) + +-------+ + | a | + | --- | + | bool | + +=======+ + | true | + | false | + | null | + +-------+ """ return self.__or__(other) @@ -527,7 +541,7 @@ def xor(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [True, False]) + >>> column = Column("a", [True, False, None]) >>> column.transform(lambda cell: cell.xor(True)) +-------+ | a | @@ -536,6 +550,7 @@ def xor(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: +=======+ | false | | true | + | null | +-------+ >>> column.transform(lambda cell: cell ^ True) @@ -546,6 +561,7 @@ def xor(self, other: _ConvertibleToBooleanCell) -> _BooleanCell: +=======+ | false | | true | + | null | +-------+ """ return self.__xor__(other) diff --git a/tests/helpers/_assertions.py b/tests/helpers/_assertions.py index 0a0434ed5..b9d084dd4 100644 --- a/tests/helpers/_assertions.py +++ b/tests/helpers/_assertions.py @@ -5,6 +5,7 @@ from safeds.data.labeled.containers import TabularDataset from safeds.data.tabular.containers import Cell, Column, Row, Table +from safeds.data.tabular.typing import ColumnType def assert_tables_are_equal( @@ -65,6 +66,8 @@ def assert_cell_operation_works( value: Any, transformer: Callable[[Cell], Cell], expected: Any, + *, + type_: ColumnType | None = None, ) -> None: """ Assert that a cell operation works as expected. @@ -77,8 +80,10 @@ def assert_cell_operation_works( The transformer to apply to the cells. expected: The expected value of the transformed cell. + type_: + The type of the column. By default, it is inferred from the value. """ - column = Column("A", [value]) + column = Column("A", [value], type_=type_) transformed_column = column.transform(transformer) actual = transformed_column[0] assert actual == expected diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_and.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_and.py index ffbe3786f..a2a981ebb 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_and.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_and.py @@ -4,6 +4,7 @@ import pytest from safeds.data.tabular.containers._lazy_cell import _LazyCell +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -12,39 +13,69 @@ [ (False, False, False), (False, True, False), + (False, None, False), (True, False, False), (True, True, True), + (True, None, None), + (None, False, False), + (None, True, None), + (None, None, None), (0, False, False), (0, True, False), (1, False, False), (1, True, True), ], ids=[ - "false - false", - "false - true", - "true - false", - "true - true", - "falsy int - false", - "falsy int - true", - "truthy int - false", - "truthy int - true", + "False - False", + "False - True", + "False - None", + "True - False", + "True - True", + "True - None", + "None - False", + "None - True", + "None - None", + "falsy int - False", + "falsy int - True", + "truthy int - False", + "truthy int - True", ], ) class TestShouldComputeConjunction: - def test_dunder_method(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell & value2, expected) + def test_dunder_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works(value1, lambda cell: cell & value2, expected, type_=ColumnType.boolean()) - def test_dunder_method_wrapped_in_cell(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell & _LazyCell(pl.lit(value2)), expected) + def test_dunder_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell & _LazyCell(pl.lit(value2)), + expected, + type_=ColumnType.boolean(), + ) - def test_dunder_method_inverted_order(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value2, lambda cell: value1 & cell, expected) + def test_dunder_method_inverted_order(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works(value2, lambda cell: value1 & cell, expected, type_=ColumnType.boolean()) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) & cell, expected) + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: Any, + value2: bool | None, + expected: bool | None, + ) -> None: + assert_cell_operation_works( + value2, + lambda cell: _LazyCell(pl.lit(value1)) & cell, + expected, + type_=ColumnType.boolean(), + ) - def test_named_method(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell.and_(value2), expected) + def test_named_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works(value1, lambda cell: cell.and_(value2), expected, type_=ColumnType.boolean()) - def test_named_method_wrapped_in_cell(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell.and_(_LazyCell(pl.lit(value2))), expected) + def test_named_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell.and_(_LazyCell(pl.lit(value2))), + expected, + type_=ColumnType.boolean(), + ) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_not.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_not.py index 6381200b7..e056d0d0a 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_not.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_not.py @@ -2,6 +2,7 @@ import pytest +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -10,19 +11,21 @@ [ (False, True), (True, False), + (None, None), (0, True), (1, False), ], ids=[ - "false", - "true", + "False", + "True", + "None", "falsy int", "truthy int", ], ) class TestShouldInvertValueOfCell: def test_dunder_method(self, value: Any, expected: bool) -> None: - assert_cell_operation_works(value, lambda cell: ~cell, expected) + assert_cell_operation_works(value, lambda cell: ~cell, expected, type_=ColumnType.boolean()) def test_named_method(self, value: Any, expected: bool) -> None: - assert_cell_operation_works(value, lambda cell: cell.not_(), expected) + assert_cell_operation_works(value, lambda cell: cell.not_(), expected, type_=ColumnType.boolean()) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_or.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_or.py index 6220b4011..5f19e3dbc 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_or.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_or.py @@ -4,6 +4,7 @@ import pytest from safeds.data.tabular.containers._lazy_cell import _LazyCell +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -12,39 +13,69 @@ [ (False, False, False), (False, True, True), + (False, None, None), (True, False, True), (True, True, True), + (True, None, True), + (None, False, None), + (None, True, True), + (None, None, None), (0, False, False), (0, True, True), (1, False, True), (1, True, True), ], ids=[ - "false - false", - "false - true", - "true - false", - "true - true", - "falsy int - false", - "falsy int - true", - "truthy int - false", - "truthy int - true", + "False - False", + "False - True", + "False - None", + "True - False", + "True - True", + "True - None", + "None - False", + "None - True", + "None - None", + "falsy int - False", + "falsy int - True", + "truthy int - False", + "truthy int - True", ], ) class TestShouldComputeDisjunction: - def test_dunder_method(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell | value2, expected) + def test_dunder_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works(value1, lambda cell: cell | value2, expected, type_=ColumnType.boolean()) - def test_dunder_method_wrapped_in_cell(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell | _LazyCell(pl.lit(value2)), expected) + def test_dunder_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell | _LazyCell(pl.lit(value2)), + expected, + type_=ColumnType.boolean(), + ) - def test_dunder_method_inverted_order(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value2, lambda cell: value1 | cell, expected) + def test_dunder_method_inverted_order(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works(value2, lambda cell: value1 | cell, expected, type_=ColumnType.boolean()) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) | cell, expected) + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: Any, + value2: bool | None, + expected: bool | None, + ) -> None: + assert_cell_operation_works( + value2, + lambda cell: _LazyCell(pl.lit(value1)) | cell, + expected, + type_=ColumnType.boolean(), + ) - def test_named_method(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell.or_(value2), expected) + def test_named_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works(value1, lambda cell: cell.or_(value2), expected, type_=ColumnType.boolean()) - def test_named_method_wrapped_in_cell(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell.or_(_LazyCell(pl.lit(value2))), expected) + def test_named_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell.or_(_LazyCell(pl.lit(value2))), + expected, + type_=ColumnType.boolean(), + ) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_xor.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_xor.py index e62398512..54d3faa5c 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_xor.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_xor.py @@ -4,6 +4,7 @@ import pytest from safeds.data.tabular.containers._lazy_cell import _LazyCell +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -12,39 +13,69 @@ [ (False, False, False), (False, True, True), + (False, None, None), (True, False, True), (True, True, False), + (True, None, None), + (None, False, None), + (None, True, None), + (None, None, None), (0, False, False), (0, True, True), (1, False, True), (1, True, False), ], ids=[ - "false - false", - "false - true", - "true - false", - "true - true", - "falsy int - false", - "falsy int - true", - "truthy int - false", - "truthy int - true", + "False - False", + "False - True", + "False - None", + "True - False", + "True - True", + "True - None", + "None - False", + "None - True", + "None - None", + "falsy int - False", + "falsy int - True", + "truthy int - False", + "truthy int - True", ], ) class TestShouldComputeExclusiveOr: - def test_dunder_method(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell ^ value2, expected) + def test_dunder_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works(value1, lambda cell: cell ^ value2, expected, type_=ColumnType.boolean()) - def test_dunder_method_wrapped_in_cell(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell ^ _LazyCell(pl.lit(value2)), expected) + def test_dunder_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell ^ _LazyCell(pl.lit(value2)), + expected, + type_=ColumnType.boolean(), + ) - def test_dunder_method_inverted_order(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value2, lambda cell: value1 ^ cell, expected) + def test_dunder_method_inverted_order(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works(value2, lambda cell: value1 ^ cell, expected, type_=ColumnType.boolean()) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) ^ cell, expected) + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: Any, + value2: bool | None, + expected: bool | None, + ) -> None: + assert_cell_operation_works( + value2, + lambda cell: _LazyCell(pl.lit(value1)) ^ cell, + expected, + type_=ColumnType.boolean(), + ) - def test_named_method(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell.xor(value2), expected) + def test_named_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works(value1, lambda cell: cell.xor(value2), expected, type_=ColumnType.boolean()) - def test_named_method_wrapped_in_cell(self, value1: Any, value2: bool, expected: bool) -> None: - assert_cell_operation_works(value1, lambda cell: cell.xor(_LazyCell(pl.lit(value2))), expected) + def test_named_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell.xor(_LazyCell(pl.lit(value2))), + expected, + type_=ColumnType.boolean(), + ) From 6973d3927513e0495b5aeb7bdfe80e684ebd6658 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 14:16:58 +0100 Subject: [PATCH 09/23] chore: fix type hints for comparison operations --- .../tabular/containers/_lazy_cell/test_eq.py | 37 +++++++++++++------ .../tabular/containers/_lazy_cell/test_ge.py | 29 +++++++++++---- .../tabular/containers/_lazy_cell/test_gt.py | 29 +++++++++++---- .../tabular/containers/_lazy_cell/test_le.py | 29 +++++++++++---- .../tabular/containers/_lazy_cell/test_lt.py | 29 +++++++++++---- .../tabular/containers/_lazy_cell/test_neq.py | 37 +++++++++++++------ 6 files changed, 140 insertions(+), 50 deletions(-) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py index 219b4e97f..19e82c373 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_eq.py @@ -25,27 +25,42 @@ ], ) class TestShouldComputeEquality: - def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell == value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell == _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 == cell, expected) # type: ignore[arg-type,return-value] def test_dunder_method_inverted_order_wrapped_in_cell( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) == cell, expected) # type: ignore[arg-type,return-value] - def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.eq(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.eq(_LazyCell(pl.lit(value2))), expected) @@ -65,16 +80,16 @@ def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expect class TestShouldComputeEqualityWithoutPropagatingMissingValues: def test_named_method( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works(value1, lambda cell: cell.eq(value2, propagate_missing_values=False), expected) def test_named_method_wrapped_in_cell( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works( diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_ge.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_ge.py index a4b488cff..b30bd15f1 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_ge.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_ge.py @@ -25,25 +25,40 @@ ], ) class TestShouldComputeGreaterThanOrEqual: - def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell >= value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell >= _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 >= cell, expected) def test_dunder_method_inverted_order_wrapped_in_cell( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) >= cell, expected) - def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.ge(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.ge(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_gt.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_gt.py index 80bd44630..3701ceae3 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_gt.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_gt.py @@ -25,25 +25,40 @@ ], ) class TestShouldComputeGreaterThan: - def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell > value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell > _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 > cell, expected) def test_dunder_method_inverted_order_wrapped_in_cell( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) > cell, expected) - def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.gt(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.gt(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_le.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_le.py index e1fcc5ca6..939ecede3 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_le.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_le.py @@ -25,25 +25,40 @@ ], ) class TestShouldComputeLessThanOrEqual: - def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell <= value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell <= _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 <= cell, expected) def test_dunder_method_inverted_order_wrapped_in_cell( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) <= cell, expected) - def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.le(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.le(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py index 1daef2437..6924d3d13 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_lt.py @@ -25,25 +25,40 @@ ], ) class TestShouldComputeLessThan: - def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell < value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell < _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 < cell, expected) def test_dunder_method_inverted_order_wrapped_in_cell( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) < cell, expected) - def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.lt(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.lt(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py index 36cde010f..efa0cfec4 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_neq.py @@ -25,27 +25,42 @@ ], ) class TestShouldComputeNegatedEquality: - def test_dunder_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell != value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell != _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: bool | None) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: value2 != cell, expected) # type: ignore[arg-type,return-value] def test_dunder_method_inverted_order_wrapped_in_cell( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works(value1, lambda cell: _LazyCell(pl.lit(value2)) != cell, expected) # type: ignore[arg-type,return-value] - def test_named_method(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: bool | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.neq(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: bool | None) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: bool | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.neq(_LazyCell(pl.lit(value2))), expected) @@ -65,16 +80,16 @@ def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expect class TestShouldComputeNegatedEqualityWithoutPropagatingMissingValues: def test_named_method( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works(value1, lambda cell: cell.neq(value2, propagate_missing_values=False), expected) def test_named_method_wrapped_in_cell( self, - value1: float, - value2: float, + value1: float | None, + value2: float | None, expected: bool | None, ) -> None: assert_cell_operation_works( From a95b005a22619f1799c16d2b38f814f6c6c1b568 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 14:27:57 +0100 Subject: [PATCH 10/23] chore: check tests for numeric operations --- tests/helpers/_assertions.py | 9 ++- .../tabular/containers/_lazy_cell/test_abs.py | 13 ++-- .../tabular/containers/_lazy_cell/test_add.py | 36 +++++++-- .../tabular/containers/_lazy_cell/test_and.py | 12 +-- .../containers/_lazy_cell/test_ceil.py | 13 ++-- .../tabular/containers/_lazy_cell/test_div.py | 36 +++++++-- .../containers/_lazy_cell/test_floor.py | 13 ++-- .../containers/_lazy_cell/test_floordiv.py | 29 ++++++-- .../tabular/containers/_lazy_cell/test_mod.py | 36 +++++++-- .../tabular/containers/_lazy_cell/test_mul.py | 36 +++++++-- .../tabular/containers/_lazy_cell/test_neg.py | 13 ++-- .../tabular/containers/_lazy_cell/test_not.py | 8 +- .../tabular/containers/_lazy_cell/test_or.py | 12 +-- .../tabular/containers/_lazy_cell/test_pos.py | 9 ++- .../tabular/containers/_lazy_cell/test_pow.py | 73 ++++++++++++++++--- .../tabular/containers/_lazy_cell/test_sub.py | 36 +++++++-- .../tabular/containers/_lazy_cell/test_xor.py | 12 +-- 17 files changed, 300 insertions(+), 96 deletions(-) diff --git a/tests/helpers/_assertions.py b/tests/helpers/_assertions.py index b9d084dd4..6c6c7f4ab 100644 --- a/tests/helpers/_assertions.py +++ b/tests/helpers/_assertions.py @@ -67,7 +67,7 @@ def assert_cell_operation_works( transformer: Callable[[Cell], Cell], expected: Any, *, - type_: ColumnType | None = None, + type_if_none: ColumnType | None = None, ) -> None: """ Assert that a cell operation works as expected. @@ -80,13 +80,14 @@ def assert_cell_operation_works( The transformer to apply to the cells. expected: The expected value of the transformed cell. - type_: - The type of the column. By default, it is inferred from the value. + type_if_none: + The type of the column if value is `None`. """ + type_ = type_if_none if value is None else None column = Column("A", [value], type_=type_) transformed_column = column.transform(transformer) actual = transformed_column[0] - assert actual == expected + assert actual == expected, f"Expected {expected}, but got {actual}." def assert_row_operation_works( diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_abs.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_abs.py index a01749e32..7cc40c878 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_abs.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_abs.py @@ -1,5 +1,6 @@ import pytest +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -12,6 +13,7 @@ (10.5, 10.5), (-10, 10), (-10.5, 10.5), + (None, None), ], ids=[ "zero int", @@ -20,11 +22,12 @@ "positive float", "negative int", "negative float", + "None", ], ) -class TestShouldReturnAbsoluteValueOfCell: - def test_dunder_method(self, value: float, expected: float) -> None: - assert_cell_operation_works(value, lambda cell: abs(cell), expected) +class TestShouldReturnAbsoluteValue: + def test_dunder_method(self, value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: abs(cell), expected, type_if_none=ColumnType.float64()) - def test_named_method(self, value: float, expected: float) -> None: - assert_cell_operation_works(value, lambda cell: cell.abs(), expected) + def test_named_method(self, value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.abs(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_add.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_add.py index 9a2d59a39..f760cbe2b 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_add.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_add.py @@ -12,29 +12,53 @@ (3, 1.5, 4.5), (1.5, 3, 4.5), (1.5, 1.5, 3.0), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeAddition: - def test_dunder_method(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell + value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell + _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 + cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) + cell, expected) - def test_named_method(self, value1: float, value2: float, expected: float) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.add(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.add(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_and.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_and.py index a2a981ebb..0d3a08c29 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_and.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_and.py @@ -43,18 +43,18 @@ ) class TestShouldComputeConjunction: def test_dunder_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: - assert_cell_operation_works(value1, lambda cell: cell & value2, expected, type_=ColumnType.boolean()) + assert_cell_operation_works(value1, lambda cell: cell & value2, expected, type_if_none=ColumnType.boolean()) def test_dunder_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: assert_cell_operation_works( value1, lambda cell: cell & _LazyCell(pl.lit(value2)), expected, - type_=ColumnType.boolean(), + type_if_none=ColumnType.boolean(), ) def test_dunder_method_inverted_order(self, value1: Any, value2: bool | None, expected: bool | None) -> None: - assert_cell_operation_works(value2, lambda cell: value1 & cell, expected, type_=ColumnType.boolean()) + assert_cell_operation_works(value2, lambda cell: value1 & cell, expected, type_if_none=ColumnType.boolean()) def test_dunder_method_inverted_order_wrapped_in_cell( self, @@ -66,16 +66,16 @@ def test_dunder_method_inverted_order_wrapped_in_cell( value2, lambda cell: _LazyCell(pl.lit(value1)) & cell, expected, - type_=ColumnType.boolean(), + type_if_none=ColumnType.boolean(), ) def test_named_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: - assert_cell_operation_works(value1, lambda cell: cell.and_(value2), expected, type_=ColumnType.boolean()) + assert_cell_operation_works(value1, lambda cell: cell.and_(value2), expected, type_if_none=ColumnType.boolean()) def test_named_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: assert_cell_operation_works( value1, lambda cell: cell.and_(_LazyCell(pl.lit(value2))), expected, - type_=ColumnType.boolean(), + type_if_none=ColumnType.boolean(), ) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_ceil.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_ceil.py index b1cd6bed0..32dce3d20 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_ceil.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_ceil.py @@ -2,6 +2,7 @@ import pytest +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -14,6 +15,7 @@ (10.5, 11), (-10, -10), (-10.5, -10), + (None, None), ], ids=[ "zero int", @@ -22,11 +24,12 @@ "positive float", "negative int", "negative float", + "None", ], ) -class TestShouldReturnCeilOfCell: - def test_dunder_method(self, value: float, expected: float) -> None: - assert_cell_operation_works(value, lambda cell: math.ceil(cell), expected) +class TestShouldReturnCeiling: + def test_dunder_method(self, value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: math.ceil(cell), expected, type_if_none=ColumnType.float64()) - def test_named_method(self, value: float, expected: float) -> None: - assert_cell_operation_works(value, lambda cell: cell.ceil(), expected) + def test_named_method(self, value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.ceil(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_div.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_div.py index 1aa8ab692..8501a4f65 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_div.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_div.py @@ -12,29 +12,53 @@ (3, 1.5, 2.0), (1.5, 3, 0.5), (1.5, 1.5, 1.0), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeDivision: - def test_dunder_method(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell / value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell / _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 / cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) / cell, expected) - def test_named_method(self, value1: float, value2: float, expected: float) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.div(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.div(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_floor.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_floor.py index 72590efa8..73ecf8e85 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_floor.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_floor.py @@ -2,6 +2,7 @@ import pytest +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -14,6 +15,7 @@ (10.5, 10), (-10, -10), (-10.5, -11), + (None, None), ], ids=[ "zero int", @@ -22,11 +24,12 @@ "positive float", "negative int", "negative float", + "None", ], ) -class TestShouldReturnFloorOfCell: - def test_dunder_method(self, value: float, expected: float) -> None: - assert_cell_operation_works(value, lambda cell: math.floor(cell), expected) +class TestShouldReturnFloor: + def test_dunder_method(self, value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: math.floor(cell), expected, type_if_none=ColumnType.float64()) - def test_named_method(self, value: float, expected: float) -> None: - assert_cell_operation_works(value, lambda cell: cell.floor(), expected) + def test_named_method(self, value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.floor(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_floordiv.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_floordiv.py index e2b2316bb..dd9a3ea36 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_floordiv.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_floordiv.py @@ -12,23 +12,42 @@ (3, 1.6, 1), (1.5, 3, 0), (1.5, 1.4, 1), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) -class TestShouldComputeDivision: - def test_dunder_method(self, value1: float, value2: float, expected: float) -> None: +class TestShouldComputeFlooredDivision: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell // value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell // _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 // cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) // cell, expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_mod.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_mod.py index f1d960e62..804b00e39 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_mod.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_mod.py @@ -12,29 +12,53 @@ (3, 1.5, 0.0), (1.5, 3, 1.5), (1.5, 1.5, 0.0), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeModulus: - def test_dunder_method(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell % value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell % _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 % cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) % cell, expected) - def test_named_method(self, value1: float, value2: float, expected: float) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.mod(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.mod(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_mul.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_mul.py index 279522aa1..b7e56dbe8 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_mul.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_mul.py @@ -12,29 +12,53 @@ (3, 1.5, 4.5), (1.5, 3, 4.5), (1.5, 1.5, 2.25), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeMultiplication: - def test_dunder_method(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell * value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell * _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 * cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) * cell, expected) - def test_named_method(self, value1: float, value2: float, expected: float) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.mul(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.mul(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_neg.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_neg.py index 306fdd530..47ed35840 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_neg.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_neg.py @@ -1,5 +1,6 @@ import pytest +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -12,6 +13,7 @@ (10.5, -10.5), (-10, 10), (-10.5, 10.5), + (None, None), ], ids=[ "zero int", @@ -20,11 +22,12 @@ "positive float", "negative int", "negative float", + "None", ], ) -class TestShouldNegateValueOfCell: - def test_dunder_method(self, value: float, expected: float) -> None: - assert_cell_operation_works(value, lambda cell: -cell, expected) +class TestShouldNegateValue: + def test_dunder_method(self, value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: -cell, expected, type_if_none=ColumnType.float64()) - def test_named_method(self, value: float, expected: float) -> None: - assert_cell_operation_works(value, lambda cell: cell.neg(), expected) + def test_named_method(self, value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.neg(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_not.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_not.py index e056d0d0a..c0619094c 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_not.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_not.py @@ -24,8 +24,8 @@ ], ) class TestShouldInvertValueOfCell: - def test_dunder_method(self, value: Any, expected: bool) -> None: - assert_cell_operation_works(value, lambda cell: ~cell, expected, type_=ColumnType.boolean()) + def test_dunder_method(self, value: Any, expected: bool | None) -> None: + assert_cell_operation_works(value, lambda cell: ~cell, expected, type_if_none=ColumnType.boolean()) - def test_named_method(self, value: Any, expected: bool) -> None: - assert_cell_operation_works(value, lambda cell: cell.not_(), expected, type_=ColumnType.boolean()) + def test_named_method(self, value: Any, expected: bool | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.not_(), expected, type_if_none=ColumnType.boolean()) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_or.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_or.py index 5f19e3dbc..0d19834bc 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_or.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_or.py @@ -43,18 +43,18 @@ ) class TestShouldComputeDisjunction: def test_dunder_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: - assert_cell_operation_works(value1, lambda cell: cell | value2, expected, type_=ColumnType.boolean()) + assert_cell_operation_works(value1, lambda cell: cell | value2, expected, type_if_none=ColumnType.boolean()) def test_dunder_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: assert_cell_operation_works( value1, lambda cell: cell | _LazyCell(pl.lit(value2)), expected, - type_=ColumnType.boolean(), + type_if_none=ColumnType.boolean(), ) def test_dunder_method_inverted_order(self, value1: Any, value2: bool | None, expected: bool | None) -> None: - assert_cell_operation_works(value2, lambda cell: value1 | cell, expected, type_=ColumnType.boolean()) + assert_cell_operation_works(value2, lambda cell: value1 | cell, expected, type_if_none=ColumnType.boolean()) def test_dunder_method_inverted_order_wrapped_in_cell( self, @@ -66,16 +66,16 @@ def test_dunder_method_inverted_order_wrapped_in_cell( value2, lambda cell: _LazyCell(pl.lit(value1)) | cell, expected, - type_=ColumnType.boolean(), + type_if_none=ColumnType.boolean(), ) def test_named_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: - assert_cell_operation_works(value1, lambda cell: cell.or_(value2), expected, type_=ColumnType.boolean()) + assert_cell_operation_works(value1, lambda cell: cell.or_(value2), expected, type_if_none=ColumnType.boolean()) def test_named_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: assert_cell_operation_works( value1, lambda cell: cell.or_(_LazyCell(pl.lit(value2))), expected, - type_=ColumnType.boolean(), + type_if_none=ColumnType.boolean(), ) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_pos.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_pos.py index da37cc41f..ec8bebf85 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_pos.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_pos.py @@ -1,5 +1,6 @@ import pytest +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -12,6 +13,7 @@ (10.5, 10.5), (-10, -10), (-10.5, -10.5), + (None, None), ], ids=[ "zero int", @@ -20,8 +22,9 @@ "positive float", "negative int", "negative float", + "None", ], ) -class TestShouldReturnValueOfCell: - def test_dunder_method(self, value: float, expected: float) -> None: - assert_cell_operation_works(value, lambda cell: +cell, expected) +class TestShouldReturnValue: + def test_dunder_method(self, value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: +cell, expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_pow.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_pow.py index a977b5e92..e0a833c88 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_pow.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_pow.py @@ -2,6 +2,7 @@ import pytest from safeds.data.tabular.containers._lazy_cell import _LazyCell +from safeds.data.tabular.typing import ColumnType from tests.helpers import assert_cell_operation_works @@ -12,29 +13,77 @@ (4, 0.5, 2.0), (1.5, 2, 2.25), (2.25, 0.5, 1.5), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputePower: - def test_dunder_method(self, value1: float, value2: float, expected: float) -> None: - assert_cell_operation_works(value1, lambda cell: cell**value2, expected) + def test_dunder_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: + if value2 is None: + pytest.skip("polars does not support null exponents.") - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: - assert_cell_operation_works(value1, lambda cell: cell ** _LazyCell(pl.lit(value2)), expected) + assert_cell_operation_works(value1, lambda cell: cell**value2, expected, type_if_none=ColumnType.float64()) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: float) -> None: - assert_cell_operation_works(value2, lambda cell: value1**cell, expected) + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell ** _LazyCell(pl.lit(value2, dtype=pl.Float64())), + expected, + type_if_none=ColumnType.float64(), + ) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: - assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) ** cell, expected) + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: + if value1 is None: + pytest.skip("polars does not support null base.") - def test_named_method(self, value1: float, value2: float, expected: float) -> None: - assert_cell_operation_works(value1, lambda cell: cell.pow(value2), expected) + assert_cell_operation_works(value2, lambda cell: value1**cell, expected, type_if_none=ColumnType.float64()) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: - assert_cell_operation_works(value1, lambda cell: cell.pow(_LazyCell(pl.lit(value2))), expected) + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: + assert_cell_operation_works( + value2, + lambda cell: _LazyCell(pl.lit(value1, dtype=pl.Float64())) ** cell, + expected, + type_if_none=ColumnType.float64(), + ) + + def test_named_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: + if value2 is None: + pytest.skip("polars does not support null exponents.") + + assert_cell_operation_works(value1, lambda cell: cell.pow(value2), expected, type_if_none=ColumnType.float64()) + + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: + assert_cell_operation_works( + value1, + lambda cell: cell.pow(_LazyCell(pl.lit(value2, dtype=pl.Float64()))), + expected, + type_if_none=ColumnType.float64(), + ) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_sub.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_sub.py index 15593ae79..b7a680f3d 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_sub.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_sub.py @@ -12,29 +12,53 @@ (3, 1.5, 1.5), (1.5, 3, -1.5), (1.5, 1.5, 0.0), + (None, 3, None), + (3, None, None), ], ids=[ "int - int", "int - float", "float - int", "float - float", + "left is None", + "right is None", ], ) class TestShouldComputeSubtraction: - def test_dunder_method(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell - value2, expected) - def test_dunder_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell - _LazyCell(pl.lit(value2)), expected) - def test_dunder_method_inverted_order(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: value1 - cell, expected) - def test_dunder_method_inverted_order_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_dunder_method_inverted_order_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value2, lambda cell: _LazyCell(pl.lit(value1)) - cell, expected) - def test_named_method(self, value1: float, value2: float, expected: float) -> None: + def test_named_method(self, value1: float | None, value2: float | None, expected: float | None) -> None: assert_cell_operation_works(value1, lambda cell: cell.sub(value2), expected) - def test_named_method_wrapped_in_cell(self, value1: float, value2: float, expected: float) -> None: + def test_named_method_wrapped_in_cell( + self, + value1: float | None, + value2: float | None, + expected: float | None, + ) -> None: assert_cell_operation_works(value1, lambda cell: cell.sub(_LazyCell(pl.lit(value2))), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_xor.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_xor.py index 54d3faa5c..995e6e0e7 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_xor.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_xor.py @@ -43,18 +43,18 @@ ) class TestShouldComputeExclusiveOr: def test_dunder_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: - assert_cell_operation_works(value1, lambda cell: cell ^ value2, expected, type_=ColumnType.boolean()) + assert_cell_operation_works(value1, lambda cell: cell ^ value2, expected, type_if_none=ColumnType.boolean()) def test_dunder_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: assert_cell_operation_works( value1, lambda cell: cell ^ _LazyCell(pl.lit(value2)), expected, - type_=ColumnType.boolean(), + type_if_none=ColumnType.boolean(), ) def test_dunder_method_inverted_order(self, value1: Any, value2: bool | None, expected: bool | None) -> None: - assert_cell_operation_works(value2, lambda cell: value1 ^ cell, expected, type_=ColumnType.boolean()) + assert_cell_operation_works(value2, lambda cell: value1 ^ cell, expected, type_if_none=ColumnType.boolean()) def test_dunder_method_inverted_order_wrapped_in_cell( self, @@ -66,16 +66,16 @@ def test_dunder_method_inverted_order_wrapped_in_cell( value2, lambda cell: _LazyCell(pl.lit(value1)) ^ cell, expected, - type_=ColumnType.boolean(), + type_if_none=ColumnType.boolean(), ) def test_named_method(self, value1: Any, value2: bool | None, expected: bool | None) -> None: - assert_cell_operation_works(value1, lambda cell: cell.xor(value2), expected, type_=ColumnType.boolean()) + assert_cell_operation_works(value1, lambda cell: cell.xor(value2), expected, type_if_none=ColumnType.boolean()) def test_named_method_wrapped_in_cell(self, value1: Any, value2: bool | None, expected: bool | None) -> None: assert_cell_operation_works( value1, lambda cell: cell.xor(_LazyCell(pl.lit(value2))), expected, - type_=ColumnType.boolean(), + type_if_none=ColumnType.boolean(), ) From 9b557789e092f36043595a4474c6275b039a01a2 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 14:46:01 +0100 Subject: [PATCH 11/23] chore: check numeric operations --- src/safeds/data/tabular/containers/_cell.py | 248 ++++++++++-------- .../data/tabular/containers/_lazy_cell.py | 10 +- 2 files changed, 141 insertions(+), 117 deletions(-) diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 97ac3cb02..3061ed33d 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -577,16 +577,17 @@ def abs(self) -> Cell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1, -2]) + >>> column = Column("a", [1, -2, None]) >>> column.transform(lambda cell: cell.abs()) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 1 | - | 2 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 1 | + | 2 | + | null | + +------+ """ return self.__abs__() @@ -597,7 +598,7 @@ def ceil(self) -> Cell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1.1, 2.9]) + >>> column = Column("a", [1.1, 3.0, None]) >>> column.transform(lambda cell: cell.ceil()) +---------+ | a | @@ -606,6 +607,7 @@ def ceil(self) -> Cell: +=========+ | 2.00000 | | 3.00000 | + | null | +---------+ """ return self.__ceil__() @@ -617,7 +619,7 @@ def floor(self) -> Cell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1.1, 2.9]) + >>> column = Column("a", [1.1, 3.0, None]) >>> column.transform(lambda cell: cell.floor()) +---------+ | a | @@ -625,28 +627,41 @@ def floor(self) -> Cell: | f64 | +=========+ | 1.00000 | - | 2.00000 | + | 3.00000 | + | null | +---------+ """ return self.__floor__() def neg(self) -> Cell: """ - Negate the value. + Negate the value. This is equivalent to the unary `-` operator. Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1, -2]) + >>> column = Column("a", [1, -2, None]) >>> column.transform(lambda cell: cell.neg()) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | -1 | - | 2 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | -1 | + | 2 | + | null | + +------+ + + >>> column.transform(lambda cell: -cell) + +------+ + | a | + | --- | + | i64 | + +======+ + | -1 | + | 2 | + | null | + +------+ """ return self.__neg__() @@ -657,26 +672,28 @@ def add(self, other: _ConvertibleToCell) -> Cell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [1, 2]) + >>> column = Column("a", [1, 2, None]) >>> column.transform(lambda cell: cell.add(3)) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 4 | - | 5 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 4 | + | 5 | + | null | + +------+ >>> column.transform(lambda cell: cell + 3) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 4 | - | 5 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 4 | + | 5 | + | null | + +------+ """ return self.__add__(other) @@ -687,7 +704,7 @@ def div(self, other: _ConvertibleToCell) -> Cell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [6, 8]) + >>> column = Column("a", [6, 8, None]) >>> column.transform(lambda cell: cell.div(2)) +---------+ | a | @@ -696,6 +713,7 @@ def div(self, other: _ConvertibleToCell) -> Cell: +=========+ | 3.00000 | | 4.00000 | + | null | +---------+ >>> column.transform(lambda cell: cell / 2) @@ -706,6 +724,7 @@ def div(self, other: _ConvertibleToCell) -> Cell: +=========+ | 3.00000 | | 4.00000 | + | null | +---------+ """ return self.__truediv__(other) @@ -717,26 +736,30 @@ def mod(self, other: _ConvertibleToCell) -> Cell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [5, 6]) + >>> column = Column("a", [5, 6, -1, None]) >>> column.transform(lambda cell: cell.mod(3)) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 2 | - | 0 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 2 | + | 0 | + | 2 | + | null | + +------+ >>> column.transform(lambda cell: cell % 3) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 2 | - | 0 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 2 | + | 0 | + | 2 | + | null | + +------+ """ return self.__mod__(other) @@ -747,26 +770,28 @@ def mul(self, other: _ConvertibleToCell) -> Cell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [2, 3]) + >>> column = Column("a", [2, 3, None]) >>> column.transform(lambda cell: cell.mul(4)) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 8 | - | 12 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 8 | + | 12 | + | null | + +------+ >>> column.transform(lambda cell: cell * 4) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 8 | - | 12 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 8 | + | 12 | + | null | + +------+ """ return self.__mul__(other) @@ -777,56 +802,61 @@ def pow(self, other: _ConvertibleToCell) -> Cell: Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [2, 3]) + >>> column = Column("a", [2, 3, None]) >>> column.transform(lambda cell: cell.pow(3)) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 8 | - | 27 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 8 | + | 27 | + | null | + +------+ + >>> column.transform(lambda cell: cell ** 3) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 8 | - | 27 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 8 | + | 27 | + | null | + +------+ """ return self.__pow__(other) def sub(self, other: _ConvertibleToCell) -> Cell: """ - Subtract a value. This is equivalent to the `-` operator. + Subtract a value. This is equivalent to the binary `-` operator. Examples -------- >>> from safeds.data.tabular.containers import Column - >>> column = Column("a", [5, 6]) + >>> column = Column("a", [5, 6, None]) >>> column.transform(lambda cell: cell.sub(3)) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 2 | - | 3 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 2 | + | 3 | + | null | + +------+ >>> column.transform(lambda cell: cell - 3) - +-----+ - | a | - | --- | - | i64 | - +=====+ - | 2 | - | 3 | - +-----+ + +------+ + | a | + | --- | + | i64 | + +======+ + | 2 | + | 3 | + | null | + +------+ """ return self.__sub__(other) diff --git a/src/safeds/data/tabular/containers/_lazy_cell.py b/src/safeds/data/tabular/containers/_lazy_cell.py index aaf9a0a9e..a215699ee 100644 --- a/src/safeds/data/tabular/containers/_lazy_cell.py +++ b/src/safeds/data/tabular/containers/_lazy_cell.py @@ -95,16 +95,10 @@ def __abs__(self) -> Cell: return _wrap(self._expression.__abs__()) def __ceil__(self) -> Cell: - import polars as pl - - # polars does not yet implement floor for integers - return _wrap(self._expression.cast(pl.Float64).ceil()) + return _wrap(self._expression.ceil()) def __floor__(self) -> Cell: - import polars as pl - - # polars does not yet implement floor for integers - return _wrap(self._expression.cast(pl.Float64).floor()) + return _wrap(self._expression.floor()) def __neg__(self) -> Cell: return _wrap(self._expression.__neg__()) From dd24d509ad7415d11db1af032e7b6cbd0fa36e0b Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 15:43:45 +0100 Subject: [PATCH 12/23] chore: check `_equals` --- src/safeds/_typing/__init__.py | 2 + src/safeds/data/tabular/containers/_cell.py | 44 ++++--- .../data/tabular/containers/_lazy_cell.py | 2 +- .../containers/_lazy_cell/test_equals.py | 108 +++++++++++++++--- .../_table/test_transform_columns.py | 6 +- 5 files changed, 125 insertions(+), 37 deletions(-) diff --git a/src/safeds/_typing/__init__.py b/src/safeds/_typing/__init__.py index 2ae84e575..b418fe5f9 100644 --- a/src/safeds/_typing/__init__.py +++ b/src/safeds/_typing/__init__.py @@ -13,12 +13,14 @@ _BooleanCell: TypeAlias = Cell[bool | None] # We cannot restrict `Cell`, because `Row.get_cell` returns a `Cell[Any]`. _ConvertibleToBooleanCell: TypeAlias = bool | Cell | None +_ConvertibleToIntCell: TypeAlias = int | Cell | None __all__ = [ "_BooleanCell", "_ConvertibleToBooleanCell", "_ConvertibleToCell", + "_ConvertibleToIntCell", "_NumericLiteral", "_PythonLiteral", "_TemporalLiteral", diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 3061ed33d..4e9c111ac 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -8,7 +8,13 @@ import polars as pl - from safeds._typing import _BooleanCell, _ConvertibleToBooleanCell, _ConvertibleToCell, _PythonLiteral + from safeds._typing import ( + _BooleanCell, + _ConvertibleToBooleanCell, + _ConvertibleToCell, + _ConvertibleToIntCell, + _PythonLiteral, + ) from safeds.data.tabular.typing import ColumnType from ._string_cell import StringCell @@ -52,9 +58,9 @@ def constant(value: _PythonLiteral | None) -> Cell: @staticmethod def date( - year: int | Cell[int], - month: int | Cell[int], - day: int | Cell[int], + year: _ConvertibleToIntCell, + month: _ConvertibleToIntCell, + day: _ConvertibleToIntCell, ) -> Cell[python_datetime.date | None]: """ Create a cell with a date. @@ -85,14 +91,14 @@ def date( @staticmethod def datetime( - year: int | Cell[int], - month: int | Cell[int], - day: int | Cell[int], + year: _ConvertibleToIntCell, + month: _ConvertibleToIntCell, + day: _ConvertibleToIntCell, *, - hour: int | Cell[int] = 0, - minute: int | Cell[int] = 0, - second: int | Cell[int] = 0, - microsecond: int | Cell[int] = 0, + hour: _ConvertibleToIntCell = 0, + minute: _ConvertibleToIntCell = 0, + second: _ConvertibleToIntCell = 0, + microsecond: _ConvertibleToIntCell = 0, ) -> Cell[python_datetime.datetime | None]: """ Create a cell with a datetime. @@ -136,14 +142,14 @@ def datetime( @staticmethod def duration( *, - weeks: int | Cell[int] = 0, - days: int | Cell[int] = 0, - hours: int | Cell[int] = 0, - minutes: int | Cell[int] = 0, - seconds: int | Cell[int] = 0, - milliseconds: int | Cell[int] = 0, - microseconds: int | Cell[int] = 0, - nanoseconds: int | Cell[int] = 0, + weeks: _ConvertibleToIntCell = 0, + days: _ConvertibleToIntCell = 0, + hours: _ConvertibleToIntCell = 0, + minutes: _ConvertibleToIntCell = 0, + seconds: _ConvertibleToIntCell = 0, + milliseconds: _ConvertibleToIntCell = 0, + microseconds: _ConvertibleToIntCell = 0, + nanoseconds: _ConvertibleToIntCell = 0, ) -> Cell[python_datetime.timedelta | None]: """ Create a cell with a duration. diff --git a/src/safeds/data/tabular/containers/_lazy_cell.py b/src/safeds/data/tabular/containers/_lazy_cell.py index a215699ee..749d7a9c0 100644 --- a/src/safeds/data/tabular/containers/_lazy_cell.py +++ b/src/safeds/data/tabular/containers/_lazy_cell.py @@ -231,7 +231,7 @@ def _equals(self, other: object) -> bool: return NotImplemented if self is other: return True - return self._expression.meta.eq(other._expression.meta) + return self._expression.meta.eq(other._expression) def _wrap(other: pl.Expr) -> Any: diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_equals.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_equals.py index e5f1c01eb..023cdf615 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_equals.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_equals.py @@ -3,40 +3,120 @@ import polars as pl import pytest -from safeds.data.tabular.containers import Cell, Table +from safeds.data.tabular.containers import Cell, Column from safeds.data.tabular.containers._lazy_cell import _LazyCell @pytest.mark.parametrize( - ("cell1", "cell2", "expected"), + ("cell_1", "cell_2", "expected"), [ - (_LazyCell(pl.col("a")), _LazyCell(pl.col("a")), True), - (_LazyCell(pl.col("a")), _LazyCell(pl.col("b")), False), + # equal (constant) + ( + Cell.constant(1), + Cell.constant(1), + True, + ), + # equal (date, int) + ( + Cell.date(2025, 1, 15), + Cell.date(2025, 1, 15), + True, + ), + # equal (date, column) + ( + Cell.date(_LazyCell(pl.col("a")), 1, 15), + Cell.date(_LazyCell(pl.col("a")), 1, 15), + True, + ), + # equal (column) + ( + _LazyCell(pl.col("a")), + _LazyCell(pl.col("a")), + True, + ), + # not equal (different constant value) + ( + Cell.constant(1), + Cell.constant(2), + False, + ), + # not equal (different constant type) + ( + Cell.constant(1), + Cell.constant("1"), + False, + ), + # not equal (different date, int) + ( + Cell.date(2025, 1, 15), + Cell.date(2024, 1, 15), + False, + ), + # not equal (different date, column) + ( + Cell.date(_LazyCell(pl.col("a")), 1, 15), + Cell.date(_LazyCell(pl.col("b")), 1, 15), + False, + ), + # not equal (different column) + ( + _LazyCell(pl.col("a")), + _LazyCell(pl.col("b")), + False, + ), + # not equal (different cell kinds) + ( + Cell.date(23, 1, 15), + Cell.time(23, 1, 15), + False, + ), ], ids=[ - "equal", - "different", + # Equal + "equal (constant)", + "equal (date, int)", + "equal (date, column)", + "equal (column)", + # Not equal + "not equal (different constant value)", + "not equal (different constant type)", + "not equal (different date, int)", + "not equal (different date, column)", + "not equal (different column)", + "not equal (different cell kinds)", ], ) -def test_should_return_whether_two_cells_are_equal(cell1: Cell, cell2: Cell, expected: bool) -> None: - assert (cell1._equals(cell2)) == expected +def test_should_return_whether_objects_are_equal(cell_1: Cell, cell_2: Cell, expected: bool) -> None: + assert (cell_1._equals(cell_2)) == expected -def test_should_return_true_if_objects_are_identical() -> None: - cell: Cell[Any] = _LazyCell(pl.col("a")) +@pytest.mark.parametrize( + "cell", + [ + Cell.constant(1), + Cell.date(2025, 1, 15), + _LazyCell(pl.col("a")), + ], + ids=[ + "constant", + "date", + "column", + ], +) +def test_should_return_true_if_objects_are_identical(cell: Cell) -> None: assert (cell._equals(cell)) is True @pytest.mark.parametrize( ("cell", "other"), [ - (_LazyCell(pl.col("a")), None), - (_LazyCell(pl.col("a")), Table({})), + (Cell.constant(1), None), + (Cell.constant(1), Column("col1", [1])), ], ids=[ "Cell vs. None", - "Cell vs. Table", + "Cell vs. Column", ], ) -def test_should_return_not_implemented_if_other_is_not_cell(cell: Cell, other: Any) -> None: +def test_should_return_not_implemented_if_other_has_different_type(cell: Cell, other: Any) -> None: assert (cell._equals(other)) is NotImplemented diff --git a/tests/safeds/data/tabular/containers/_table/test_transform_columns.py b/tests/safeds/data/tabular/containers/_table/test_transform_columns.py index ecd079750..3a30da46a 100644 --- a/tests/safeds/data/tabular/containers/_table/test_transform_columns.py +++ b/tests/safeds/data/tabular/containers/_table/test_transform_columns.py @@ -13,7 +13,7 @@ ( lambda: Table({"col1": []}), "col1", - lambda _: Cell.from_literal(None), + lambda _: Cell.constant(None), Table({"col1": []}), ), # no rows (computed value) @@ -27,7 +27,7 @@ ( lambda: Table({"col1": [1, 2]}), "col1", - lambda _: Cell.from_literal(None), + lambda _: Cell.constant(None), Table({"col1": [None, None]}), ), # non-empty (computed value) @@ -41,7 +41,7 @@ ( lambda: Table({"col1": [1, 2], "col2": [3, 4]}), ["col1", "col2"], - lambda _: Cell.from_literal(None), + lambda _: Cell.constant(None), Table({"col1": [None, None], "col2": [None, None]}), ), # multiple columns transformed (computed value) From ddfd7c5ac4043537bfb165534708f7f28532b7dd Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 15:50:25 +0100 Subject: [PATCH 13/23] chore: check `__hash__` --- .../_lazy_cell/__snapshots__/test_hash.ambr | 13 +++ .../containers/_lazy_cell/test_hash.py | 80 ++++++++++++++++--- 2 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 tests/safeds/data/tabular/containers/_lazy_cell/__snapshots__/test_hash.ambr diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/__snapshots__/test_hash.ambr b/tests/safeds/data/tabular/containers/_lazy_cell/__snapshots__/test_hash.ambr new file mode 100644 index 000000000..34f48031a --- /dev/null +++ b/tests/safeds/data/tabular/containers/_lazy_cell/__snapshots__/test_hash.ambr @@ -0,0 +1,13 @@ +# serializer version: 1 +# name: TestContract.test_should_return_same_hash_in_different_processes[column] + 8162512882156938440 +# --- +# name: TestContract.test_should_return_same_hash_in_different_processes[constant] + 4610312201483200147 +# --- +# name: TestContract.test_should_return_same_hash_in_different_processes[date, column] + 740357503917492401 +# --- +# name: TestContract.test_should_return_same_hash_in_different_processes[date, int] + 495023986348121879 +# --- diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_hash.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_hash.py index 6270f0e14..7c5a6d312 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_hash.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_hash.py @@ -1,27 +1,85 @@ -from typing import Any +from collections.abc import Callable import polars as pl import pytest +from syrupy import SnapshotAssertion from safeds.data.tabular.containers import Cell from safeds.data.tabular.containers._lazy_cell import _LazyCell -def test_should_be_deterministic() -> None: - cell: Cell[Any] = _LazyCell(pl.col("a")) - assert hash(cell) == 8162512882156938440 +@pytest.mark.parametrize( + "cell_factory", + [ + lambda: Cell.constant(1), + lambda: Cell.date(2025, 1, 15), + lambda: Cell.date(_LazyCell(pl.col("a")), 1, 15), + lambda: _LazyCell(pl.col("a")), + ], + ids=[ + "constant", + "date, int", + "date, column", + "column", + ], +) +class TestContract: + def test_should_return_same_hash_for_equal_objects(self, cell_factory: Callable[[], Cell]) -> None: + cell_1 = cell_factory() + cell_2 = cell_factory() + assert hash(cell_1) == hash(cell_2) + + def test_should_return_same_hash_in_different_processes( + self, + cell_factory: Callable[[], Cell], + snapshot: SnapshotAssertion, + ) -> None: + cell = cell_factory() + assert hash(cell) == snapshot @pytest.mark.parametrize( - ("cell1", "cell2", "expected"), + ("cell_1", "cell_2"), [ - (_LazyCell(pl.col("a")), _LazyCell(pl.col("a")), True), - (_LazyCell(pl.col("a")), _LazyCell(pl.col("b")), False), + # different constant value + ( + Cell.constant(1), + Cell.constant(2), + ), + # different constant type + ( + Cell.constant(1), + Cell.constant("1"), + ), + # different date, int + ( + Cell.date(2025, 1, 15), + Cell.date(2024, 1, 15), + ), + # different date, column + ( + Cell.date(_LazyCell(pl.col("a")), 1, 15), + Cell.date(_LazyCell(pl.col("b")), 1, 15), + ), + # different column + ( + _LazyCell(pl.col("a")), + _LazyCell(pl.col("b")), + ), + # different cell kinds + ( + Cell.date(23, 1, 15), + Cell.time(23, 1, 15), + ), ], ids=[ - "equal", - "different", + "different constant value", + "different constant type", + "different date, int", + "different date, column", + "different column", + "different cell kinds", ], ) -def test_should_be_good_hash(cell1: Cell, cell2: Cell, expected: bool) -> None: - assert (hash(cell1) == hash(cell2)) == expected +def test_should_be_good_hash(cell_1: Cell, cell_2: Cell) -> None: + assert hash(cell_1) != hash(cell_2) From 20299891392722fef74ca5e8ebf691fd191aad6f Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 15:59:15 +0100 Subject: [PATCH 14/23] chore: check `__str__` and `__repr__` --- .../containers/_lazy_cell/test_repr.py | 27 +++++++++++++++++++ .../containers/_lazy_cell/test_sizeof.py | 24 ++++++++++++----- .../tabular/containers/_lazy_cell/test_str.py | 27 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 tests/safeds/data/tabular/containers/_lazy_cell/test_repr.py create mode 100644 tests/safeds/data/tabular/containers/_lazy_cell/test_str.py diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_repr.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_repr.py new file mode 100644 index 000000000..91313f5ad --- /dev/null +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_repr.py @@ -0,0 +1,27 @@ +import polars as pl +import pytest + +from safeds.data.tabular.containers import Cell +from safeds.data.tabular.containers._lazy_cell import _LazyCell + + +@pytest.mark.parametrize( + ("cell", "expected"), + [ + ( + Cell.constant(1), + "dyn int: 1", + ), + ( + _LazyCell(pl.col("a")), + 'col("a")', + ), + ], + ids=[ + "constant", + "column", + ], +) +def test_should_return_a_string_representation(cell: Cell, expected: str) -> None: + # We do not care about the exact string representation, this is only for debugging + assert expected in repr(cell) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_sizeof.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_sizeof.py index 3bd544fc5..18f0a2017 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_sizeof.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_sizeof.py @@ -1,14 +1,26 @@ import sys -from typing import TYPE_CHECKING, Any import polars as pl +import pytest +from safeds.data.tabular.containers import Cell from safeds.data.tabular.containers._lazy_cell import _LazyCell -if TYPE_CHECKING: - from safeds.data.tabular.containers import Cell - -def test_should_return_size_greater_than_normal_object() -> None: - cell: Cell[Any] = _LazyCell(pl.col("a")) +@pytest.mark.parametrize( + "cell", + [ + Cell.constant(1), + Cell.date(2025, 1, 15), + Cell.date(_LazyCell(pl.col("a")), 1, 15), + _LazyCell(pl.col("a")), + ], + ids=[ + "constant", + "date, int", + "date, column", + "column", + ], +) +def test_should_be_larger_than_normal_object(cell: Cell) -> None: assert sys.getsizeof(cell) > sys.getsizeof(object()) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_str.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_str.py new file mode 100644 index 000000000..5fd8d0d0c --- /dev/null +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_str.py @@ -0,0 +1,27 @@ +import polars as pl +import pytest + +from safeds.data.tabular.containers import Cell +from safeds.data.tabular.containers._lazy_cell import _LazyCell + + +@pytest.mark.parametrize( + ("cell", "expected"), + [ + ( + Cell.constant(1), + "dyn int: 1", + ), + ( + _LazyCell(pl.col("a")), + 'col("a")', + ), + ], + ids=[ + "constant", + "column", + ], +) +def test_should_return_a_string_representation(cell: Cell, expected: str) -> None: + # We do not care about the exact string representation, this is only for debugging + assert str(cell) == expected From f2e765632f5224de9dec391fc0a10a780515dec7 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 16:16:44 +0100 Subject: [PATCH 15/23] chore: check `first_not_none` --- src/safeds/data/tabular/containers/_cell.py | 60 ++++++++------- .../data/tabular/containers/_lazy_cell.py | 15 +--- .../_lazy_cell/test_first_not_none.py | 73 ++++++------------- 3 files changed, 58 insertions(+), 90 deletions(-) diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 4e9c111ac..6e80aab41 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -83,9 +83,9 @@ def date( from ._lazy_cell import _LazyCell # circular import - year = _to_polars_expression(year) - month = _to_polars_expression(month) - day = _to_polars_expression(day) + year = _unwrap(year) + month = _unwrap(month) + day = _unwrap(day) return _LazyCell(pl.date(year, month, day)) @@ -129,13 +129,13 @@ def datetime( from ._lazy_cell import _LazyCell # circular import - year = _to_polars_expression(year) - month = _to_polars_expression(month) - day = _to_polars_expression(day) - hour = _to_polars_expression(hour) - minute = _to_polars_expression(minute) - second = _to_polars_expression(second) - microsecond = _to_polars_expression(microsecond) + year = _unwrap(year) + month = _unwrap(month) + day = _unwrap(day) + hour = _unwrap(hour) + minute = _unwrap(minute) + second = _unwrap(second) + microsecond = _unwrap(microsecond) return _LazyCell(pl.datetime(year, month, day, hour, minute, second, microsecond)) @@ -182,14 +182,14 @@ def duration( from ._lazy_cell import _LazyCell # circular import - weeks = _to_polars_expression(weeks) - days = _to_polars_expression(days) - hours = _to_polars_expression(hours) - minutes = _to_polars_expression(minutes) - seconds = _to_polars_expression(seconds) - milliseconds = _to_polars_expression(milliseconds) - microseconds = _to_polars_expression(microseconds) - nanoseconds = _to_polars_expression(nanoseconds) + weeks = _unwrap(weeks) + days = _unwrap(days) + hours = _unwrap(hours) + minutes = _unwrap(minutes) + seconds = _unwrap(seconds) + milliseconds = _unwrap(milliseconds) + microseconds = _unwrap(microseconds) + nanoseconds = _unwrap(nanoseconds) return _LazyCell( pl.duration( @@ -235,10 +235,10 @@ def time( from ._lazy_cell import _LazyCell # circular import - hour = _to_polars_expression(hour) - minute = _to_polars_expression(minute) - second = _to_polars_expression(second) - microsecond = _to_polars_expression(microsecond) + hour = _unwrap(hour) + minute = _unwrap(minute) + second = _unwrap(second) + microsecond = _unwrap(microsecond) return _LazyCell(pl.time(hour, minute, second, microsecond)) @@ -250,7 +250,7 @@ def first_not_none(cells: list[Cell[P]]) -> Cell[P | None]: Parameters ---------- cells: - The list of cells to be searched. + The list of cells to be checked. Returns ------- @@ -261,7 +261,11 @@ def first_not_none(cells: list[Cell[P]]) -> Cell[P | None]: from ._lazy_cell import _LazyCell # circular import - return _LazyCell(pl.coalesce([cell._polars_expression for cell in cells])) + # `coalesce` raises in this case + if not cells: + return Cell.constant(None) + + return _LazyCell(pl.coalesce([_unwrap(cell) for cell in cells])) # ------------------------------------------------------------------------------------------------------------------ # Dunder methods @@ -1155,10 +1159,10 @@ def _equals(self, other: object) -> bool: """ -def _to_polars_expression(cell: _ConvertibleToCell) -> pl.Expr: +def _unwrap(cell_proxy: _ConvertibleToCell) -> pl.Expr: import polars as pl - if isinstance(cell, Cell): - return cell._polars_expression + if isinstance(cell_proxy, Cell): + return cell_proxy._polars_expression else: - return pl.lit(cell) + return pl.lit(cell_proxy) diff --git a/src/safeds/data/tabular/containers/_lazy_cell.py b/src/safeds/data/tabular/containers/_lazy_cell.py index 749d7a9c0..e4f850537 100644 --- a/src/safeds/data/tabular/containers/_lazy_cell.py +++ b/src/safeds/data/tabular/containers/_lazy_cell.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, TypeVar from safeds._utils import _structural_hash -from ._cell import Cell +from ._cell import Cell, _unwrap if TYPE_CHECKING: import polars as pl @@ -234,12 +234,5 @@ def _equals(self, other: object) -> bool: return self._expression.meta.eq(other._expression) -def _wrap(other: pl.Expr) -> Any: - return _LazyCell(other) - - -def _unwrap(other: _ConvertibleToCell) -> Any: - if isinstance(other, Cell): - return other._polars_expression - - return other +def _wrap(expression: pl.Expr) -> Cell: + return _LazyCell(expression) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_first_not_none.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_first_not_none.py index 91f38f4b5..77f5fea33 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_first_not_none.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_first_not_none.py @@ -1,58 +1,29 @@ -from datetime import date, time +from typing import Any -import polars as pl import pytest +from safeds.data.tabular.containers import Table from safeds.data.tabular.containers._cell import Cell -from safeds.data.tabular.containers._lazy_cell import _LazyCell +_none_cell = Cell.constant(None) -class TestFirstNotNone: - def test_should_return_none(self) -> None: - to_eval: list[Cell] = [_LazyCell(None) for i in range(5)] - res = Cell.first_not_none(to_eval) - assert res.eq(_LazyCell(None)) - @pytest.mark.parametrize( - ("list_of_cells", "expected"), - [ - ([_LazyCell(None), _LazyCell(1), _LazyCell(None), _LazyCell(4)], _LazyCell(1)), - ([_LazyCell(i) for i in range(5)], _LazyCell(1)), - ( - [ - _LazyCell(None), - _LazyCell(None), - _LazyCell(pl.lit("Hello, World!")), - _LazyCell(pl.lit("Not returned")), - ], - _LazyCell("Hello, World!"), - ), - ([_LazyCell(pl.lit(i)) for i in ["a", "b", "c", "d"]], _LazyCell(pl.lit("a"))), - ([_LazyCell(i) for i in [None, time(0, 0, 0, 0), None, time(1, 1, 1, 1)]], _LazyCell(time(0, 0, 0, 0))), - ( - [_LazyCell(i) for i in [time(0, 0, 0, 0), time(1, 1, 1, 1), time(2, 2, 2, 2), time(3, 3, 3, 3)]], - _LazyCell(time(0, 0, 0, 0)), - ), - ([_LazyCell(i) for i in [None, date(2000, 1, 1), date(1098, 3, 4), None]], _LazyCell(date(2000, 1, 1))), - ([_LazyCell(date(2000, 3, i)) for i in range(1, 5)], _LazyCell(date(2000, 3, 1))), - ([_LazyCell(i) for i in [None, pl.lit("a"), 1, time(0, 0, 0, 0)]], _LazyCell(pl.lit("a"))), - ([_LazyCell(i) for i in [time(1, 1, 1, 1), 0, pl.lit("c"), date(2020, 1, 7)]], _LazyCell(time(1, 1, 1, 1))), - ([], _LazyCell(None)), - ], - ids=[ - "numeric_with_null", - "numeric_no_null", - "strings_with_null", - "strings_no_null", - "times_with_null", - "times_no_null", - "dates_with_null", - "dates_no_null", - "mixed_with_null", - "mixed_no_null", - "empty_list", - ], - ) - def test_should_return_first_non_none_value(self, list_of_cells: list[Cell], expected: Cell) -> None: - res = Cell.first_not_none(list_of_cells) - assert res.eq(expected) +@pytest.mark.parametrize( + ("cells", "expected"), + [ + ([], None), + ([_none_cell], None), + ([_none_cell, Cell.constant(1)], 1), + ([Cell.constant(1), _none_cell, Cell.constant(2)], 1), + ], + ids=[ + "empty", + "all None", + "one not None", + "multiple not None", + ], +) +def test_should_return_first_non_none_value(cells: list[Cell], expected: Any) -> None: + table = Table({"col1": [1]}) + actual = table.add_computed_column("col2", lambda _: Cell.first_not_none(cells)) + assert actual.get_column("col2").get_value(0) == expected From 737a1b301478bda28a553170f5a26abffdee6d51 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 16:35:06 +0100 Subject: [PATCH 16/23] chore: check `cast` --- tests/helpers/__init__.py | 4 ++-- tests/helpers/_assertions.py | 4 ++-- tests/helpers/_devices.py | 6 ++--- tests/safeds/_config/test_torch.py | 4 ++-- .../containers/_lazy_cell/test_cast.py | 23 +++++++++++++++++++ 5 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 tests/safeds/data/tabular/containers/_lazy_cell/test_cast.py diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 24b79b286..ddad13ead 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -2,7 +2,7 @@ assert_cell_operation_works, assert_row_operation_works, assert_tables_are_equal, - assert_that_tabular_datasets_are_equal, + assert_tabular_datasets_are_equal, ) from ._devices import ( configure_test_with_device, @@ -41,7 +41,7 @@ "assert_cell_operation_works", "assert_row_operation_works", "assert_tables_are_equal", - "assert_that_tabular_datasets_are_equal", + "assert_tabular_datasets_are_equal", "configure_test_with_device", "device_cpu", "device_cuda", diff --git a/tests/helpers/_assertions.py b/tests/helpers/_assertions.py index 6c6c7f4ab..2f7397f9f 100644 --- a/tests/helpers/_assertions.py +++ b/tests/helpers/_assertions.py @@ -45,7 +45,7 @@ def assert_tables_are_equal( ) -def assert_that_tabular_datasets_are_equal(table1: TabularDataset, table2: TabularDataset) -> None: +def assert_tabular_datasets_are_equal(table1: TabularDataset, table2: TabularDataset) -> None: """ Assert that two tabular datasets are equal. @@ -81,7 +81,7 @@ def assert_cell_operation_works( expected: The expected value of the transformed cell. type_if_none: - The type of the column if value is `None`. + The type of the column if the value is `None`. """ type_ = type_if_none if value is None else None column = Column("A", [value], type_=type_) diff --git a/tests/helpers/_devices.py b/tests/helpers/_devices.py index 3c4d4ce9a..7d2bc0803 100644 --- a/tests/helpers/_devices.py +++ b/tests/helpers/_devices.py @@ -19,10 +19,10 @@ def get_devices_ids() -> list[str]: def configure_test_with_device(device: Device) -> None: - _skip_if_device_not_available(device) # This will end the function if device is not available + skip_if_device_not_available(device) # This will end the function if device is not available _set_default_device(device) -def _skip_if_device_not_available(device: Device) -> None: +def skip_if_device_not_available(device: Device) -> None: if device == device_cuda and not torch.cuda.is_available(): - pytest.skip("This test requires cuda") + pytest.skip("This test requires CUDA.") diff --git a/tests/safeds/_config/test_torch.py b/tests/safeds/_config/test_torch.py index 9b993df74..0bc8b988b 100644 --- a/tests/safeds/_config/test_torch.py +++ b/tests/safeds/_config/test_torch.py @@ -4,7 +4,7 @@ from safeds._config import _get_device, _init_default_device, _set_default_device from tests.helpers import configure_test_with_device, device_cpu, device_cuda, get_devices, get_devices_ids -from tests.helpers._devices import _skip_if_device_not_available +from tests.helpers._devices import skip_if_device_not_available @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) @@ -16,7 +16,7 @@ def test_default_device(device: Device) -> None: @pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids()) def test_set_default_device(device: Device) -> None: - _skip_if_device_not_available(device) + skip_if_device_not_available(device) _set_default_device(device) assert _get_device().type == device.type assert torch.get_default_device().type == device.type diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_cast.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_cast.py new file mode 100644 index 000000000..3a5a7e188 --- /dev/null +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_cast.py @@ -0,0 +1,23 @@ +from typing import Any + +import pytest +from helpers import assert_cell_operation_works + +from safeds.data.tabular.typing import ColumnType + + +@pytest.mark.parametrize( + ("value", "type_", "expected"), + [ + (1, ColumnType.string(), "1"), + ("1", ColumnType.int64(), 1), + (None, ColumnType.int64(), None), + ], + ids=[ + "int64 to string", + "string to int64", + "None to int64", + ], +) +def test_should_cast_values_to_requested_type(value: Any, type_: ColumnType, expected: Any) -> None: + assert_cell_operation_works(value, lambda cell: cell.cast(type_), expected) From f5a4e9e367c4f43776d7fe20403e514394606cd9 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 16:37:20 +0100 Subject: [PATCH 17/23] chore: check `constant` --- .../containers/_lazy_cell/test_constant.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/safeds/data/tabular/containers/_lazy_cell/test_constant.py diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_constant.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_constant.py new file mode 100644 index 000000000..a3251bca6 --- /dev/null +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_constant.py @@ -0,0 +1,21 @@ +from typing import Any + +import pytest +from helpers import assert_cell_operation_works + +from safeds.data.tabular.containers import Cell + + +@pytest.mark.parametrize( + "value", + [ + None, + 1, + ], + ids=[ + "None", + "int", + ], +) +def test_should_return_constant_value(value: Any) -> None: + assert_cell_operation_works(None, lambda _: Cell.constant(value), value) From d302dfe64a5734ccc17e941242bc582e2a058552 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 17:05:32 +0100 Subject: [PATCH 18/23] chore: check `date` and `time` --- src/safeds/data/tabular/containers/_cell.py | 94 ++++++++++++++++++- .../containers/_lazy_cell/test_date.py | 41 ++++++++ .../containers/_lazy_cell/test_time.py | 60 ++++++++++++ 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 tests/safeds/data/tabular/containers/_lazy_cell/test_date.py create mode 100644 tests/safeds/data/tabular/containers/_lazy_cell/test_time.py diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 6e80aab41..0f486c56f 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -49,6 +49,21 @@ def constant(value: _PythonLiteral | None) -> Cell: ------- cell: The created cell. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [1, 2, None]) + >>> column.transform(lambda _: Cell.constant(3)) + +-----+ + | a | + | --- | + | i32 | + +=====+ + | 3 | + | 3 | + | 3 | + +-----+ """ import polars as pl @@ -65,6 +80,8 @@ def date( """ Create a cell with a date. + Invalid dates are converted to cells with missing values (`None`). + Parameters ---------- year: @@ -78,6 +95,32 @@ def date( ------- cell: The created cell. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [1, 2, None]) + >>> column.transform(lambda _: Cell.date(2025, 1, 15)) + +------------+ + | a | + | --- | + | date | + +============+ + | 2025-01-15 | + | 2025-01-15 | + | 2025-01-15 | + +------------+ + + >>> column.transform(lambda cell: Cell.date(2025, cell, 15)) + +------------+ + | a | + | --- | + | date | + +============+ + | 2025-01-15 | + | 2025-02-15 | + | null | + +------------+ """ import polars as pl @@ -103,6 +146,8 @@ def datetime( """ Create a cell with a datetime. + Invalid datetimes are converted to cells with missing values (`None`). + Parameters ---------- year: @@ -215,6 +260,8 @@ def time( """ Create a cell with a time. + Invalid times are converted to cells with missing values (`None`). + Parameters ---------- hour: @@ -230,6 +277,32 @@ def time( ------- cell: The created cell. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [1, 2, None]) + >>> column.transform(lambda _: Cell.time(12, 0, 0)) + +----------+ + | a | + | --- | + | time | + +==========+ + | 12:00:00 | + | 12:00:00 | + | 12:00:00 | + +----------+ + + >>> column.transform(lambda cell: Cell.time(12, cell, 0, microsecond=1)) + +-----------------+ + | a | + | --- | + | time | + +=================+ + | 12:01:00.000001 | + | 12:02:00.000001 | + | null | + +-----------------+ """ import polars as pl @@ -240,7 +313,10 @@ def time( second = _unwrap(second) microsecond = _unwrap(microsecond) - return _LazyCell(pl.time(hour, minute, second, microsecond)) + # By default, microseconds overflow into seconds + return _LazyCell( + pl.when(microsecond <= 999_999).then(pl.time(hour, minute, second, microsecond)).otherwise(None), + ) @staticmethod def first_not_none(cells: list[Cell[P]]) -> Cell[P | None]: @@ -1139,6 +1215,22 @@ def cast(self, type_: ColumnType) -> Cell: ------- cell: The cast cell. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> from safeds.data.tabular.typing import ColumnType + >>> column = Column("a", [1, 2, None]) + >>> column.transform(lambda cell: cell.cast(ColumnType.string())) + +------+ + | a | + | --- | + | str | + +======+ + | 1 | + | 2 | + | null | + +------+ """ # ------------------------------------------------------------------------------------------------------------------ diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py new file mode 100644 index 000000000..92a96178e --- /dev/null +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py @@ -0,0 +1,41 @@ +from datetime import date + +import pytest +from helpers import assert_cell_operation_works + +from safeds._typing import _ConvertibleToIntCell +from safeds.data.tabular.containers import Cell + + +@pytest.mark.parametrize( + ("year", "month", "day", "expected"), + [ + (2025, 1, 1, date(2025, 1, 1)), + (Cell.constant(2025), Cell.constant(1), Cell.constant(1), date(2025, 1, 1)), + (None, 1, 1, None), + (2025, None, 1, None), + (2025, 0, 1, None), + (2025, 13, 1, None), + (2025, 1, None, None), + (2025, 1, 0, None), + (2025, 1, 32, None), + ], + ids=[ + "int components", + "cell components", + "year is None", + "month is None", + "month is too low", + "month is too high", + "day is None", + "day is too low", + "day is too high", + ], +) +def test_should_return_constant_value( + year: _ConvertibleToIntCell, + month: _ConvertibleToIntCell, + day: _ConvertibleToIntCell, + expected: date, +) -> None: + assert_cell_operation_works(None, lambda _: Cell.date(year, month, day), expected) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py new file mode 100644 index 000000000..90cee336f --- /dev/null +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py @@ -0,0 +1,60 @@ +from datetime import time + +import pytest +from helpers import assert_cell_operation_works + +from safeds._typing import _ConvertibleToIntCell +from safeds.data.tabular.containers import Cell + + +@pytest.mark.parametrize( + ("hour", "minute", "second", "microsecond", "expected"), + [ + (1, 2, 3, 4, time(1, 2, 3, 4)), + (Cell.constant(1), Cell.constant(2), Cell.constant(3), Cell.constant(4), time(1, 2, 3, 4)), + # invalid hour + (None, 2, 3, 4, None), + (-1, 2, 3, 4, None), + (24, 2, 3, 4, None), + # invalid minute + (1, None, 3, 4, None), + (1, -1, 3, 4, None), + (1, 60, 3, 4, None), + # invalid second + (1, 2, None, 4, None), + (1, 2, -1, 4, None), + (1, 2, 60, 4, None), + # invalid microsecond + (1, 2, 3, None, None), + (1, 2, 3, -1, None), + (1, 2, 3, 1_000_000, None), + ], + ids=[ + "int components", + "cell components", + "hour is None", + "hour is too low", + "hour is too high", + "minute is None", + "minute is too low", + "minute is too high", + "second is None", + "second is too low", + "second is too high", + "microsecond is None", + "microsecond is too low", + "microsecond is too high", + ], +) +def test_should_return_constant_value( + hour: _ConvertibleToIntCell, + minute: _ConvertibleToIntCell, + second: _ConvertibleToIntCell, + microsecond: _ConvertibleToIntCell, + expected: time, +) -> None: + assert_cell_operation_works( + None, + lambda _: Cell.time(hour, minute, second, microsecond=microsecond), + expected, + ) From 2420769a7e40500b28530803d8f204f60f277067 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 17:40:14 +0100 Subject: [PATCH 19/23] chore: check `datetime` --- src/safeds/data/tabular/containers/_cell.py | 72 ++++++++++++-- .../containers/_lazy_cell/test_date.py | 21 ++-- .../containers/_lazy_cell/test_datetime.py | 97 +++++++++++++++++++ 3 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index 0f486c56f..a3b6a34d1 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -80,7 +80,7 @@ def date( """ Create a cell with a date. - Invalid dates are converted to cells with missing values (`None`). + Invalid dates are converted to missing values (`None`). Parameters ---------- @@ -146,7 +146,7 @@ def datetime( """ Create a cell with a datetime. - Invalid datetimes are converted to cells with missing values (`None`). + Invalid datetimes are converted to missing values (`None`). Parameters ---------- @@ -169,6 +169,32 @@ def datetime( ------- cell: The created cell. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [1, 2, None]) + >>> column.transform(lambda _: Cell.datetime(2025, 1, 15, hour=12)) + +---------------------+ + | a | + | --- | + | datetime[μs] | + +=====================+ + | 2025-01-15 12:00:00 | + | 2025-01-15 12:00:00 | + | 2025-01-15 12:00:00 | + +---------------------+ + + >>> column.transform(lambda cell: Cell.datetime(2025, 1, 15, hour=cell)) + +---------------------+ + | a | + | --- | + | datetime[μs] | + +=====================+ + | 2025-01-15 01:00:00 | + | 2025-01-15 02:00:00 | + | null | + +---------------------+ """ import polars as pl @@ -182,7 +208,12 @@ def datetime( second = _unwrap(second) microsecond = _unwrap(microsecond) - return _LazyCell(pl.datetime(year, month, day, hour, minute, second, microsecond)) + # By default, microseconds overflow into seconds + return _LazyCell( + pl.when(microsecond <= 999_999) + .then(pl.datetime(year, month, day, hour, minute, second, microsecond)) + .otherwise(None), + ) @staticmethod def duration( @@ -194,11 +225,12 @@ def duration( seconds: _ConvertibleToIntCell = 0, milliseconds: _ConvertibleToIntCell = 0, microseconds: _ConvertibleToIntCell = 0, - nanoseconds: _ConvertibleToIntCell = 0, ) -> Cell[python_datetime.timedelta | None]: """ Create a cell with a duration. + Invalid durations are converted to missing values (`None`). + Parameters ---------- weeks: @@ -215,13 +247,37 @@ def duration( The number of milliseconds. microseconds: The number of microseconds. - nanoseconds: - The number of nanoseconds. Returns ------- cell: The created cell. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [1, 2, None]) + >>> column.transform(lambda _: Cell.duration(hours=1)) + +--------------+ + | a | + | --- | + | duration[μs] | + +==============+ + | 1h | + | 1h | + | 1h | + +--------------+ + + >>> column.transform(lambda cell: Cell.duration(hours = cell)) + +--------------+ + | a | + | --- | + | duration[μs] | + +==============+ + | 1h | + | 2h | + | null | + +--------------+ """ import polars as pl @@ -234,7 +290,6 @@ def duration( seconds = _unwrap(seconds) milliseconds = _unwrap(milliseconds) microseconds = _unwrap(microseconds) - nanoseconds = _unwrap(nanoseconds) return _LazyCell( pl.duration( @@ -245,7 +300,6 @@ def duration( seconds=seconds, milliseconds=milliseconds, microseconds=microseconds, - nanoseconds=nanoseconds, ), ) @@ -260,7 +314,7 @@ def time( """ Create a cell with a time. - Invalid times are converted to cells with missing values (`None`). + Invalid times are converted to missing values (`None`). Parameters ---------- diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py index 92a96178e..aa6a35b2f 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py @@ -10,15 +10,18 @@ @pytest.mark.parametrize( ("year", "month", "day", "expected"), [ - (2025, 1, 1, date(2025, 1, 1)), - (Cell.constant(2025), Cell.constant(1), Cell.constant(1), date(2025, 1, 1)), - (None, 1, 1, None), - (2025, None, 1, None), - (2025, 0, 1, None), - (2025, 13, 1, None), - (2025, 1, None, None), - (2025, 1, 0, None), - (2025, 1, 32, None), + (1, 2, 3, date(1, 2, 3)), + (Cell.constant(1), Cell.constant(2), Cell.constant(3), date(1, 2, 3)), + # invalid year + (None, 2, 3, None), + # invalid month + (1, None, 3, None), + (1, 0, 3, None), + (1, 13, 3, None), + # invalid day + (1, 2, None, None), + (1, 2, 0, None), + (1, 2, 32, None), ], ids=[ "int components", diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py new file mode 100644 index 000000000..152063d87 --- /dev/null +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py @@ -0,0 +1,97 @@ +from datetime import datetime + +import pytest +from helpers import assert_cell_operation_works + +from safeds._typing import _ConvertibleToIntCell +from safeds.data.tabular.containers import Cell + + +@pytest.mark.parametrize( + ("year", "month", "day", "hour", "minute", "second", "microsecond", "expected"), + [ + (1, 2, 3, 4, 5, 6, 7, datetime(1, 2, 3, 4, 5, 6, 7)), # noqa: DTZ001 + ( + Cell.constant(1), + Cell.constant(2), + Cell.constant(3), + Cell.constant(4), + Cell.constant(5), + Cell.constant(6), + Cell.constant(7), + datetime(1, 2, 3, 4, 5, 6, 7), # noqa: DTZ001 + ), + # invalid year + (None, 2, 3, 4, 5, 6, 7, None), + # invalid month + (1, None, 3, 4, 5, 6, 7, None), + (1, 0, 3, 4, 5, 6, 7, None), + (1, 13, 3, 4, 5, 6, 7, None), + # invalid day + (1, 2, None, 4, 5, 6, 7, None), + (1, 2, 0, 4, 5, 6, 7, None), + (1, 2, 32, 4, 5, 6, 7, None), + # invalid hour + (1, 2, 3, None, 5, 6, 7, None), + (1, 2, 3, -1, 5, 6, 7, None), + (1, 2, 3, 24, 5, 6, 7, None), + # invalid minute + (1, 2, 3, 4, None, 6, 7, None), + (1, 2, 3, 4, -1, 6, 7, None), + (1, 2, 3, 4, 60, 6, 7, None), + # invalid second + (1, 2, 3, 4, 5, None, 7, None), + (1, 2, 3, 4, 5, -1, 7, None), + (1, 2, 3, 4, 5, 60, 7, None), + # invalid microsecond + (1, 2, 3, 4, 5, 6, None, None), + (1, 2, 3, 4, 5, 6, -1, None), + (1, 2, 3, 4, 5, 6, 1_000_000, None), + ], + ids=[ + "int components", + "cell components", + "year is None", + "month is None", + "month is too low", + "month is too high", + "day is None", + "day is too low", + "day is too high", + "hour is None", + "hour is too low", + "hour is too high", + "minute is None", + "minute is too low", + "minute is too high", + "second is None", + "second is too low", + "second is too high", + "microsecond is None", + "microsecond is too low", + "microsecond is too high", + ], +) +def test_should_return_constant_value( + year: _ConvertibleToIntCell, + month: _ConvertibleToIntCell, + day: _ConvertibleToIntCell, + hour: _ConvertibleToIntCell, + minute: _ConvertibleToIntCell, + second: _ConvertibleToIntCell, + microsecond: _ConvertibleToIntCell, + expected: datetime, +) -> None: + assert_cell_operation_works( + None, + lambda _: Cell.datetime( + year, + month, + day, + hour=hour, + minute=minute, + second=second, + microsecond=microsecond, + ), + expected, + ) From 1a0ae7796b9876d2b6d27ce481be62799107ddef Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 17:46:00 +0100 Subject: [PATCH 20/23] chore: check `duration` --- .../containers/_lazy_cell/test_date.py | 2 +- .../containers/_lazy_cell/test_datetime.py | 13 ++- .../containers/_lazy_cell/test_duration.py | 86 +++++++++++++++++++ .../containers/_lazy_cell/test_time.py | 2 +- 4 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tests/safeds/data/tabular/containers/_lazy_cell/test_duration.py diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py index aa6a35b2f..206a34bdf 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py @@ -35,7 +35,7 @@ "day is too high", ], ) -def test_should_return_constant_value( +def test_should_return_date( year: _ConvertibleToIntCell, month: _ConvertibleToIntCell, day: _ConvertibleToIntCell, diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py index 152063d87..84723cabd 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py @@ -10,7 +10,16 @@ @pytest.mark.parametrize( ("year", "month", "day", "hour", "minute", "second", "microsecond", "expected"), [ - (1, 2, 3, 4, 5, 6, 7, datetime(1, 2, 3, 4, 5, 6, 7)), # noqa: DTZ001 + ( + 1, + 2, + 3, + 4, + 5, + 6, + 7, + datetime(1, 2, 3, 4, 5, 6, 7), # noqa: DTZ001 + ), ( Cell.constant(1), Cell.constant(2), @@ -72,7 +81,7 @@ "microsecond is too high", ], ) -def test_should_return_constant_value( +def test_should_return_datetime( year: _ConvertibleToIntCell, month: _ConvertibleToIntCell, day: _ConvertibleToIntCell, diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_duration.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_duration.py new file mode 100644 index 000000000..3645a5ee1 --- /dev/null +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_duration.py @@ -0,0 +1,86 @@ +from datetime import timedelta + +import pytest +from helpers import assert_cell_operation_works + +from safeds._typing import _ConvertibleToIntCell +from safeds.data.tabular.containers import Cell + + +@pytest.mark.parametrize( + ("weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds", "expected"), + [ + ( + 1, + 2, + 3, + 4, + 5, + 6, + 7, + timedelta(weeks=1, days=2, hours=3, minutes=4, seconds=5, milliseconds=6, microseconds=7), + ), + ( + -1, + -2, + -3, + -4, + -5, + -6, + -7, + timedelta(weeks=-1, days=-2, hours=-3, minutes=-4, seconds=-5, milliseconds=-6, microseconds=-7), + ), + ( + Cell.constant(1), + Cell.constant(2), + Cell.constant(3), + Cell.constant(4), + Cell.constant(5), + Cell.constant(6), + Cell.constant(7), + timedelta(weeks=1, days=2, hours=3, minutes=4, seconds=5, milliseconds=6, microseconds=7), + ), + (None, 2, 3, 4, 5, 6, 7, None), + (1, None, 3, 4, 5, 6, 7, None), + (1, 2, None, 4, 5, 6, 7, None), + (1, 2, 3, None, 5, 6, 7, None), + (1, 2, 3, 4, None, 6, 7, None), + (1, 2, 3, 4, 5, None, 7, None), + (1, 2, 3, 4, 5, 6, None, None), + ], + ids=[ + "positive int components", + "negative int components", + "cell components", + "weeks is None", + "days is None", + "hours is None", + "minutes is None", + "seconds is None", + "microseconds is None", + "milliseconds is None", + ], +) +def test_should_return_duration( + weeks: _ConvertibleToIntCell, + days: _ConvertibleToIntCell, + hours: _ConvertibleToIntCell, + minutes: _ConvertibleToIntCell, + seconds: _ConvertibleToIntCell, + milliseconds: _ConvertibleToIntCell, + microseconds: _ConvertibleToIntCell, + expected: timedelta, +) -> None: + assert_cell_operation_works( + None, + lambda _: Cell.duration( + weeks=weeks, + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + milliseconds=milliseconds, + microseconds=microseconds, + ), + expected, + ) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py index 90cee336f..342b1b784 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py @@ -46,7 +46,7 @@ "microsecond is too high", ], ) -def test_should_return_constant_value( +def test_should_return_time( hour: _ConvertibleToIntCell, minute: _ConvertibleToIntCell, second: _ConvertibleToIntCell, From 1103dc25c0a1651f7b7b5456cb93c411840051fe Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 17:50:53 +0100 Subject: [PATCH 21/23] fix: wrong import --- tests/safeds/data/tabular/containers/_lazy_cell/test_cast.py | 2 +- .../safeds/data/tabular/containers/_lazy_cell/test_constant.py | 2 +- tests/safeds/data/tabular/containers/_lazy_cell/test_date.py | 2 +- .../safeds/data/tabular/containers/_lazy_cell/test_datetime.py | 2 +- .../safeds/data/tabular/containers/_lazy_cell/test_duration.py | 2 +- tests/safeds/data/tabular/containers/_lazy_cell/test_time.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_cast.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_cast.py index 3a5a7e188..37528c696 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_cast.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_cast.py @@ -1,9 +1,9 @@ from typing import Any import pytest -from helpers import assert_cell_operation_works from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works @pytest.mark.parametrize( diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_constant.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_constant.py index a3251bca6..1be74f34a 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_constant.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_constant.py @@ -1,9 +1,9 @@ from typing import Any import pytest -from helpers import assert_cell_operation_works from safeds.data.tabular.containers import Cell +from tests.helpers import assert_cell_operation_works @pytest.mark.parametrize( diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py index 206a34bdf..0e0374f7c 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py @@ -1,7 +1,7 @@ from datetime import date import pytest -from helpers import assert_cell_operation_works +from tests.helpers import assert_cell_operation_works from safeds._typing import _ConvertibleToIntCell from safeds.data.tabular.containers import Cell diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py index 84723cabd..dcab1d935 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_datetime.py @@ -1,10 +1,10 @@ from datetime import datetime import pytest -from helpers import assert_cell_operation_works from safeds._typing import _ConvertibleToIntCell from safeds.data.tabular.containers import Cell +from tests.helpers import assert_cell_operation_works @pytest.mark.parametrize( diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_duration.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_duration.py index 3645a5ee1..1d0784353 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_duration.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_duration.py @@ -1,10 +1,10 @@ from datetime import timedelta import pytest -from helpers import assert_cell_operation_works from safeds._typing import _ConvertibleToIntCell from safeds.data.tabular.containers import Cell +from tests.helpers import assert_cell_operation_works @pytest.mark.parametrize( diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py index 342b1b784..34e5c3367 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_time.py @@ -1,10 +1,10 @@ from datetime import time import pytest -from helpers import assert_cell_operation_works from safeds._typing import _ConvertibleToIntCell from safeds.data.tabular.containers import Cell +from tests.helpers import assert_cell_operation_works @pytest.mark.parametrize( From e4b84d742ee6ac685ad863643247d42645e7c593 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 15 Jan 2025 18:00:12 +0100 Subject: [PATCH 22/23] fix: mypy errors --- src/safeds/data/tabular/containers/_cell.py | 72 ++++++++++----------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/src/safeds/data/tabular/containers/_cell.py b/src/safeds/data/tabular/containers/_cell.py index a3b6a34d1..0cc022b45 100644 --- a/src/safeds/data/tabular/containers/_cell.py +++ b/src/safeds/data/tabular/containers/_cell.py @@ -126,11 +126,13 @@ def date( from ._lazy_cell import _LazyCell # circular import - year = _unwrap(year) - month = _unwrap(month) - day = _unwrap(day) - - return _LazyCell(pl.date(year, month, day)) + return _LazyCell( + pl.date( + year=_unwrap(year), + month=_unwrap(month), + day=_unwrap(day), + ), + ) @staticmethod def datetime( @@ -200,18 +202,18 @@ def datetime( from ._lazy_cell import _LazyCell # circular import - year = _unwrap(year) - month = _unwrap(month) - day = _unwrap(day) - hour = _unwrap(hour) - minute = _unwrap(minute) - second = _unwrap(second) - microsecond = _unwrap(microsecond) + pl_year = _unwrap(year) + pl_month = _unwrap(month) + pl_day = _unwrap(day) + pl_hour = _unwrap(hour) + pl_minute = _unwrap(minute) + pl_second = _unwrap(second) + pl_microsecond = _unwrap(microsecond) # By default, microseconds overflow into seconds return _LazyCell( - pl.when(microsecond <= 999_999) - .then(pl.datetime(year, month, day, hour, minute, second, microsecond)) + pl.when(pl_microsecond <= 999_999) + .then(pl.datetime(pl_year, pl_month, pl_day, pl_hour, pl_minute, pl_second, pl_microsecond)) .otherwise(None), ) @@ -283,33 +285,25 @@ def duration( from ._lazy_cell import _LazyCell # circular import - weeks = _unwrap(weeks) - days = _unwrap(days) - hours = _unwrap(hours) - minutes = _unwrap(minutes) - seconds = _unwrap(seconds) - milliseconds = _unwrap(milliseconds) - microseconds = _unwrap(microseconds) - return _LazyCell( pl.duration( - weeks=weeks, - days=days, - hours=hours, - minutes=minutes, - seconds=seconds, - milliseconds=milliseconds, - microseconds=microseconds, + weeks=_unwrap(weeks), + days=_unwrap(days), + hours=_unwrap(hours), + minutes=_unwrap(minutes), + seconds=_unwrap(seconds), + milliseconds=_unwrap(milliseconds), + microseconds=_unwrap(microseconds), ), ) @staticmethod def time( - hour: int | Cell[int], - minute: int | Cell[int], - second: int | Cell[int], + hour: _ConvertibleToIntCell, + minute: _ConvertibleToIntCell, + second: _ConvertibleToIntCell, *, - microsecond: int | Cell[int] = 0, + microsecond: _ConvertibleToIntCell = 0, ) -> Cell[python_datetime.time | None]: """ Create a cell with a time. @@ -362,14 +356,16 @@ def time( from ._lazy_cell import _LazyCell # circular import - hour = _unwrap(hour) - minute = _unwrap(minute) - second = _unwrap(second) - microsecond = _unwrap(microsecond) + pl_hour = _unwrap(hour) + pl_minute = _unwrap(minute) + pl_second = _unwrap(second) + pl_microsecond = _unwrap(microsecond) # By default, microseconds overflow into seconds return _LazyCell( - pl.when(microsecond <= 999_999).then(pl.time(hour, minute, second, microsecond)).otherwise(None), + pl.when(pl_microsecond <= 999_999) + .then(pl.time(pl_hour, pl_minute, pl_second, pl_microsecond)) + .otherwise(None), ) @staticmethod From a70c63506829749d12ab846b17e51bd4d827eb40 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:01:59 +0000 Subject: [PATCH 23/23] style: apply automated linter fixes --- tests/safeds/data/tabular/containers/_lazy_cell/test_date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py index 0e0374f7c..e052f1e91 100644 --- a/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py +++ b/tests/safeds/data/tabular/containers/_lazy_cell/test_date.py @@ -1,10 +1,10 @@ from datetime import date import pytest -from tests.helpers import assert_cell_operation_works from safeds._typing import _ConvertibleToIntCell from safeds.data.tabular.containers import Cell +from tests.helpers import assert_cell_operation_works @pytest.mark.parametrize(