Skip to content

Commit

Permalink
Add APIs for overriding the local timezone (#121)
Browse files Browse the repository at this point in the history
* Add utility for overriding the local timezone

* Rename context manager to use_local_timezone
  • Loading branch information
allenporter authored Oct 6, 2022
1 parent d9c38f2 commit 4df2138
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 7 deletions.
61 changes: 54 additions & 7 deletions ical/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@

import datetime
import uuid
from collections.abc import Generator
from contextlib import contextmanager
from contextvars import ContextVar

__all__ = [
"dtstamp_factory",
"uid_factory",
"local_timezone",
"use_local_timezone",
"dtstamp_factory",
"uid_factory",
]


MIDNIGHT = datetime.time()
LOCAL_TZ = ContextVar[datetime.tzinfo]("_local_tz")


def dtstamp_factory() -> datetime.datetime:
Expand All @@ -25,12 +29,55 @@ def uid_factory() -> str:
return str(uuid.uuid1())


@contextmanager
def use_local_timezone(local_tz: datetime.tzinfo) -> Generator[None, None, None]:
"""Set the local timezone to use when converting a date to datetime.
This is expected to be used as a context manager when the default timezone
used by python is not the timezone to be used for calendar operations (the
attendees local timezone).
Example:
```
import datetime
import zoneinfo
from ical.calendar import Calendar
from ical.event import Event
from ical.util import use_local_timezone
cal = Calendar()
cal.events.append(
Event(
summary="Example",
start=datetime.date(2022, 2, 1),
end=datetime.date(2022, 2, 2)
)
)
# Use UTC-8 as local timezone
with use_local_timezone(zoneinfo.ZoneInfo("America/Los_Angeles")):
# Returns event above
events = cal.timeline.start_after(
datetime.datetime(2022, 2, 1, 7, 59, 59, tzinfo=datetime.timezone.utc))
# Does not return event above
events = cal.timeline.start_after(
datetime.datetime(2022, 2, 1, 8, 00, 00, tzinfo=datetime.timezone.utc))
```
"""
orig_tz = LOCAL_TZ.set(local_tz)
try:
yield
finally:
LOCAL_TZ.reset(orig_tz)


def local_timezone() -> datetime.tzinfo:
"""Get the local timezone to use when converting date to datetime."""
local_tz = datetime.datetime.now().astimezone().tzinfo
if not local_tz:
return datetime.timezone.utc
return local_tz
if local_tz := LOCAL_TZ.get(None):
return local_tz
if local_tz := datetime.datetime.now().astimezone().tzinfo:
return local_tz
return datetime.timezone.utc


def normalize_datetime(value: datetime.date | datetime.datetime) -> datetime.datetime:
Expand Down
46 changes: 46 additions & 0 deletions tests/test_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import datetime
import re
import uuid
import zoneinfo
from typing import Generator
from unittest.mock import patch

Expand All @@ -13,6 +14,7 @@
from ical.calendar import Calendar
from ical.calendar_stream import IcsCalendarStream
from ical.event import Event
from ical.util import use_local_timezone


@pytest.fixture(name="calendar")
Expand Down Expand Up @@ -275,3 +277,47 @@ def test_create_and_serialize_calendar(
"END:VEVENT",
"END:VCALENDAR",
]


@pytest.mark.parametrize(
"tzname,dt_before,dt_after",
[
(
"America/Los_Angeles", # UTC-8 in Feb
datetime.datetime(2000, 2, 1, 7, 59, 59, tzinfo=datetime.timezone.utc),
datetime.datetime(2000, 2, 1, 8, 0, 0, tzinfo=datetime.timezone.utc),
),
(
"America/Regina", # UTC-6 all year round
datetime.datetime(2000, 2, 1, 5, 59, 59, tzinfo=datetime.timezone.utc),
datetime.datetime(2000, 2, 1, 6, 0, 0, tzinfo=datetime.timezone.utc),
),
(
"CET", # UTC-1 in Feb
datetime.datetime(2000, 1, 31, 22, 59, 59, tzinfo=datetime.timezone.utc),
datetime.datetime(2000, 1, 31, 23, 0, 0, tzinfo=datetime.timezone.utc),
),
],
)
def test_all_day_with_local_timezone(
tzname: str, dt_before: datetime.datetime, dt_after: datetime.datetime
) -> None:
"""Test iteration of all day events using local timezone override."""
cal = Calendar()
cal.events.extend(
[
Event(
summary="event",
start=datetime.date(2000, 2, 1),
end=datetime.date(2000, 2, 2),
),
]
)

def start_after(dtstart: datetime.datetime) -> list[str]:
nonlocal cal
return [e.summary for e in cal.timeline.start_after(dtstart)]

with use_local_timezone(zoneinfo.ZoneInfo(tzname)):
assert start_after(dt_before) == ["event"]
assert not start_after(dt_after)

0 comments on commit 4df2138

Please sign in to comment.