Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/cal/todo.py: 61%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

102 statements  

1""":rfc:`5545` VTODO component.""" 

2 

3from __future__ import annotations 

4 

5import uuid 

6from datetime import date, datetime, timedelta 

7from typing import TYPE_CHECKING, Literal, Sequence 

8 

9from icalendar.attr import ( 

10 CONCEPTS_TYPE_SETTER, 

11 LINKS_TYPE_SETTER, 

12 RELATED_TO_TYPE_SETTER, 

13 X_MOZ_LASTACK_property, 

14 X_MOZ_SNOOZE_TIME_property, 

15 attendees_property, 

16 categories_property, 

17 class_property, 

18 color_property, 

19 conferences_property, 

20 contacts_property, 

21 create_single_property, 

22 description_property, 

23 exdates_property, 

24 get_duration_property, 

25 get_end_property, 

26 get_start_end_duration_with_validation, 

27 get_start_property, 

28 images_property, 

29 location_property, 

30 organizer_property, 

31 priority_property, 

32 property_del_duration, 

33 property_doc_duration_template, 

34 property_get_duration, 

35 property_set_duration, 

36 rdates_property, 

37 rrules_property, 

38 sequence_property, 

39 set_duration_with_locking, 

40 set_end_with_locking, 

41 set_start_with_locking, 

42 status_property, 

43 summary_property, 

44 uid_property, 

45 url_property, 

46) 

47from icalendar.cal.component import Component 

48from icalendar.cal.examples import get_example 

49 

50if TYPE_CHECKING: 

51 from icalendar.alarms import Alarms 

52 from icalendar.enums import CLASS, STATUS 

53 from icalendar.prop import vCalAddress 

54 from icalendar.prop.conference import Conference 

55 

56 

57class Todo(Component): 

58 """ 

59 A "VTODO" calendar component is a grouping of component 

60 properties that represents an action item or assignment. For 

61 example, it can be used to represent an item of work assigned to 

62 an individual, such as "Prepare for the upcoming conference 

63 seminar on Internet Calendaring". 

64 

65 Examples: 

66 Create a new Todo: 

67 

68 >>> from icalendar import Todo 

69 >>> todo = Todo.new() 

70 >>> print(todo.to_ical()) 

71 BEGIN:VTODO 

72 DTSTAMP:20250517T080612Z 

73 UID:d755cef5-2311-46ed-a0e1-6733c9e15c63 

74 END:VTODO 

75 

76 """ 

77 

78 name = "VTODO" 

79 

80 required = ( 

81 "UID", 

82 "DTSTAMP", 

83 ) 

84 singletons = ( 

85 "CLASS", 

86 "COLOR", 

87 "COMPLETED", 

88 "CREATED", 

89 "DESCRIPTION", 

90 "DTSTAMP", 

91 "DTSTART", 

92 "GEO", 

93 "LAST-MODIFIED", 

94 "LOCATION", 

95 "ORGANIZER", 

96 "PERCENT-COMPLETE", 

97 "PRIORITY", 

98 "RECURRENCE-ID", 

99 "SEQUENCE", 

100 "STATUS", 

101 "SUMMARY", 

102 "UID", 

103 "URL", 

104 "DUE", 

105 "DURATION", 

106 ) 

107 exclusive = ( 

108 "DUE", 

109 "DURATION", 

110 ) 

111 multiple = ( 

112 "ATTACH", 

113 "ATTENDEE", 

114 "CATEGORIES", 

115 "COMMENT", 

116 "CONTACT", 

117 "EXDATE", 

118 "RSTATUS", 

119 "RELATED", 

120 "RESOURCES", 

121 "RDATE", 

122 "RRULE", 

123 ) 

124 DTSTART = create_single_property( 

125 "DTSTART", 

126 "dt", 

127 (datetime, date), 

128 date, 

129 'The "DTSTART" property for a "VTODO" specifies the inclusive start of the Todo.', # noqa: E501 

130 ) 

131 DUE = create_single_property( 

132 "DUE", 

133 "dt", 

134 (datetime, date), 

135 date, 

136 'The "DUE" property for a "VTODO" calendar component specifies the non-inclusive end of the Todo.', # noqa: E501 

137 ) 

138 DURATION = property( 

139 property_get_duration, 

140 property_set_duration, 

141 property_del_duration, 

142 property_doc_duration_template.format(component="VTODO"), 

143 ) 

144 

145 def _get_start_end_duration(self): 

146 """Verify the calendar validity and return the right attributes.""" 

147 return get_start_end_duration_with_validation(self, "DTSTART", "DUE", "VTODO") 

148 

