1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
|
import sys
import time
from datetime import timezone
from pathlib import Path
import dateutil
import icalendar
import pytest
import pytz
from recurring_ical_events import of
try:
import zoneinfo as _zoneinfo
except ImportError:
import backports.zoneinfo as _zoneinfo
HERE = Path(__file__).parent
REPO = Path(HERE).parent
sys.path.append(REPO)
CALENDARS_FOLDER = HERE / "calendars"
# set the default time zone
# see https://stackoverflow.com/questions/1301493/setting-timezone-in-python
time.tzset()
class ICSCalendars:
"""A collection of parsed ICS calendars
components is the argument to pass to the of function"""
Calendar = icalendar.Calendar
components = None
def __init__(self, tzp):
"""Create ICS calendars in a specific timezone."""
self.tzp = tzp
def get_calendar(self, content):
"""Return the calendar given the content."""
self.tzp()
return self.Calendar.from_ical(content)
def __getitem__(self, name):
return getattr(self, name)
@property
def raw(self):
return ICSCalendars(self.tzp)
def consistent_tz(self, dt):
"""Make the datetime consistent with the time zones used in these calendars."""
assert (
dt.tzinfo is None or "pytz" in dt.tzinfo.__class__.__module__
), "We need pytz time zones for now."
return dt
def _of(self, calendar):
"""Return the calendar but also with selected components."""
if self.components is None:
return of(calendar)
return of(calendar, components=self.components)
def __repr__(self):
return f"{self.__class__.__name__}({self.tzp.__name__})"
_calendar_names = []
for calendar_path in CALENDARS_FOLDER.iterdir():
content = calendar_path.read_bytes()
@property
def get_calendar(self, content=content): # noqa: PLR0206
return self.get_calendar(content)
attribute_name = calendar_path.stem
setattr(ICSCalendars, attribute_name, get_calendar)
_calendar_names.append(attribute_name)
class Calendars(ICSCalendars):
"""Collection of calendars from recurring_ical_events"""
def get_calendar(self, content):
return self._of(ICSCalendars.get_calendar(self, content))
class ReversedCalendars(ICSCalendars):
"""All test should run in reversed item order.
RFC5545:
This memo imposes no ordering of properties within an iCalendar object.
"""
def get_calendar(self, content):
"""Calendar traversing events in reversed order."""
calendar = ICSCalendars.get_calendar(self, content)
_walk = calendar.walk
def walk(*args, **kw):
"""Return properties in reversed order."""
return reversed(_walk(*args, **kw))
calendar.walk = walk
return self._of(calendar)
if hasattr(icalendar, "use_pytz") and hasattr(icalendar, "use_zoneinfo"):
tzps = [icalendar.use_pytz, icalendar.use_zoneinfo]
else:
tzps = [lambda: ...]
@pytest.fixture(params=tzps, scope="module")
def tzp(request):
"""The timezone provider supported by icalendar."""
return request.param
# for parametrizing fixtures, see https://docs.pytest.org/en/latest/fixture.html#parametrizing-fixtures
@pytest.fixture(params=[Calendars, ReversedCalendars], scope="module")
def calendars(request, tzp):
"""The calendars we can use in the tests."""
return request.param(tzp)
@pytest.fixture
def todo():
"""Skip a test because it needs to be written first."""
pytest.skip("This test is not yet implemented.")
@pytest.fixture(scope="module")
def zoneinfo():
"""Return the zoneinfo module if present, otherwise skip the test.
Uses backports.zoneinfo or zoneinfo.
"""
return _zoneinfo
@pytest.fixture(scope="module")
def ZoneInfo(zoneinfo):
"""Shortcut for zoneinfo.ZoneInfo."""
return zoneinfo.ZoneInfo
@pytest.fixture(
scope="module",
params=[pytz.utc, _zoneinfo.ZoneInfo("UTC"), timezone.utc, dateutil.tz.UTC],
)
def utc(request):
"""Return all the UTC implementations."""
return request.param
class DoctestZoneInfo(_zoneinfo.ZoneInfo):
"""Constent ZoneInfo representation for tests."""
def __repr__(self):
return f"ZoneInfo(key={self.key!r})"
def doctest_print(obj):
"""doctest print"""
if isinstance(obj, bytes):
obj = obj.decode("UTF-8")
print(str(obj).strip().replace("\r\n", "\n").replace("\r", "\n"))
@pytest.fixture
def env_for_doctest(monkeypatch):
"""Modify the environment to make doctests run."""
monkeypatch.setitem(sys.modules, "zoneinfo", _zoneinfo)
monkeypatch.setattr(_zoneinfo, "ZoneInfo", DoctestZoneInfo)
from icalendar.timezone.zoneinfo import ZONEINFO
monkeypatch.setattr(ZONEINFO, "utc", _zoneinfo.ZoneInfo("UTC"))
return {
"print": doctest_print,
"CALENDARS": CALENDARS_FOLDER,
}
# remove invalid names
_calendar_names.remove("end_before_start_event")
_calendar_names.sort()
@pytest.fixture(scope="module", params=_calendar_names)
def calendar_name(request) -> str:
"""All the calendar names."""
return request.param
|