Skip to content

Commit 76ce112

Browse files
authored
add datetime var comparison operations (#4406)
* add datetime var operations * add future annotations * add LiteralDatetimeVar * remove methods that don't apply * fix serialization * add unit and integrations test * oops, forgot to commit that important change
1 parent 682bca7 commit 76ce112

File tree

4 files changed

+311
-0
lines changed

4 files changed

+311
-0
lines changed

reflex/vars/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .base import get_uuid_string_var as get_uuid_string_var
1010
from .base import var_operation as var_operation
1111
from .base import var_operation_return as var_operation_return
12+
from .datetime import DateTimeVar as DateTimeVar
1213
from .function import FunctionStringVar as FunctionStringVar
1314
from .function import FunctionVar as FunctionVar
1415
from .function import VarOperationCall as VarOperationCall

reflex/vars/datetime.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
"""Immutable datetime and date vars."""
2+
3+
from __future__ import annotations
4+
5+
import dataclasses
6+
import sys
7+
from datetime import date, datetime
8+
from typing import Any, NoReturn, TypeVar, Union, overload
9+
10+
from reflex.utils.exceptions import VarTypeError
11+
from reflex.vars.number import BooleanVar
12+
13+
from .base import (
14+
CustomVarOperationReturn,
15+
LiteralVar,
16+
Var,
17+
VarData,
18+
var_operation,
19+
var_operation_return,
20+
)
21+
22+
DATETIME_T = TypeVar("DATETIME_T", datetime, date)
23+
24+
datetime_types = Union[datetime, date]
25+
26+
27+
def raise_var_type_error():
28+
"""Raise a VarTypeError.
29+
30+
Raises:
31+
VarTypeError: Cannot compare a datetime object with a non-datetime object.
32+
"""
33+
raise VarTypeError("Cannot compare a datetime object with a non-datetime object.")
34+
35+
36+
class DateTimeVar(Var[DATETIME_T], python_types=(datetime, date)):
37+
"""A variable that holds a datetime or date object."""
38+
39+
@overload
40+
def __lt__(self, other: datetime_types) -> BooleanVar: ...
41+
42+
@overload
43+
def __lt__(self, other: NoReturn) -> NoReturn: ...
44+
45+
def __lt__(self, other: Any):
46+
"""Less than comparison.
47+
48+
Args:
49+
other: The other datetime to compare.
50+
51+
Returns:
52+
The result of the comparison.
53+
"""
54+
if not isinstance(other, DATETIME_TYPES):
55+
raise_var_type_error()
56+
return date_lt_operation(self, other)
57+
58+
@overload
59+
def __le__(self, other: datetime_types) -> BooleanVar: ...
60+
61+
@overload
62+
def __le__(self, other: NoReturn) -> NoReturn: ...
63+
64+
def __le__(self, other: Any):
65+
"""Less than or equal comparison.
66+
67+
Args:
68+
other: The other datetime to compare.
69+
70+
Returns:
71+
The result of the comparison.
72+
"""
73+
if not isinstance(other, DATETIME_TYPES):
74+
raise_var_type_error()
75+
return date_le_operation(self, other)
76+
77+
@overload
78+
def __gt__(self, other: datetime_types) -> BooleanVar: ...
79+
80+
@overload
81+
def __gt__(self, other: NoReturn) -> NoReturn: ...
82+
83+
def __gt__(self, other: Any):
84+
"""Greater than comparison.
85+
86+
Args:
87+
other: The other datetime to compare.
88+
89+
Returns:
90+
The result of the comparison.
91+
"""
92+
if not isinstance(other, DATETIME_TYPES):
93+
raise_var_type_error()
94+
return date_gt_operation(self, other)
95+
96+
@overload
97+
def __ge__(self, other: datetime_types) -> BooleanVar: ...
98+
99+
@overload
100+
def __ge__(self, other: NoReturn) -> NoReturn: ...
101+
102+
def __ge__(self, other: Any):
103+
"""Greater than or equal comparison.
104+
105+
Args:
106+
other: The other datetime to compare.
107+
108+
Returns:
109+
The result of the comparison.
110+
"""
111+
if not isinstance(other, DATETIME_TYPES):
112+
raise_var_type_error()
113+
return date_ge_operation(self, other)
114+
115+
116+
@var_operation
117+
def date_gt_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn:
118+
"""Greater than comparison.
119+
120+
Args:
121+
lhs: The left-hand side of the operation.
122+
rhs: The right-hand side of the operation.
123+
124+
Returns:
125+
The result of the operation.
126+
"""
127+
return date_compare_operation(rhs, lhs, strict=True)
128+
129+
130+
@var_operation
131+
def date_lt_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn:
132+
"""Less than comparison.
133+
134+
Args:
135+
lhs: The left-hand side of the operation.
136+
rhs: The right-hand side of the operation.
137+
138+
Returns:
139+
The result of the operation.
140+
"""
141+
return date_compare_operation(lhs, rhs, strict=True)
142+
143+
144+
@var_operation
145+
def date_le_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn:
146+
"""Less than or equal comparison.
147+
148+
Args:
149+
lhs: The left-hand side of the operation.
150+
rhs: The right-hand side of the operation.
151+
152+
Returns:
153+
The result of the operation.
154+
"""
155+
return date_compare_operation(lhs, rhs)
156+
157+
158+
@var_operation
159+
def date_ge_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn:
160+
"""Greater than or equal comparison.
161+
162+
Args:
163+
lhs: The left-hand side of the operation.
164+
rhs: The right-hand side of the operation.
165+
166+
Returns:
167+
The result of the operation.
168+
"""
169+
return date_compare_operation(rhs, lhs)
170+
171+
172+
def date_compare_operation(
173+
lhs: DateTimeVar[DATETIME_T] | Any,
174+
rhs: DateTimeVar[DATETIME_T] | Any,
175+
strict: bool = False,
176+
) -> CustomVarOperationReturn:
177+
"""Check if the value is less than the other value.
178+
179+
Args:
180+
lhs: The left-hand side of the operation.
181+
rhs: The right-hand side of the operation.
182+
strict: Whether to use strict comparison.
183+
184+
Returns:
185+
The result of the operation.
186+
"""
187+
return var_operation_return(
188+
f"({lhs} { '<' if strict else '<='} {rhs})",
189+
bool,
190+
)
191+
192+
193+
@dataclasses.dataclass(
194+
eq=False,
195+
frozen=True,
196+
**{"slots": True} if sys.version_info >= (3, 10) else {},
197+
)
198+
class LiteralDatetimeVar(LiteralVar, DateTimeVar):
199+
"""Base class for immutable datetime and date vars."""
200+
201+
_var_value: datetime | date = dataclasses.field(default=datetime.now())
202+
203+
@classmethod
204+
def create(cls, value: datetime | date, _var_data: VarData | None = None):
205+
"""Create a new instance of the class.
206+
207+
Args:
208+
value: The value to set.
209+
210+
Returns:
211+
LiteralDatetimeVar: The new instance of the class.
212+
"""
213+
js_expr = f'"{str(value)}"'
214+
return cls(
215+
_js_expr=js_expr,
216+
_var_type=type(value),
217+
_var_value=value,
218+
_var_data=_var_data,
219+
)
220+
221+
222+
DATETIME_TYPES = (datetime, date, DateTimeVar)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from typing import Generator
2+
3+
import pytest
4+
from playwright.sync_api import Page, expect
5+
6+
from reflex.testing import AppHarness
7+
8+
9+
def DatetimeOperationsApp():
10+
from datetime import datetime
11+
12+
import reflex as rx
13+
14+
class DtOperationsState(rx.State):
15+
date1: datetime = datetime(2021, 1, 1)
16+
date2: datetime = datetime(2031, 1, 1)
17+
date3: datetime = datetime(2021, 1, 1)
18+
19+
app = rx.App(state=DtOperationsState)
20+
21+
@app.add_page
22+
def index():
23+
return rx.vstack(
24+
rx.text(DtOperationsState.date1, id="date1"),
25+
rx.text(DtOperationsState.date2, id="date2"),
26+
rx.text(DtOperationsState.date3, id="date3"),
27+
rx.text("Operations between date1 and date2"),
28+
rx.text(DtOperationsState.date1 == DtOperationsState.date2, id="1_eq_2"),
29+
rx.text(DtOperationsState.date1 != DtOperationsState.date2, id="1_neq_2"),
30+
rx.text(DtOperationsState.date1 < DtOperationsState.date2, id="1_lt_2"),
31+
rx.text(DtOperationsState.date1 <= DtOperationsState.date2, id="1_le_2"),
32+
rx.text(DtOperationsState.date1 > DtOperationsState.date2, id="1_gt_2"),
33+
rx.text(DtOperationsState.date1 >= DtOperationsState.date2, id="1_ge_2"),
34+
rx.text("Operations between date1 and date3"),
35+
rx.text(DtOperationsState.date1 == DtOperationsState.date3, id="1_eq_3"),
36+
rx.text(DtOperationsState.date1 != DtOperationsState.date3, id="1_neq_3"),
37+
rx.text(DtOperationsState.date1 < DtOperationsState.date3, id="1_lt_3"),
38+
rx.text(DtOperationsState.date1 <= DtOperationsState.date3, id="1_le_3"),
39+
rx.text(DtOperationsState.date1 > DtOperationsState.date3, id="1_gt_3"),
40+
rx.text(DtOperationsState.date1 >= DtOperationsState.date3, id="1_ge_3"),
41+
)
42+
43+
44+
@pytest.fixture()
45+
def datetime_operations_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
46+
"""Start Table app at tmp_path via AppHarness.
47+
48+
Args:
49+
tmp_path_factory: pytest tmp_path_factory fixture
50+
51+
Yields:
52+
running AppHarness instance
53+
54+
"""
55+
with AppHarness.create(
56+
root=tmp_path_factory.mktemp("datetime_operations_app"),
57+
app_source=DatetimeOperationsApp, # type: ignore
58+
) as harness:
59+
assert harness.app_instance is not None, "app is not running"
60+
yield harness
61+
62+
63+
def test_datetime_operations(datetime_operations_app: AppHarness, page: Page):
64+
assert datetime_operations_app.frontend_url is not None
65+
66+
page.goto(datetime_operations_app.frontend_url)
67+
expect(page).to_have_url(datetime_operations_app.frontend_url + "/")
68+
# Check the actual values
69+
expect(page.locator("id=date1")).to_have_text("2021-01-01 00:00:00")
70+
expect(page.locator("id=date2")).to_have_text("2031-01-01 00:00:00")
71+
expect(page.locator("id=date3")).to_have_text("2021-01-01 00:00:00")
72+
73+
# Check the operations between date1 and date2
74+
expect(page.locator("id=1_eq_2")).to_have_text("false")
75+
expect(page.locator("id=1_neq_2")).to_have_text("true")
76+
expect(page.locator("id=1_lt_2")).to_have_text("true")
77+
expect(page.locator("id=1_le_2")).to_have_text("true")
78+
expect(page.locator("id=1_gt_2")).to_have_text("false")
79+
expect(page.locator("id=1_ge_2")).to_have_text("false")
80+
81+
# Check the operations between date1 and date3
82+
expect(page.locator("id=1_eq_3")).to_have_text("true")
83+
expect(page.locator("id=1_neq_3")).to_have_text("false")
84+
expect(page.locator("id=1_lt_3")).to_have_text("false")
85+
expect(page.locator("id=1_le_3")).to_have_text("true")
86+
expect(page.locator("id=1_gt_3")).to_have_text("false")
87+
expect(page.locator("id=1_ge_3")).to_have_text("true")

tests/units/utils/test_serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def test_serialize(value: Any, expected: str):
222222
'"2021-01-01 01:01:01.000001"',
223223
True,
224224
),
225+
(datetime.date(2021, 1, 1), '"2021-01-01"', True),
225226
(Color(color="slate", shade=1), '"var(--slate-1)"', True),
226227
(BaseSubclass, '"BaseSubclass"', True),
227228
(Path("."), '"."', True),

0 commit comments

Comments
 (0)