11"""Tests for issue #867: Todo.duration should work without DTSTART."""
22
3- from datetime import datetime , timedelta
4-
53import pytest
4+ from datetime import datetime , timedelta
65
7- from icalendar import Event , Todo
6+ from icalendar import Todo , Event
87from icalendar .error import IncompleteComponent
98
109
@@ -26,43 +25,43 @@ def test_todo_duration_without_dtstart():
2625def test_todo_duration_calculated_from_start_and_due ():
2726 """Test that Todo.duration still works for calculated duration from `DTSTART` and `DUE`."""
2827 todo = Todo ()
29- todo .add (" UID" , " test-calculated" )
28+ todo .add (' UID' , ' test-calculated' )
3029 todo .start = datetime (2026 , 3 , 19 , 12 , 0 )
3130 todo .end = datetime (2026 , 3 , 19 , 15 , 30 )
32-
31+
3332 # Should calculate duration from start and end
3433 assert todo .duration == timedelta (hours = 3 , minutes = 30 )
3534
3635
3736def test_todo_duration_prefers_duration_property ():
3837 """Test that explicit `DURATION` property takes precedence over calculated duration."""
3938 todo = Todo ()
40- todo .add (" UID" , " test-precedence" )
39+ todo .add (' UID' , ' test-precedence' )
4140 todo .start = datetime (2026 , 3 , 19 , 12 , 0 )
4241 todo .end = datetime (2026 , 3 , 19 , 15 , 0 ) # This would be 3 hours
43- todo .add (" DURATION" , timedelta (days = 2 )) # But DURATION says 2 days
44-
42+ todo .add (' DURATION' , timedelta (days = 2 )) # But DURATION says 2 days
43+
4544 # Should return DURATION property, not calculated value
4645 assert todo .duration == timedelta (days = 2 )
4746
4847
4948def test_todo_duration_with_dtstart_and_duration ():
5049 """Test Todo with `DTSTART` and `DURATION` (valid per RFC 5545)."""
5150 todo = Todo ()
52- todo .add (" UID" , " test-start-duration" )
51+ todo .add (' UID' , ' test-start-duration' )
5352 todo .start = datetime (2026 , 3 , 19 , 12 , 0 )
54- todo .add (" DURATION" , timedelta (hours = 4 ))
55-
53+ todo .add (' DURATION' , timedelta (hours = 4 ))
54+
5655 # Should return DURATION property
5756 assert todo .duration == timedelta (hours = 4 )
5857
5958
6059def test_todo_duration_without_any_time_info_raises_error ():
6160 """Test that Todo.duration raises error when no time information is available."""
6261 todo = Todo ()
63- todo .add (" UID" , " test-no-time" )
64- todo .add (" SUMMARY" , " Task without any time info" )
65-
62+ todo .add (' UID' , ' test-no-time' )
63+ todo .add (' SUMMARY' , ' Task without any time info' )
64+
6665 # Should raise error since no DURATION, DTSTART, or DUE is set
6766 with pytest .raises (IncompleteComponent ):
6867 _ = todo .duration
@@ -76,32 +75,29 @@ def test_todo_duration_complex_duration_values():
7675 ("PT1H30M" , timedelta (hours = 1 , minutes = 30 )),
7776 ("P1DT2H" , timedelta (days = 1 , hours = 2 )),
7877 ("P1W" , timedelta (weeks = 1 )),
79- (
80- "P1Y2M3DT4H5M6S" ,
81- timedelta (days = 428 , hours = 4 , minutes = 5 , seconds = 6 ),
82- ), # Approx 1 year 2 months
78+ ("P1Y2M3DT4H5M6S" , timedelta (days = 428 , hours = 4 , minutes = 5 , seconds = 6 )), # Approx 1 year 2 months
8379 ]
84-
80+
8581 for duration_str , expected_delta in test_cases :
8682 todo = Todo ()
87- todo .add (" UID" , f" test-{ duration_str } " )
88- todo .add (" DURATION" , expected_delta )
89-
83+ todo .add (' UID' , f' test-{ duration_str } ' )
84+ todo .add (' DURATION' , expected_delta )
85+
9086 assert todo .duration == expected_delta
9187
9288
9389def test_todo_duration_maintains_backward_compatibility ():
9490 """Test that the fix doesn't break existing functionality."""
9591 # Create todo the old way (DTSTART + DUE)
9692 todo = Todo ()
97- todo .add (" UID" , " test-backward-compat" )
98- todo .add (" SUMMARY" , " Backward compatibility test" )
93+ todo .add (' UID' , ' test-backward-compat' )
94+ todo .add (' SUMMARY' , ' Backward compatibility test' )
9995 todo .start = datetime (2026 , 3 , 19 , 9 , 0 )
10096 todo .end = datetime (2026 , 3 , 19 , 17 , 0 )
101-
97+
10298 # Should still work as before
10399 assert todo .duration == timedelta (hours = 8 )
104-
100+
105101 # Properties should still work
106102 assert todo .start == datetime (2026 , 3 , 19 , 9 , 0 )
107103 assert todo .end == datetime (2026 , 3 , 19 , 17 , 0 )
@@ -110,9 +106,9 @@ def test_todo_duration_maintains_backward_compatibility():
110106def test_todo_duration_edge_case_only_dtstart ():
111107 """Test Todo with only `DTSTART` (no `DUE` or `DURATION`)."""
112108 todo = Todo ()
113- todo .add (" UID" , " test-only-start" )
109+ todo .add (' UID' , ' test-only-start' )
114110 todo .start = datetime (2026 , 3 , 19 , 12 , 0 )
115-
111+
116112 # This should use the fallback logic from the end property
117113 # which returns start + 1 day for date, or just start for datetime
118114 assert todo .duration == timedelta (0 ) # end defaults to start for datetime
@@ -131,26 +127,23 @@ def test_issue_867_exact_reproduction():
131127
132128 # This assertion was failing before the fix
133129 assert my_task .duration == timedelta (days = 5 )
134-
130+
135131 # Verify the todo has the expected properties
136- assert my_task .get ("UID" ) == "taskwithoutdtstart"
137- assert (
138- my_task .get ("SUMMARY" )
139- == "This is a task that is expected to take five days to complete"
140- )
141- assert "DURATION" in my_task
142- assert "DTSTART" not in my_task # Confirming no DTSTART
132+ assert my_task .get ('UID' ) == 'taskwithoutdtstart'
133+ assert my_task .get ('SUMMARY' ) == 'This is a task that is expected to take five days to complete'
134+ assert 'DURATION' in my_task
135+ assert 'DTSTART' not in my_task # Confirming no DTSTART
143136
144137
145138def test_todo_duration_preserves_property_access ():
146139 """Test that direct property access still works as expected."""
147140 todo = Todo ()
148- todo .add (" UID" , " test-property-access" )
149- todo .add (" DURATION" , timedelta (hours = 2 ))
150-
141+ todo .add (' UID' , ' test-property-access' )
142+ todo .add (' DURATION' , timedelta (hours = 2 ))
143+
151144 # Direct property access should still work
152- assert todo [" DURATION" ].dt == timedelta (hours = 2 )
153-
145+ assert todo [' DURATION' ].dt == timedelta (hours = 2 )
146+
154147 # And our computed property should match
155148 assert todo .duration == timedelta (hours = 2 )
156149
@@ -159,21 +152,21 @@ def test_todo_duration_preserves_property_access():
159152def test_event_duration_prefers_duration_property ():
160153 """Test that Event.duration also prefers `DURATION` property over calculated duration."""
161154 event = Event ()
162- event .add (" UID" , " test-event-duration" )
155+ event .add (' UID' , ' test-event-duration' )
163156 event .start = datetime (2026 , 3 , 19 , 12 , 0 )
164157 event .end = datetime (2026 , 3 , 19 , 15 , 0 ) # This would be 3 hours
165- event .add (" DURATION" , timedelta (hours = 2 )) # But DURATION says 2 hours
166-
158+ event .add (' DURATION' , timedelta (hours = 2 )) # But DURATION says 2 hours
159+
167160 # Should return DURATION property, not calculated value
168161 assert event .duration == timedelta (hours = 2 )
169162
170163
171164def test_event_duration_calculated_fallback ():
172165 """Test that Event.duration falls back to calculated duration when no `DURATION` property."""
173166 event = Event ()
174- event .add (" UID" , " test-event-calculated" )
167+ event .add (' UID' , ' test-event-calculated' )
175168 event .start = datetime (2026 , 3 , 19 , 12 , 0 )
176169 event .end = datetime (2026 , 3 , 19 , 14 , 30 )
177-
170+
178171 # Should calculate duration from start and end (no DURATION property)
179- assert event .duration == timedelta (hours = 2 , minutes = 30 )
172+ assert event .duration == timedelta (hours = 2 , minutes = 30 )
0 commit comments