149 @property 

150 def start(self) -> date | datetime: 

151 """The start of the VTODO. 

152 

153 Invalid values raise an InvalidCalendar. 

154 If there is no start, we also raise an IncompleteComponent error. 

155 

156 You can get the start, end and duration of a Todo as follows: 

157 

158 >>> from datetime import datetime 

159 >>> from icalendar import Todo 

160 >>> todo = Todo() 

161 >>> todo.start = datetime(2021, 1, 1, 12) 

162 >>> todo.end = datetime(2021, 1, 1, 12, 30) # 30 minutes 

163 >>> todo.duration # 1800 seconds == 30 minutes 

164 datetime.timedelta(seconds=1800) 

165 >>> print(todo.to_ical()) 

166 BEGIN:VTODO 

167 DTSTART:20210101T120000 

168 DUE:20210101T123000 

169 END:VTODO 

170 """ 

171 return get_start_property(self) 

172 

173 @start.setter 

174 def start(self, start: date | datetime | None): 

175 """Set the start.""" 

176 self.DTSTART = start 

177 

178 @property 

179 def end(self) -> date | datetime: 

180 """The end of the todo. 

181 

182 Invalid values raise an InvalidCalendar error. 

183 If there is no end, we also raise an IncompleteComponent error. 

184 """ 

185 return get_end_property(self, "DUE") 

186 

187 @end.setter 

188 def end(self, end: date | datetime | None): 

189 """Set the end.""" 

190 self.DUE = end 

191 

192 @property 

193 def duration(self) -> timedelta: 

194 """The duration of the VTODO. 

195 

196 Returns the DURATION property if set, otherwise calculated from start and end. 

197 You can set the duration to automatically adjust the end time while keeping 

198 start locked. 

199 

200 Setting the duration will: 

201 1. Keep the start time locked (unchanged) 

202 2. Adjust the end time to start + duration 

203 3. Remove any existing DUE property 

204 4. Set the DURATION property 

205 """ 

206 return get_duration_property(self) 

207 

208 @duration.setter 

209 def duration(self, value: timedelta): 

210 if not isinstance(value, timedelta): 

211 raise TypeError(f"Use timedelta, not {type(value).__name__}.") 

212 

213 # Use the set_duration method with default start-locked behavior 

214 self.set_duration(value, locked="start") 

215 

216 def set_duration( 

217 self, duration: timedelta | None, locked: Literal["start", "end"] = "start" 

218 ): 

219 """Set the duration of the event relative to either start or end. 

220 

221 Args: 

222 duration: The duration to set, or None to convert to DURATION property 

223 locked: Which property to keep unchanged ('start' or 'end') 

224 """ 

225 set_duration_with_locking(self, duration, locked, "DUE") 

226 

227 def set_start( 

228 self, start: date | datetime, locked: Literal["duration", "end"] | None = None 

229 ): 

230 """Set the start with explicit locking behavior. 

231 

232 Args: 

233 start: The start time to set 

234 locked: Which property to keep unchanged ('duration', 'end', or None 

235 for auto-detect) 

236 """ 

237 set_start_with_locking(self, start, locked, "DUE") 

238 

239 def set_end( 

240 self, end: date | datetime, locked: Literal["start", "duration"] = "start" 

241 ): 

242 """Set the end of the component, keeping either the start or the duration same. 

243 

244 Args: 

245 end: The end time to set 

246 locked: Which property to keep unchanged ('start' or 'duration') 

247 """ 

248 set_end_with_locking(self, end, locked, "DUE") 

249 

250 X_MOZ_SNOOZE_TIME = X_MOZ_SNOOZE_TIME_property 

251 X_MOZ_LASTACK = X_MOZ_LASTACK_property 

252 

253 @property 

254 def alarms(self) -> Alarms: 

255 """Compute the alarm times for this component. 

256 

257 >>> from datetime import datetime 

258 >>> from icalendar import Todo 

259 >>> todo = Todo() # empty without alarms 

260 >>> todo.start = datetime(2024, 10, 26, 10, 21) 

261 >>> len(todo.alarms.times) 

262 0 

263 

264 Note that this only uses DTSTART and DUE, but ignores 

265 RDATE, EXDATE, and RRULE properties. 

266 """ 

267 from icalendar.alarms import Alarms 

268 

269 return Alarms(self) 

270 

271 color = color_property 

272 sequence = sequence_property 

273 categories = categories_property 

274 rdates = rdates_property 

275 exdates = exdates_property 

276 rrules = rrules_property 

277 uid = uid_property 

278 summary = summary_property 

279 description = description_property 

280 classification = class_property 

