Skip to content

Commit e60de76

Browse files
committed
Add docstrings.
1 parent d24cf91 commit e60de76

File tree

1 file changed

+90
-0
lines changed

1 file changed

+90
-0
lines changed

oncalendar/__init__.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
UTC = timezone.utc
1111

12+
# systemd seems to stop iteration when it reaches year 2200. We do the same.
1213
MAX_YEAR = 2200
1314
RANGES = [
1415
range(0, 7),
@@ -42,6 +43,8 @@
4243
"quarterly": "*-01,04,07,10-01 00:00:00",
4344
"semiannually": "*-01,07-01 00:00:00",
4445
}
46+
# timedelta initialization is not cheap so we prepare a few constants
47+
# that we will need often:
4548
SECOND = td(seconds=1)
4649
MINUTE = td(minutes=1)
4750
HOUR = td(hours=1)
@@ -66,13 +69,26 @@ def msg(self) -> str:
6669
def _int(self, value: str) -> int:
6770
if value == "":
6871
raise OnCalendarError(self.msg())
72+
# Make sure the value contains digits and nothing else
73+
# (for example, we reject integer literals with underscores)
6974
for ch in value:
7075
if ch not in "0123456789":
7176
raise OnCalendarError(self.msg())
7277

7378
return int(value)
7479

7580
def int(self, s: str) -> int:
81+
"""Convert the supplied sting to an integer.
82+
83+
This function handles a few special cases:
84+
* It converts weekdays "Mon", "Tue", ..., "Sun" to 0, 1, ..., 6
85+
* It converts years 70 .. 99 to 1970 - 1999
86+
* It converts years 0 .. 69 to 2000 - 2069
87+
88+
It also checks if the resulting integer is within the range
89+
of valid values for the given field, and raises `OnCalendarError`
90+
if it is not.
91+
"""
7692
if self == Field.DOW:
7793
s = s.upper()
7894
if s in SYMBOLIC_DAYS:
@@ -97,6 +113,11 @@ def int(self, s: str) -> int:
97113
return v
98114

99115
def parse(self, s: str, reverse: bool = False) -> set[__builtins__.int]:
116+
"""Parse a single component of an expression into a set of integers.
117+
118+
To handle lists, intervals, and intervals with a step, this function
119+
recursively calls itself.
120+
"""
100121
if self == Field.DAY and s.startswith("~"):
101122
# Chop leading "~" and set the reverse flag
102123
return self.parse(s[1:], reverse=True)
@@ -168,11 +189,42 @@ def parse(self, s: str, reverse: bool = False) -> set[__builtins__.int]:
168189

169190

170191
def is_imaginary(dt: datetime) -> bool:
192+
"""Return True if dt gets skipped over during DST transition."""
171193
return dt != dt.astimezone(UTC).astimezone(dt.tzinfo)
172194

173195

174196
class BaseIterator(object):
197+
"""OnCalendar expression parser and iterator.
198+
199+
This iterator supports most syntax features supported by systemd. It however
200+
does *not* support:
201+
202+
* Timezone specified within the expression (use `TzIterator` instead).
203+
* Seconds fields with decimal values.
204+
205+
This iterator works with both naive and timezone-aware datetimes. In case
206+
of timezone-aware datetimes, it mimics systemd behaviour during DST transitions:
207+
208+
* It skips over datetimes that fall in the skipped hour during the spring DST
209+
transition.
210+
* It repeats the datetimes that fall in the repeated hour during the fall DST
211+
transition. It returns a datetime with the pre-transition timezone,
212+
then the same datetime but with the post-transition timezone.
213+
"""
214+
175215
def __init__(self, expression: str, start: datetime):
216+
"""Initialize the iterator with an OnCalendar expression and the start time.
217+
218+
`expression` should contain a single OnCalendar expression without a timezone,
219+
for example: `Mon 01-01 12:00:00`.
220+
221+
`start` is the datetime to start iteration from. The first result
222+
returned by the iterator will be greater than `start`. The supplied
223+
datetime can be either naive or timezone-aware. If `start` is naive,
224+
the iterator will also return naive datetimes. If `start` is timezone-aware,
225+
the iterator will return timezone-aware datetimes using the same timezone
226+
as `start`.
227+
"""
176228
self.dt = start.replace(microsecond=0)
177229

178230
if expression.lower() in SPECIALS:
@@ -359,6 +411,13 @@ def advance_month(self) -> bool:
359411
return True
360412

361413
def advance_year(self) -> None:
414+
"""Roll forward the year component until it satisfies the constraints.
415+
416+
Return False if the year meets contraints without modification.
417+
Return True if self.dt was rolled forward.
418+
419+
"""
420+
362421
if self.dt.year in self.years:
363422
return
364423

@@ -407,6 +466,7 @@ def __next__(self) -> datetime:
407466

408467

409468
def parse_tz(value: str) -> ZoneInfo | None:
469+
"""Return ZoneInfo object or None if value fails to parse."""
410470
# Optimization: there are no timezones that start with a digit or star
411471
if value[0] in "0123456789*":
412472
return None
@@ -418,7 +478,22 @@ def parse_tz(value: str) -> ZoneInfo | None:
418478

419479

420480
class TzIterator(object):
481+
"""OnCalendar expression parser and iterator (with timezone support).
482+
483+
This iterator wraps `BaseIterator` and adds support for timezones within
484+
the expression. This iterator requires the starting datetime to be
485+
timezone-aware.
486+
"""
487+
421488
def __init__(self, expression: str, start: datetime):
489+
"""Initialize the iterator with an OnCalendar expression and the start time.
490+
491+
`expression` should contain a single OnCalendar expression with or without a
492+
timezone, for example: `Mon 01-01 12:00:00 Europe/Riga`.
493+
494+
`start` is the timezone-aware datetime to start iteration from. The iterator
495+
will return datetimes using the same timezone as `start`.
496+
"""
422497
if not start.tzinfo:
423498
raise OnCalendarError("Argument 'dt' must be timezone-aware")
424499

@@ -436,7 +511,22 @@ def __next__(self) -> datetime:
436511

437512

438513
class OnCalendar(object):
514+
"""OnCalendar expression parser and iterator (with multiple expression support).
515+
516+
This iterator wraps `TzIterator` and adds support for iterating over multiple
517+
expressions (separated by newlines) at once.
518+
"""
519+
439520
def __init__(self, expressions: str, start: datetime):
521+
"""Initialize the iterator with OnCalendar expression(s) and the start time.
522+
523+
`expressions` should contain one or more OnCalendar expressions with or without
524+
a timezone, separated with newlines. Example:
525+
`00:00 Europe/Riga\n00:00 UTC`.
526+
527+
`start` is the timezone-aware datetime to start iteration from. The iterator
528+
will return datetimes using the same timezone as `start`.
529+
"""
440530
if not start.tzinfo:
441531
raise OnCalendarError("Argument 'dt' must be timezone-aware")
442532

0 commit comments

Comments
 (0)