kopia lustrzana https://github.com/collective/icalendar
Merge pull request #733 from niccokunzmann/issue-662-todo
VTODO - start, end, durationpull/744/head
commit
65e5e07a8e
|
|
@ -6,6 +6,7 @@ Changelog
|
|||
|
||||
Minor changes:
|
||||
|
||||
- Added ``end``, ``start``, ``duration``, ``DTSTART``, ``DUE``, and ``DURATION`` attributes to ``Todo`` components. See `Issue 662`_.
|
||||
- Format test code with Ruff. See `Issue 672 <https://github.com/collective/icalendar/issues/672>`_.
|
||||
- Document the Debian package. See `Issue 701 <https://github.com/collective/icalendar/issues/701>`_.
|
||||
|
||||
|
|
@ -27,13 +28,16 @@ Bug fixes:
|
|||
|
||||
New features:
|
||||
|
||||
- Added ``Event.end``, ``Event.start``, ``Event.dtstart``, and ``Event.dtend`` attributes. See `Issue 662 <https://github.com/collective/icalendar/issues/662>`_.
|
||||
- Added ``end``, ``start``, ``duration``, ``DTSTART``, ``DUE``, and ``DURATION`` attributes to ``Event`` components. See `Issue 662`_.
|
||||
- Added ``end``, ``start``, ``duration``, and ``DTSTART`` attributes to ``Journal`` components. See `Issue 662`_.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Fix a few ``__all__`` variables.
|
||||
- Added missing ``docs`` folder to distribution packages. See `Issue 712 <https://github.com/collective/icalendar/issues/712>`_.
|
||||
|
||||
.. _`Issue 662`: https://github.com/collective/icalendar/issues/662
|
||||
|
||||
6.0.0 (2024-09-28)
|
||||
------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ def create_single_property(prop:str, value_attr:str, value_type:tuple[type], typ
|
|||
raise InvalidCalendar(f"Multiple {prop} defined.")
|
||||
value = getattr(result, value_attr, result)
|
||||
if not isinstance(value, value_type):
|
||||
raise InvalidCalendar(f"{prop} must be either a date or a datetime, not {value}.")
|
||||
raise InvalidCalendar(f"{prop} must be either a {' or '.join(t.__name__ for t in value_type)}, not {value}.")
|
||||
return value
|
||||
|
||||
def p_set(self:Component, value) -> None:
|
||||
|
|
@ -548,6 +548,48 @@ def is_datetime(dt: date) -> bool:
|
|||
"""Whether this is a date and not a datetime."""
|
||||
return isinstance(dt, datetime)
|
||||
|
||||
def _get_duration(self: Component) -> Optional[timedelta]:
|
||||
"""Getter for property DURATION."""
|
||||
default = object()
|
||||
duration = self.get("duration", default)
|
||||
if isinstance(duration, vDDDTypes):
|
||||
return duration.dt
|
||||
if isinstance(duration, vDuration):
|
||||
return duration.td
|
||||
if duration is not default and not isinstance(duration, timedelta):
|
||||
raise InvalidCalendar(
|
||||
f"DURATION must be a timedelta, not {type(duration).__name__}."
|
||||
)
|
||||
return None
|
||||
|
||||
def _set_duration(self: Component, value: Optional[timedelta]):
|
||||
"""Setter for property DURATION."""
|
||||
if value is None:
|
||||
self.pop("duration", None)
|
||||
return
|
||||
if not isinstance(value, timedelta):
|
||||
raise TypeError(f"Use timedelta, not {type(value).__name__}.")
|
||||
self["duration"] = vDuration(value)
|
||||
self.pop("DTEND")
|
||||
self.pop("DUE")
|
||||
|
||||
|
||||
def _del_duration(self: Component):
|
||||
"""Delete property DURATION."""
|
||||
self.pop("DURATION")
|
||||
|
||||
_doc_duration = """The DURATION property.
|
||||
|
||||
The "DTSTART" property for a "{component}" specifies the inclusive start of the event.
|
||||
The "DURATION" property in conjunction with the DTSTART property
|
||||
for a "{component}" calendar component specifies the non-inclusive end
|
||||
of the event.
|
||||
|
||||
If you would like to calculate the duration of a {component}, do not use this.
|
||||
Instead use the duration property (lower case).
|
||||
"""
|
||||
|
||||
|
||||
class Event(Component):
|
||||
|
||||
name = 'VEVENT'
|
||||
|
|
@ -593,41 +635,12 @@ class Event(Component):
|
|||
raise InvalidCalendar("DTSTART and DTEND must be of the same type, either date or datetime.")
|
||||
return start, end, duration
|
||||
|
||||
@property
|
||||
def DURATION(self) -> Optional[timedelta]: # noqa: N802
|
||||
"""The DURATION of the component.
|
||||
|
||||
The "DTSTART" property for a "VEVENT" specifies the inclusive start of the event.
|
||||
The "DURATION" property in conjunction with the DTSTART property
|
||||
for a "VEVENT" calendar component specifies the non-inclusive end
|
||||
of the event.
|
||||
|
||||
If you would like to calculate the duration of an event do not use this.
|
||||
Instead use the difference between DTSTART and DTEND.
|
||||
"""
|
||||
default = object()
|
||||
duration = self.get("duration", default)
|
||||
if isinstance(duration, vDDDTypes):
|
||||
return duration.dt
|
||||
if isinstance(duration, vDuration):
|
||||
return duration.td
|
||||
if duration is not default and not isinstance(duration, timedelta):
|
||||
raise InvalidCalendar(f"DURATION must be a timedelta, not {type(duration).__name__}.")
|
||||
return None
|
||||
|
||||
@DURATION.setter
|
||||
def DURATION(self, value: Optional[timedelta]): # noqa: N802
|
||||
if value is None:
|
||||
self.pop("duration", None)
|
||||
return
|
||||
if not isinstance(value, timedelta):
|
||||
raise TypeError(f"Use timedelta, not {type(value).__name__}.")
|
||||
self["duration"] = vDuration(value)
|
||||
del self.DTEND
|
||||
DURATION = property(_get_duration, _set_duration, _del_duration, _doc_duration.format(component='VEVENT'))
|
||||
|
||||
@property
|
||||
def duration(self) -> timedelta:
|
||||
"""The duration of the component.
|
||||
"""The duration of the VEVENT.
|
||||
|
||||
This duration is calculated from the start and end of the event.
|
||||
You cannot set the duration as it is unclear what happens to start and end.
|
||||
|
|
@ -648,7 +661,7 @@ class Event(Component):
|
|||
>>> event = Event()
|
||||
>>> event.start = datetime(2021, 1, 1, 12)
|
||||
>>> event.end = datetime(2021, 1, 1, 12, 30) # 30 minutes
|
||||
>>> event.end - event.start # 1800 seconds == 30 minutes
|
||||
>>> event.duration # 1800 seconds == 30 minutes
|
||||
datetime.timedelta(seconds=1800)
|
||||
>>> print(event.to_ical())
|
||||
BEGIN:VEVENT
|
||||
|
|
@ -708,6 +721,89 @@ class Todo(Component):
|
|||
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
|
||||
'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
|
||||
)
|
||||
DTSTART = create_single_property("DTSTART", "dt", (datetime, date), date, 'The "DTSTART" property for a "VTODO" specifies the inclusive start of the Todo.')
|
||||
DUE = create_single_property("DUE", "dt", (datetime, date), date, 'The "DUE" property for a "VTODO" calendar component specifies the non-inclusive end of the Todo.')
|
||||
DURATION = property(_get_duration, _set_duration, _del_duration, _doc_duration.format(component='VTODO'))
|
||||
|
||||
def _get_start_end_duration(self):
|
||||
"""Verify the calendar validity and return the right attributes."""
|
||||
start = self.DTSTART
|
||||
end = self.DUE
|
||||
duration = self.DURATION
|
||||
if duration is not None and end is not None:
|
||||
raise InvalidCalendar("Only one of DUE and DURATION may be in a VTODO, not both.")
|
||||
if isinstance(start, date) and not isinstance(start, datetime) and duration is not None and duration.seconds != 0:
|
||||
raise InvalidCalendar("When DTSTART is a date, DURATION must be of days or weeks.")
|
||||
if start is not None and end is not None and is_date(start) != is_date(end):
|
||||
raise InvalidCalendar("DTSTART and DUE must be of the same type, either date or datetime.")
|
||||
return start, end, duration
|
||||
|
||||
|
||||
@property
|
||||
def start(self) -> date | datetime:
|
||||
"""The start of the VTODO.
|
||||
|
||||
Invalid values raise an InvalidCalendar.
|
||||
If there is no start, we also raise an IncompleteComponent error.
|
||||
|
||||
You can get the start, end and duration of a Todo as follows:
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from icalendar import Todo
|
||||
>>> todo = Todo()
|
||||
>>> todo.start = datetime(2021, 1, 1, 12)
|
||||
>>> todo.end = datetime(2021, 1, 1, 12, 30) # 30 minutes
|
||||
>>> todo.duration # 1800 seconds == 30 minutes
|
||||
datetime.timedelta(seconds=1800)
|
||||
>>> print(todo.to_ical())
|
||||
BEGIN:VTODO
|
||||
DTSTART:20210101T120000
|
||||
DUE:20210101T123000
|
||||
END:VTODO
|
||||
"""
|
||||
start = self._get_start_end_duration()[0]
|
||||
if start is None:
|
||||
raise IncompleteComponent("No DTSTART given.")
|
||||
return start
|
||||
|
||||
@start.setter
|
||||
def start(self, start: Optional[date | datetime]):
|
||||
"""Set the start."""
|
||||
self.DTSTART = start
|
||||
|
||||
@property
|
||||
def end(self) -> date | datetime:
|
||||
"""The end of the component.
|
||||
|
||||
Invalid values raise an InvalidCalendar error.
|
||||
If there is no end, we also raise an IncompleteComponent error.
|
||||
"""
|
||||
start, end, duration = self._get_start_end_duration()
|
||||
if end is None and duration is None:
|
||||
if start is None:
|
||||
raise IncompleteComponent("No DUE or DURATION+DTSTART given.")
|
||||
if is_date(start):
|
||||
return start + timedelta(days=1)
|
||||
return start
|
||||
if duration is not None:
|
||||
if start is not None:
|
||||
return start + duration
|
||||
raise IncompleteComponent("No DUE or DURATION+DTSTART given.")
|
||||
return end
|
||||
|
||||
@end.setter
|
||||
def end(self, end: date | datetime | None):
|
||||
"""Set the end."""
|
||||
self.DUE = end
|
||||
|
||||
@property
|
||||
def duration(self) -> timedelta:
|
||||
"""The duration of the VTODO.
|
||||
|
||||
This duration is calculated from the start and end of the Todo.
|
||||
You cannot set the duration as it is unclear what happens to start and end.
|
||||
"""
|
||||
return self.end - self.start
|
||||
|
||||
|
||||
class Journal(Component):
|
||||
|
|
@ -763,7 +859,7 @@ class Journal(Component):
|
|||
|
||||
@property
|
||||
def duration(self) -> timedelta:
|
||||
"""The journal has no duration."""
|
||||
"""The journal has no duration: timedelta(0)."""
|
||||
return timedelta(0)
|
||||
|
||||
class FreeBusy(Component):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""This tests the properties of components and their types."""
|
||||
from __future__ import annotations
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
|
@ -13,63 +14,73 @@ from icalendar import (
|
|||
IncompleteComponent,
|
||||
InvalidCalendar,
|
||||
Journal,
|
||||
Todo,
|
||||
vDDDTypes,
|
||||
)
|
||||
from icalendar.prop import vDuration
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def event():
|
||||
def prop(component: Event|Todo, prop:str) -> str:
|
||||
"""Translate the end property.
|
||||
|
||||
This allows us to run the same tests on Event and Todo.
|
||||
"""
|
||||
if isinstance(component, Todo) and prop.upper() == "DTEND":
|
||||
return "DUE"
|
||||
return prop
|
||||
|
||||
@pytest.fixture(params=[Event, Todo])
|
||||
def start_end_component(request):
|
||||
"""The event to test."""
|
||||
return Event()
|
||||
return request.param()
|
||||
|
||||
@pytest.fixture(params=[
|
||||
datetime(2022, 7, 22, 12, 7),
|
||||
date(2022, 7, 22),
|
||||
datetime(2022, 7, 22, 13, 7, tzinfo=ZoneInfo("Europe/Paris")),
|
||||
])
|
||||
def dtstart(request, set_event_start, event):
|
||||
def dtstart(request, set_component_start, start_end_component):
|
||||
"""Start of the event."""
|
||||
set_event_start(event, request.param)
|
||||
set_component_start(start_end_component, request.param)
|
||||
return request.param
|
||||
|
||||
|
||||
def _set_event_start_init(event, start):
|
||||
def _set_component_start_init(component, start):
|
||||
"""Create the event with the __init__ method."""
|
||||
d = dict(event)
|
||||
d = dict(component)
|
||||
d["dtstart"] = vDDDTypes(start)
|
||||
event.clear()
|
||||
event.update(Event(d))
|
||||
component.clear()
|
||||
component.update(type(component)(d))
|
||||
|
||||
def _set_event_dtstart(event, start):
|
||||
def _set_component_dtstart(component, start):
|
||||
"""Create the event with the dtstart property."""
|
||||
event.DTSTART = start
|
||||
component.DTSTART = start
|
||||
|
||||
def _set_event_start_attr(event, start):
|
||||
def _set_component_start_attr(component, start):
|
||||
"""Create the event with the dtstart property."""
|
||||
event.start = start
|
||||
component.start = start
|
||||
|
||||
def _set_event_start_ics(event, start):
|
||||
def _set_component_start_ics(component, start):
|
||||
"""Create the event with the start property."""
|
||||
event.add("dtstart", start)
|
||||
ics = event.to_ical().decode()
|
||||
component.add("dtstart", start)
|
||||
ics = component.to_ical().decode()
|
||||
print(ics)
|
||||
event.clear()
|
||||
event.update(Event.from_ical(ics))
|
||||
component.clear()
|
||||
component.update(type(component).from_ical(ics))
|
||||
|
||||
@pytest.fixture(params=[_set_event_start_init, _set_event_start_ics, _set_event_dtstart, _set_event_start_attr])
|
||||
def set_event_start(request):
|
||||
@pytest.fixture(params=[_set_component_start_init, _set_component_start_ics, _set_component_dtstart, _set_component_start_attr])
|
||||
def set_component_start(request):
|
||||
"""Create a new event."""
|
||||
return request.param
|
||||
|
||||
def test_event_dtstart(dtstart, event):
|
||||
def test_component_dtstart(dtstart, start_end_component):
|
||||
"""Test the start of events."""
|
||||
assert event.DTSTART == dtstart
|
||||
assert start_end_component.DTSTART == dtstart
|
||||
|
||||
|
||||
def test_event_start(dtstart, event):
|
||||
def test_event_start(dtstart, start_end_component):
|
||||
"""Test the start of events."""
|
||||
assert event.start == dtstart
|
||||
assert start_end_component.start == dtstart
|
||||
|
||||
|
||||
invalid_start_event_1 = Event()
|
||||
|
|
@ -78,8 +89,20 @@ invalid_start_event_1.add("dtstart", datetime(2022, 7, 22, 12, 8))
|
|||
invalid_start_event_2 = Event.from_ical(invalid_start_event_1.to_ical())
|
||||
invalid_start_event_3 = Event()
|
||||
invalid_start_event_3.add("DTSTART", (date(2018, 1, 1), date(2018, 2, 1)))
|
||||
invalid_start_todo_1 = Todo(invalid_start_event_1)
|
||||
invalid_start_todo_2 = Todo(invalid_start_event_2)
|
||||
invalid_start_todo_3 = Todo(invalid_start_event_3)
|
||||
|
||||
@pytest.mark.parametrize("invalid_event", [invalid_start_event_1, invalid_start_event_2, invalid_start_event_3])
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_event", [
|
||||
invalid_start_event_1,
|
||||
invalid_start_event_2,
|
||||
invalid_start_event_3,
|
||||
invalid_start_todo_1,
|
||||
invalid_start_todo_2,
|
||||
invalid_start_todo_3,
|
||||
]
|
||||
)
|
||||
def test_multiple_dtstart(invalid_event):
|
||||
"""Check that we get the right error."""
|
||||
with pytest.raises(InvalidCalendar):
|
||||
|
|
@ -87,7 +110,8 @@ def test_multiple_dtstart(invalid_event):
|
|||
with pytest.raises(InvalidCalendar):
|
||||
invalid_event.DTSTART # noqa: B018
|
||||
|
||||
def test_no_dtstart():
|
||||
|
||||
def test_no_dtstart(start_end_component):
|
||||
"""DTSTART is optional.
|
||||
|
||||
The following is REQUIRED if the component
|
||||
|
|
@ -96,9 +120,9 @@ def test_no_dtstart():
|
|||
is OPTIONAL; in any case, it MUST NOT occur
|
||||
more than once.
|
||||
"""
|
||||
assert Event().DTSTART is None
|
||||
assert start_end_component.DTSTART is None
|
||||
with pytest.raises(IncompleteComponent):
|
||||
Event().start # noqa: B018
|
||||
start_end_component.start # noqa: B018
|
||||
|
||||
|
||||
@pytest.fixture(params=[
|
||||
|
|
@ -106,55 +130,57 @@ def test_no_dtstart():
|
|||
date(2022, 7, 23),
|
||||
datetime(2022, 7, 22, 14, 7, tzinfo=ZoneInfo("Europe/Paris")),
|
||||
])
|
||||
def dtend(request, set_event_end, event):
|
||||
def dtend(request, set_component_end, start_end_component):
|
||||
"""end of the event."""
|
||||
set_event_end(event, request.param)
|
||||
set_component_end(start_end_component, request.param)
|
||||
return request.param
|
||||
|
||||
|
||||
def _set_event_end_init(event, end):
|
||||
def _set_component_end_init(component, end):
|
||||
"""Create the event with the __init__ method."""
|
||||
d = dict(event)
|
||||
d["dtend"] = vDDDTypes(end)
|
||||
event.clear()
|
||||
event.update(Event(d))
|
||||
d = dict(component)
|
||||
d[prop(component, "dtend")] = vDDDTypes(end)
|
||||
component.clear()
|
||||
component.update(type(component)(d))
|
||||
|
||||
def _set_event_dtend(event, end):
|
||||
def _set_component_end_property(component, end):
|
||||
"""Create the event with the dtend property."""
|
||||
event.DTEND = end
|
||||
setattr(component, prop(component, "DTEND"), end)
|
||||
|
||||
def _set_event_end_attr(event, end):
|
||||
def _set_component_end_attr(component, end):
|
||||
"""Create the event with the dtend property."""
|
||||
event.end = end
|
||||
component.end = end
|
||||
|
||||
def _set_event_end_ics(event, end):
|
||||
def _set_component_end_ics(component, end):
|
||||
"""Create the event with the end property."""
|
||||
event.add("dtend", end)
|
||||
ics = event.to_ical().decode()
|
||||
component.add(prop(component, "DTEND"), end)
|
||||
ics = component.to_ical().decode()
|
||||
print(ics)
|
||||
event.clear()
|
||||
event.update(Event.from_ical(ics))
|
||||
component.clear()
|
||||
component.update(type(component).from_ical(ics))
|
||||
|
||||
@pytest.fixture(params=[_set_event_end_init, _set_event_end_ics, _set_event_dtend, _set_event_end_attr])
|
||||
def set_event_end(request):
|
||||
@pytest.fixture(params=[_set_component_end_init, _set_component_end_ics, _set_component_end_property, _set_component_end_attr])
|
||||
def set_component_end(request):
|
||||
"""Create a new event."""
|
||||
return request.param
|
||||
|
||||
def test_event_dtend(dtend, event):
|
||||
def test_component_end_property(dtend, start_end_component):
|
||||
"""Test the end of events."""
|
||||
assert event.DTEND == dtend # noqa: SIM300
|
||||
attr = prop(start_end_component, "DTEND")
|
||||
assert getattr(start_end_component, attr) == dtend # noqa: SIM300
|
||||
|
||||
|
||||
def test_event_end(dtend, event):
|
||||
def test_component_end(dtend, start_end_component):
|
||||
"""Test the end of events."""
|
||||
assert event.end == dtend
|
||||
assert start_end_component.end == dtend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("attr", ["DTSTART", "DTEND"])
|
||||
def test_delete_attr(event, dtstart, dtend, attr):
|
||||
delattr(event, attr)
|
||||
assert getattr(event, attr) is None
|
||||
delattr(event, attr)
|
||||
def test_delete_attr(start_end_component, dtstart, dtend, attr):
|
||||
attr = prop(start_end_component, attr)
|
||||
delattr(start_end_component, attr)
|
||||
assert getattr(start_end_component, attr) is None
|
||||
delattr(start_end_component, attr)
|
||||
|
||||
|
||||
def _set_duration_vdddtypes(event:Event, duration:timedelta):
|
||||
|
|
@ -170,20 +196,20 @@ def _set_duration_vduration(event:Event, duration:timedelta):
|
|||
event["DURATION"] = vDuration(duration)
|
||||
|
||||
@pytest.fixture(params=[_set_duration_vdddtypes, _set_duration_add, _set_duration_vduration])
|
||||
def duration(event, dtstart, request):
|
||||
def duration(start_end_component, dtstart, request):
|
||||
"""... events have a DATE value type for the "DTSTART" property ...
|
||||
If such a "VEVENT" has a "DURATION"
|
||||
property, it MUST be specified as a "dur-day" or "dur-week" value.
|
||||
"""
|
||||
duration = timedelta(hours=1) if isinstance(dtstart, datetime) else timedelta(days=2)
|
||||
request.param(event, duration)
|
||||
request.param(start_end_component, duration)
|
||||
return duration
|
||||
|
||||
def test_start_and_duration(event, dtstart, duration):
|
||||
def test_start_and_duration(start_end_component, dtstart, duration):
|
||||
"""Check calculation of end with duration."""
|
||||
dur = event.end - event.start
|
||||
dur = start_end_component.end - start_end_component.start
|
||||
assert dur == duration
|
||||
assert event.duration == duration
|
||||
assert start_end_component.duration == duration
|
||||
|
||||
# The "VEVENT" is also the calendar component used to specify an
|
||||
# anniversary or daily reminder within a calendar. These events
|
||||
|
|
@ -203,23 +229,42 @@ invalid_event_end_3.add("DURATION", timedelta(days=1))
|
|||
invalid_event_end_4 = Event()
|
||||
invalid_event_end_4.add("DTSTART", date(2024, 1, 1))
|
||||
invalid_event_end_4.add("DURATION", timedelta(hours=1))
|
||||
|
||||
invalid_todo_end_1 = Todo()
|
||||
invalid_todo_end_1.add("DTSTART", datetime(2024, 1, 1, 10, 20))
|
||||
invalid_todo_end_1.add("DUE", date(2024, 1, 1))
|
||||
invalid_todo_end_2 = Todo()
|
||||
invalid_todo_end_2.add("DUE", datetime(2024, 1, 1, 10, 20))
|
||||
invalid_todo_end_2.add("DTSTART", date(2024, 1, 1))
|
||||
invalid_todo_end_3 = Todo()
|
||||
invalid_todo_end_3.add("DUE", datetime(2024, 1, 1, 10, 20))
|
||||
invalid_todo_end_3.add("DTSTART", datetime(2024, 1, 1, 10, 20))
|
||||
invalid_todo_end_3.add("DURATION", timedelta(days=1))
|
||||
invalid_todo_end_4 = Todo()
|
||||
invalid_todo_end_4.add("DTSTART", date(2024, 1, 1))
|
||||
invalid_todo_end_4.add("DURATION", timedelta(hours=1))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("invalid_event", "message"),
|
||||
("invalid_component", "message"),
|
||||
[
|
||||
(invalid_event_end_1, "DTSTART and DTEND must be of the same type, either date or datetime."),
|
||||
(invalid_event_end_2, "DTSTART and DTEND must be of the same type, either date or datetime."),
|
||||
(invalid_event_end_3, "Only one of DTEND and DURATION may be in a VEVENT, not both."),
|
||||
(invalid_event_end_4, "When DTSTART is a date, DURATION must be of days or weeks."),
|
||||
(invalid_todo_end_1, "DTSTART and DUE must be of the same type, either date or datetime."),
|
||||
(invalid_todo_end_2, "DTSTART and DUE must be of the same type, either date or datetime."),
|
||||
(invalid_todo_end_3, "Only one of DUE and DURATION may be in a VTODO, not both."),
|
||||
(invalid_todo_end_4, "When DTSTART is a date, DURATION must be of days or weeks."),
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize("attr", ["start", "end"])
|
||||
def test_invalid_event(invalid_event, message, attr):
|
||||
def test_invalid_event(invalid_component, message, attr):
|
||||
"""Test that the end and start throuw the right error."""
|
||||
with pytest.raises(InvalidCalendar) as e:
|
||||
getattr(invalid_event, attr)
|
||||
getattr(invalid_component, attr)
|
||||
assert e.value.args[0] == message
|
||||
|
||||
def test_duration_zero():
|
||||
def test_event_duration_zero():
|
||||
"""
|
||||
For cases where a "VEVENT" calendar component
|
||||
specifies a "DTSTART" property with a DATE-TIME value type but no
|
||||
|
|
@ -231,7 +276,8 @@ def test_duration_zero():
|
|||
assert event.end == event.start
|
||||
assert event.duration == timedelta(days=0)
|
||||
|
||||
def test_duration_one_day():
|
||||
|
||||
def test_event_duration_one_day():
|
||||
"""
|
||||
For cases where a "VEVENT" calendar component
|
||||
specifies a "DTSTART" property with a DATE value type but no
|
||||
|
|
@ -244,11 +290,46 @@ def test_duration_one_day():
|
|||
assert event.duration == timedelta(days=1)
|
||||
|
||||
|
||||
def test_todo_duration_zero():
|
||||
"""We do not know about the duration of a todo really."""
|
||||
todo = Todo()
|
||||
todo.start = datetime(2024, 10, 11, 10, 20)
|
||||
assert todo.end == todo.start
|
||||
assert todo.duration == timedelta(days=0)
|
||||
|
||||
def test_todo_duration_one_day():
|
||||
""" The end is at the end of the day, excluding midnight.
|
||||
|
||||
RFC 5545:
|
||||
The following is an example of a "VTODO" calendar
|
||||
component that needs to be completed before May 1st, 2007. On
|
||||
midnight May 1st, 2007 this to-do would be considered overdue.
|
||||
"""
|
||||
event = Event()
|
||||
event.start = date(2024, 10, 11)
|
||||
assert event.end == event.start + timedelta(days=1)
|
||||
assert event.duration == timedelta(days=1)
|
||||
|
||||
|
||||
|
||||
incomplete_event_1 = Event()
|
||||
incomplete_event_2 = Event()
|
||||
incomplete_event_2.add("DURATION", timedelta(hours=1))
|
||||
incomplete_todo_1 = Todo()
|
||||
incomplete_todo_2 = Todo()
|
||||
incomplete_todo_2.add("DURATION", timedelta(hours=1))
|
||||
|
||||
@pytest.mark.parametrize("incomplete_event_end", [incomplete_event_1, incomplete_event_2])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"incomplete_event_end",
|
||||
[
|
||||
incomplete_event_1,
|
||||
incomplete_event_2,
|
||||
incomplete_todo_1,
|
||||
incomplete_todo_2,
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize("attr", ["start", "end", "duration"])
|
||||
def test_incomplete_event(incomplete_event_end, attr):
|
||||
"""Test that the end throws the right error."""
|
||||
|
|
@ -274,6 +355,10 @@ def test_incomplete_event(incomplete_event_end, attr):
|
|||
(Journal,"start"),
|
||||
(Journal,"end"),
|
||||
(Journal,"DTSTART"),
|
||||
(Todo,"start"),
|
||||
(Todo,"end"),
|
||||
(Todo,"DTSTART"),
|
||||
(Todo,"DUE"),
|
||||
]
|
||||
)
|
||||
def test_set_invalid_start(invalid_value, attr, Component):
|
||||
|
|
@ -282,9 +367,9 @@ def test_set_invalid_start(invalid_value, attr, Component):
|
|||
- other types that vDDDTypes accepts
|
||||
- object
|
||||
"""
|
||||
event = Component()
|
||||
component = Component()
|
||||
with pytest.raises(TypeError) as e:
|
||||
setattr(event, attr, invalid_value)
|
||||
setattr(component, attr, invalid_value)
|
||||
assert e.value.args[0] == f"Use datetime or date, not {type(invalid_value).__name__}."
|
||||
|
||||
|
||||
|
|
@ -301,35 +386,35 @@ def setitem(d:dict, key, value):
|
|||
datetime(2022, 2, 2),
|
||||
]
|
||||
)
|
||||
def test_check_invalid_duration(invalid_value):
|
||||
def test_check_invalid_duration(start_end_component, invalid_value):
|
||||
"""Check that we get the right error."""
|
||||
event = Event()
|
||||
event["DURATION"] = invalid_value
|
||||
start_end_component["DURATION"] = invalid_value
|
||||
with pytest.raises(InvalidCalendar) as e:
|
||||
event.DURATION # noqa: B018
|
||||
start_end_component.DURATION # noqa: B018
|
||||
assert e.value.args[0] == f"DURATION must be a timedelta, not {type(invalid_value).__name__}."
|
||||
|
||||
|
||||
def test_setting_the_end_deletes_the_duration():
|
||||
def test_setting_the_end_deletes_the_duration(start_end_component):
|
||||
"""Setting the end should not break the event."""
|
||||
event = Event()
|
||||
event.DTSTART = datetime(2024, 10, 11, 10, 20)
|
||||
event.DURATION = timedelta(days=1)
|
||||
event.DTEND = datetime(2024, 10, 11, 10, 21)
|
||||
assert "DURATION" not in event
|
||||
assert event.DURATION is None
|
||||
assert event.DTEND == datetime(2024, 10, 11, 10, 21)
|
||||
DTEND = prop(start_end_component, "DTEND")
|
||||
start_end_component.DTSTART = datetime(2024, 10, 11, 10, 20)
|
||||
start_end_component.DURATION = timedelta(days=1)
|
||||
setattr(start_end_component, DTEND, datetime(2024, 10, 11, 10, 21))
|
||||
assert "DURATION" not in start_end_component
|
||||
assert start_end_component.DURATION is None
|
||||
end = getattr(start_end_component, DTEND)
|
||||
assert end == datetime(2024, 10, 11, 10, 21)
|
||||
|
||||
|
||||
def test_setting_duration_deletes_the_end():
|
||||
def test_setting_duration_deletes_the_end(start_end_component):
|
||||
"""Setting the duration should not break the event."""
|
||||
event = Event()
|
||||
event.DTSTART = datetime(2024, 10, 11, 10, 20)
|
||||
event.DTEND = datetime(2024, 10, 11, 10, 21)
|
||||
event.DURATION = timedelta(days=1)
|
||||
assert "DTEND" not in event
|
||||
assert event.DTEND is None
|
||||
assert event.DURATION == timedelta(days=1)
|
||||
DTEND = prop(start_end_component, "DTEND")
|
||||
start_end_component.DTSTART = datetime(2024, 10, 11, 10, 20)
|
||||
setattr(start_end_component, DTEND, datetime(2024, 10, 11, 10, 21))
|
||||
start_end_component.DURATION = timedelta(days=1)
|
||||
assert DTEND not in start_end_component
|
||||
assert getattr(start_end_component, DTEND) is None
|
||||
assert start_end_component.DURATION == timedelta(days=1)
|
||||
|
||||
valid_values = pytest.mark.parametrize(
|
||||
("attr", "value"),
|
||||
|
|
@ -340,32 +425,39 @@ valid_values = pytest.mark.parametrize(
|
|||
]
|
||||
)
|
||||
@valid_values
|
||||
def test_setting_to_none_deletes_value(attr, value):
|
||||
def test_setting_to_none_deletes_value(start_end_component, attr, value):
|
||||
"""Setting attributes to None deletes them."""
|
||||
event = Event()
|
||||
setattr(event, attr, value)
|
||||
assert attr in event
|
||||
assert getattr(event, attr) == value
|
||||
setattr(event, attr, None)
|
||||
assert attr not in event
|
||||
attr = prop(start_end_component, attr)
|
||||
setattr(start_end_component, attr, value)
|
||||
assert attr in start_end_component
|
||||
assert getattr(start_end_component, attr) == value
|
||||
setattr(start_end_component, attr, None)
|
||||
assert attr not in start_end_component
|
||||
|
||||
|
||||
@valid_values
|
||||
def test_setting_a_value_twice(attr, value):
|
||||
def test_setting_a_value_twice(start_end_component, attr, value):
|
||||
"""Setting attributes twice replaces them."""
|
||||
event = Event()
|
||||
setattr(event, attr, value + timedelta(days=1))
|
||||
setattr(event, attr, value)
|
||||
assert getattr(event, attr) == value
|
||||
attr = prop(start_end_component, attr)
|
||||
setattr(start_end_component, attr, value + timedelta(days=1))
|
||||
setattr(start_end_component, attr, value)
|
||||
assert getattr(start_end_component, attr) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("attr", ["DTSTART", "DTEND", "DURATION"])
|
||||
def test_invalid_none(attr):
|
||||
def test_invalid_none(start_end_component, attr):
|
||||
"""Special case for None."""
|
||||
event = Event()
|
||||
event[attr] = None
|
||||
attr = prop(start_end_component, attr)
|
||||
start_end_component[attr] = None
|
||||
with pytest.raises(InvalidCalendar):
|
||||
getattr(event, attr)
|
||||
getattr(start_end_component, attr)
|
||||
|
||||
|
||||
def test_delete_duration(start_end_component):
|
||||
"""Test the del command."""
|
||||
start_end_component.DURATION = timedelta(days=1)
|
||||
del start_end_component.DURATION
|
||||
assert start_end_component.DURATION is None
|
||||
|
||||
@pytest.mark.parametrize("attr", ["DTSTART", "end", "start"])
|
||||
@pytest.mark.parametrize("start", [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
"""Test the alarm classification.
|
||||
|
||||
Events can have alarms.
|
||||
Alarms can be in this state:
|
||||
|
||||
- active - the user wants the alarm to pop up
|
||||
- acknowledged - the user no longer wants the alarm
|
||||
- snoozed - the user moved that alarm to another time
|
||||
|
||||
The alarms can only work on the properties of the event like
|
||||
DTSTART, DTEND and DURATION.
|
||||
|
||||
"""
|
||||
Ładowanie…
Reference in New Issue