From 46d9f4a54a7961b865168b83b181c05df9762368 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Mar 2024 16:59:11 -0700 Subject: [PATCH] Allow users to pass in a timezone for automatic dtstart calculations. --- ical/store.py | 7 +++++-- tests/test_store.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/ical/store.py b/ical/store.py index 0f031ec..716d4e0 100644 --- a/ical/store.py +++ b/ical/store.py @@ -164,12 +164,14 @@ def __init__( timezones: list[Timezone], exc: type[StoreError], dtstamp_fn: Callable[[], datetime.datetime] = lambda: dtstamp_factory(), + tzinfo: datetime.tzinfo | None = None, ): """Initialize the EventStore.""" self._items = items self._timezones = timezones self._exc = exc self._dtstamp_fn = dtstamp_fn + self._tzinfo = tzinfo or local_timezone() def add(self, item: _T) -> _T: """Add the specified item to the calendar. @@ -190,7 +192,7 @@ def add(self, item: _T) -> _T: if item.due: update["dtstart"] = item.due - datetime.timedelta(days=1) else: - update["dtstart"] = datetime.datetime.now(tz=local_timezone()) + update["dtstart"] = datetime.datetime.now(tz=self._tzinfo) new_item = cast(_T, item.copy_and_validate(update=update)) # The store can only manage cascading deletes for some relationship types @@ -467,6 +469,7 @@ def __init__( calendar.timezones, EventStoreError, dtstamp_fn, + tzinfo=None, ) @@ -485,9 +488,9 @@ def __init__( calendar.timezones, TodoStoreError, dtstamp_fn, + tzinfo=tzinfo, ) self._calendar = calendar - self._tzinfo = tzinfo or local_timezone() def todo_list(self, dtstart: datetime.datetime | None = None) -> Iterable[Todo]: """Return a list of all todos on the calendar. diff --git a/tests/test_store.py b/tests/test_store.py index 0e7cf1e..5fd868b 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -23,6 +23,7 @@ from ical.types.recur import Range, Recur from ical.types import RelationshipType, RelatedTo +TZ = zoneinfo.ZoneInfo("America/Los_Angeles") @pytest.fixture(name="calendar") def mock_calendar() -> Calendar: @@ -39,7 +40,7 @@ def mock_store(calendar: Calendar) -> EventStore: @pytest.fixture(name="todo_store") def mock_todo_store(calendar: Calendar) -> TodoStore: """Fixture to create an event store.""" - return TodoStore(calendar) + return TodoStore(calendar, tzinfo=TZ) @pytest.fixture(name="_uid", autouse=True) @@ -1364,7 +1365,6 @@ def test_modify_todo_rrule_for_this_and_future( def test_modify_todo_due_without_dtstart( calendar: Calendar, todo_store: TodoStore, - snapshot: SnapshotAssertion, ) -> None: """Validate that a due date modification without updating dtstart will be repaired.""" # Create a recurring to-do item to wash the card every Saturday @@ -1391,3 +1391,32 @@ def test_modify_todo_due_without_dtstart( assert todo.due == datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc) assert isinstance(todo.dtstart, datetime.datetime) assert todo.dtstart < todo.due + + +@pytest.mark.parametrize( + ("due", "expected_tz"), + [ + (None, TZ), + ("2024-01-07T10:00:00Z", datetime.timezone.utc), + ("2024-01-07T10:00:00-05:00", zoneinfo.ZoneInfo("America/New_York")), + ], +) +def test_dtstart_timezone( + calendar: Calendar, + todo_store: TodoStore, + due: str | None, + expected_tz: zoneinfo.ZoneInfo, +) -> None: + """Validate that a due date modification without updating dtstart will be repaired.""" + # Create a recurring to-do item to wash the card every Saturday + todo_store.add( + Todo( + summary="Wash car", + ) + ) + todos = list(todo_store.todo_list()) + assert len(todos) == 1 + todo = todos[0] + assert todo.due is None + assert todo.dtstart.tzinfo == TZ + \ No newline at end of file