Merge pull request #733 from niccokunzmann/issue-662-todo

VTODO - start, end, duration
pull/744/head
Nicco Kunzmann 2024-10-26 10:19:13 +01:00 zatwierdzone przez GitHub
commit 65e5e07a8e
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
4 zmienionych plików z 343 dodań i 138 usunięć

Wyświetl plik

@ -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)
------------------

Wyświetl plik

@ -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):

Wyświetl plik

@ -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", [

Wyświetl plik

@ -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.
"""