281 url = url_property 

282 organizer = organizer_property 

283 location = location_property 

284 priority = priority_property 

285 contacts = contacts_property 

286 status = status_property 

287 attendees = attendees_property 

288 images = images_property 

289 conferences = conferences_property 

290 

291 @classmethod 

292 def new( 

293 cls, 

294 /, 

295 attendees: list[vCalAddress] | None = None, 

296 categories: Sequence[str] = (), 

297 classification: CLASS | None = None, 

298 color: str | None = None, 

299 comments: list[str] | str | None = None, 

300 concepts: CONCEPTS_TYPE_SETTER = None, 

301 contacts: list[str] | str | None = None, 

302 conferences: list[Conference] | None = None, 

303 created: date | None = None, 

304 description: str | None = None, 

305 end: date | datetime | None = None, 

306 last_modified: date | None = None, 

307 links: LINKS_TYPE_SETTER = None, 

308 location: str | None = None, 

309 organizer: vCalAddress | str | None = None, 

310 priority: int | None = None, 

311 refids: list[str] | str | None = None, 

312 related_to: RELATED_TO_TYPE_SETTER = None, 

313 sequence: int | None = None, 

314 stamp: date | None = None, 

315 start: date | datetime | None = None, 

316 status: STATUS | None = None, 

317 summary: str | None = None, 

318 uid: str | uuid.UUID | None = None, 

319 url: str | None = None, 

320 ): 

321 """Create a new TODO with all required properties. 

322 

323 This creates a new Todo in accordance with :rfc:`5545`. 

324 

325 Arguments: 

326 attendees: The :attr:`attendees` of the todo. 

327 categories: The :attr:`categories` of the todo. 

328 classification: The :attr:`classification` of the todo. 

329 color: The :attr:`color` of the todo. 

330 comments: The :attr:`~icalendar.Component.comments` of the todo. 

331 concepts: The :attr:`~icalendar.Component.concepts` of the todo. 

332 contacts: The :attr:`contacts` of the todo. 

333 conferences: The :attr:`conferences` of the todo. 

334 created: The :attr:`~icalendar.Component.created` of the todo. 

335 description: The :attr:`description` of the todo. 

336 end: The :attr:`end` of the todo. 

337 last_modified: The :attr:`~icalendar.Component.last_modified` of the todo. 

338 links: The :attr:`~icalendar.Component.links` of the todo. 

339 location: The :attr:`location` of the todo. 

340 organizer: The :attr:`organizer` of the todo. 

341 refids: :attr:`~icalendar.Component.refids` of the todo. 

342 related_to: :attr:`~icalendar.Component.related_to` of the todo. 

343 sequence: The :attr:`sequence` of the todo. 

344 stamp: The :attr:`~icalendar.Component.DTSTAMP` of the todo. 

345 If None, this is set to the current time. 

346 start: The :attr:`start` of the todo. 

347 status: The :attr:`status` of the todo. 

348 summary: The :attr:`summary` of the todo. 

349 uid: The :attr:`uid` of the todo. 

350 If None, this is set to a new :func:`uuid.uuid4`. 

351 url: The :attr:`url` of the todo. 

352 

353 Returns: 

354 :class:`Todo` 

355 

356 Raises: 

357 InvalidCalendar: If the content is not valid according to :rfc:`5545`. 

358 

359 .. warning:: As time progresses, we will be stricter with the validation. 

360 """ 

361 todo: Todo = super().new( 

362 stamp=stamp if stamp is not None else cls._utc_now(), 

363 created=created, 

364 last_modified=last_modified, 

365 comments=comments, 

366 links=links, 

367 related_to=related_to, 

368 refids=refids, 

369 concepts=concepts, 

370 ) 

371 todo.summary = summary 

372 todo.description = description 

373 todo.uid = uid if uid is not None else uuid.uuid4() 

374 todo.start = start 

375 todo.end = end 

376 todo.color = color 

377 todo.categories = categories 

378 todo.sequence = sequence 

379 todo.classification = classification 

380 todo.url = url 

381 todo.organizer = organizer 

382 todo.location = location 

383 todo.priority = priority 

384 todo.contacts = contacts 

385 todo.status = status 

386 todo.attendees = attendees 

387 todo.conferences = conferences 

388 

389 if cls._validate_new: 

390 cls._validate_start_and_end(start, end) 

391 return todo 

392 

393 @classmethod 

394 def example(cls, name: str = "example") -> "Todo": 

395 """Return the todo example with the given name.""" 

396 return cls.from_ical(get_example("todos", name)) 

397 

398__all__ = ["Todo"]