Skip to content

Commit

Permalink
Update test coverage for start validation and improve types
Browse files Browse the repository at this point in the history
  • Loading branch information
allenporter committed Feb 3, 2024
1 parent 5968368 commit b83e309
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 11 deletions.
14 changes: 5 additions & 9 deletions ical/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
RecurIterable,
SortableItem,
SortableItemValue,
SortedItemIterable,
)
from .types.recur import RecurrenceId


_LOGGER = logging.getLogger(__name__)
_SortableTodoItem = SortableItem[datetime.datetime | datetime.date | None, Todo]


class RecurAdapter:
Expand All @@ -41,9 +41,7 @@ def __init__(self, todo: Todo, tzinfo: datetime.tzinfo | None = None):
self._duration = todo.computed_duration
self._tzinfo = tzinfo

def get(
self, dtstart: datetime.datetime | datetime.date
) -> SortableItem[datetime.datetime | datetime.date | None, Todo]:
def get(self, dtstart: datetime.datetime | datetime.date) -> _SortableTodoItem:
"""Return a lazy sortable item."""

recur_id_dt = dtstart
Expand Down Expand Up @@ -87,20 +85,18 @@ def _pick_todo(todos: list[Todo], tzinfo: datetime.tzinfo) -> Todo:
next todo that is incomplete and has the latest due date.
"""
# For a recurring todo, the dtstart is after the last due date. Therefore
# we can stort items by dtstart and pick the last one that hasn't happened
iters = []
# we can stort items by dtstart and pick the last one that hasn't happened
iters: list[Iterable[_SortableTodoItem]] = []
for todo in todos:
if not (recur := todo.as_rrule()):
iters.append([SortableItemValue(todo.dtstart, todo)])
continue
iters.append(RecurIterable(RecurAdapter(todo, tzinfo=tzinfo).get, recur))

root_iter = MergedIterable(iters)

# Pick the first todo that hasn't started yet based on its dtstart
now = datetime.datetime.now(tzinfo)
last: Todo | None = None

it = iter(root_iter)
last = next(it, None)
if not last:
Expand Down
9 changes: 7 additions & 2 deletions ical/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from .alarm import Alarm
from .component import ComponentModel, validate_until_dtstart, validate_recurrence_dates
from .exceptions import CalendarParseError
from .iter import RulesetIterable
from .parsing.property import ParsedProperty
from .types import (
Expand Down Expand Up @@ -229,8 +230,12 @@ def as_rrule(self) -> Iterable[datetime.datetime | datetime.date] | None:
This is only valid for events where `recurring` is True.
"""
if not (self.rrule or self.rdate) or not self.computed_duration:
if not self.rrule and not self.rdate:
return None
if not self.start:
raise CalendarParseError("Event must have a start date to be recurring")
if not self.due:
raise CalendarParseError("Event must have a due date to be recurring")
return RulesetIterable(
self.start,
[self.rrule.as_rrule(self.start)] if self.rrule else [],
Expand All @@ -247,7 +252,7 @@ def validate_one_due_or_duration(cls, values: dict[str, Any]) -> dict[str, Any]:

@root_validator
def validate_duration_requires_start(cls, values: dict[str, Any]) -> dict[str, Any]:
"""Validate that only one of duration or end date may be set."""
"""Validate that a duration requires the dtstart."""
if values.get("duration") and not values.get("dtstart"):
raise ValueError("Duration requires that dtstart is specified")
return values
Expand Down
34 changes: 34 additions & 0 deletions tests/test_todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

import datetime
import zoneinfo
from typing import Any
from unittest.mock import patch

import pytest

from ical.exceptions import CalendarParseError
from ical.todo import Todo
from ical.types.recur import Recur


def test_empty() -> None:
Expand Down Expand Up @@ -53,3 +55,35 @@ def test_duration() -> None:
"ical.util.local_timezone", return_value=zoneinfo.ZoneInfo("America/Regina")
):
assert todo.start_datetime.isoformat() == "2022-08-07T06:00:00+00:00"


@pytest.mark.parametrize(
("params"),
[
({}),
(
{
"start": datetime.datetime(2022, 9, 6, 6, 0, 0),
}
),
(
{
"due": datetime.datetime(2022, 9, 6, 6, 0, 0),
}
),
(
{
"duration": datetime.timedelta(hours=1),
}
),
],
)
def test_validate_rrule_required_fields(params: dict[str, Any]) -> None:
"""Test that a Todo with an rrule requires a dtstart."""
with pytest.raises(CalendarParseError):
event = Todo(
summary="Event 1",
rrule=Recur.from_rrule("FREQ=WEEKLY;BYDAY=WE,MO,TU,TH,FR;COUNT=3"),
**params,
)
event.as_rrule()

0 comments on commit b83e309

Please sign in to comment.