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

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

261 statements  

1"""The base for :rfc:`5545` components.""" 

2 

3from __future__ import annotations 

4 

5from datetime import date, datetime, timezone 

6from typing import ClassVar, Optional 

7 

8from icalendar.attr import comments_property, single_utc_property, uid_property 

9from icalendar.cal.component_factory import ComponentFactory 

10from icalendar.caselessdict import CaselessDict 

11from icalendar.compatibility import Self 

12from icalendar.error import InvalidCalendar 

13from icalendar.parser import Contentline, Contentlines, Parameters, q_join, q_split 

14from icalendar.parser_tools import DEFAULT_ENCODING 

15from icalendar.prop import TypesFactory, vDDDLists, vText 

16from icalendar.timezone import tzp 

17 

18_marker = [] 

19 

20 

21class Component(CaselessDict): 

22 """Base class for calendar components. 

23 

24 Component is the base object for calendar, Event and the other 

25 components defined in :rfc:`5545`. Normally you will not use this class 

26 directly, but rather one of the subclasses. 

27 

28 Attributes: 

29 name: The name of the component. Example: ``VCALENDAR``. 

30 required: These properties are required. 

31 singletons: These properties must only appear once. 

32 multiple: These properties may occur more than once. 

33 exclusive: These properties are mutually exclusive. 

34 inclusive: If the first in a tuple occurs, the second one must also occur. 

35 ignore_exceptions: If True, and we cannot parse this 

36 component, we will silently ignore it, rather than let the 

37 exception propagate upwards. 

38 types_factory: Factory for property types 

39 """ 

40 

41 name = None # should be defined in each component 

42 required = () # These properties are required 

43 singletons = () # These properties must only appear once 

44 multiple = () # may occur more than once 

45 exclusive = () # These properties are mutually exclusive 

46 inclusive: ( 

47 tuple[str] | tuple[tuple[str, str]] 

48 ) = () # if any occurs the other(s) MUST occur 

49 # ('duration', 'repeat') 

50 ignore_exceptions = False # if True, and we cannot parse this 

51 # component, we will silently ignore 

52 # it, rather than let the exception 

53 # propagate upwards 

54 # not_compliant = [''] # List of non-compliant properties. 

55 

56 types_factory = TypesFactory() 

57 _components_factory: ClassVar[Optional[ComponentFactory]] = None 

58 

59 @classmethod 

60 def get_component_class(cls, name: str) -> type[Component]: 

61 """Return a component with this name. 

62 

63 Arguments: 

64 name: Name of the component, i.e. ``VCALENDAR`` 

65 """ 

66 if cls._components_factory is None: 

67 cls._components_factory = ComponentFactory() 

68 return cls._components_factory.get(name, Component) 

69 

70 def __init__(self, *args, **kwargs): 

71 """Set keys to upper for initial dict.""" 

72 super().__init__(*args, **kwargs) 

73 # set parameters here for properties that use non-default values 

74 self.subcomponents: list[Component] = [] # Components can be nested. 

75 self.errors = [] # If we ignored exception(s) while 

76 # parsing a property, contains error strings 

77 

78 def __bool__(self): 

79 """Returns True, CaselessDict would return False if it had no items.""" 

80 return True 

81 

82 def is_empty(self): 

83 """Returns True if Component has no items or subcomponents, else False.""" 

84 return bool(not list(self.values()) + self.subcomponents) 

85 

86 ############################# 

87 # handling of property values 

88 

89 @classmethod 

90 def _encode(cls, name, value, parameters=None, encode=1): 

91 """Encode values to icalendar property values. 

92 

93 :param name: Name of the property. 

94 :type name: string 

95 

96 :param value: Value of the property. Either of a basic Python type of 

97 any of the icalendar's own property types. 

98 :type value: Python native type or icalendar property type. 

99 

100 :param parameters: Property parameter dictionary for the value. Only 

101 available, if encode is set to True. 

102 :type parameters: Dictionary 

103 

104 :param encode: True, if the value should be encoded to one of 

105 icalendar's own property types (Fallback is "vText") 

106 or False, if not. 

107 :type encode: Boolean 

108 

109 :returns: icalendar property value 

110 """ 

111 if not encode: 

112 return value 

113 if isinstance(value, cls.types_factory.all_types): 

114 # Don't encode already encoded values. 

115 obj = value 

116 else: 

117 klass = cls.types_factory.for_property(name) 

118 obj = klass(value) 

119 if parameters: 

120 if not hasattr(obj, "params"): 

121 obj.params = Parameters() 

122 for key, item in parameters.items(): 

123 if item is None: 

124 if key in obj.params: 

125 del obj.params[key] 

126 else: 

127 obj.params[key] = item 

128 return obj 

129 

130 def add(self, name, value, parameters=None, encode=1): 

131 """Add a property. 

132 

133 :param name: Name of the property. 

134 :type name: string 

135 

136 :param value: Value of the property. Either of a basic Python type of 

137 any of the icalendar's own property types. 

138 :type value: Python native type or icalendar property type. 

139 

140 :param parameters: Property parameter dictionary for the value. Only 

141 available, if encode is set to True. 

142 :type parameters: Dictionary 

143 

144 :param encode: True, if the value should be encoded to one of 

145 icalendar's own property types (Fallback is "vText") 

146 or False, if not. 

147 :type encode: Boolean 

148 

149 :returns: None 

150 """ 

151 if isinstance(value, datetime) and name.lower() in ( 

152 "dtstamp", 

153 "created", 

154 "last-modified", 

155 ): 

156 # RFC expects UTC for those... force value conversion. 

157 value = tzp.localize_utc(value) 

158 

159 # encode value 

160 if ( 

161 encode 

162 and isinstance(value, list) 

163 and name.lower() not in ["rdate", "exdate", "categories"] 

164 ): 

165 # Individually convert each value to an ical type except rdate and 

166 # exdate, where lists of dates might be passed to vDDDLists. 

167 value = [self._encode(name, v, parameters, encode) for v in value] 

168 else: 

169 value = self._encode(name, value, parameters, encode) 

170 

171 # set value 

172 if name in self: 

173 # If property already exists, append it. 

174 oldval = self[name] 

175 if isinstance(oldval, list): 

176 if isinstance(value, list): 

177 value = oldval + value 

178 else: 

179 oldval.append(value) 

180 value = oldval 

181 else: 

182 value = [oldval, value] 

183 self[name] = value 

184 

185 def _decode(self, name, value): 

186 """Internal for decoding property values.""" 

187 

188 # TODO: Currently the decoded method calls the icalendar.prop instances 

189 # from_ical. We probably want to decode properties into Python native 

190 # types here. But when parsing from an ical string with from_ical, we 

191 # want to encode the string into a real icalendar.prop property. 

192 if isinstance(value, vDDDLists): 

193 # TODO: Workaround unfinished decoding 

194 return value 

195 decoded = self.types_factory.from_ical(name, value) 

196 # TODO: remove when proper decoded is implemented in every prop.* class 

197 # Workaround to decode vText properly 

198 if isinstance(decoded, vText): 

199 decoded = decoded.encode(DEFAULT_ENCODING) 

200 return decoded 

201 

202 def decoded(self, name, default=_marker): 

203 """Returns decoded value of property.""" 

204 # XXX: fail. what's this function supposed to do in the end? 

205 # -rnix 

206 

207 if name in self: 

208 value = self[name] 

209 if isinstance(value, list): 

210 return [self._decode(name, v) for v in value] 

211 return self._decode(name, value) 

212 if default is _marker: 

213 raise KeyError(name) 

214 return default 

215 

216 ######################################################################## 

217 # Inline values. A few properties have multiple values inlined in in one 

218 # property line. These methods are used for splitting and joining these. 

219 

220 def get_inline(self, name, decode=1): 

221 """Returns a list of values (split on comma).""" 

222 vals = [v.strip('" ') for v in q_split(self[name])] 

223 if decode: 

224 return [self._decode(name, val) for val in vals] 

225 return vals 

226 

227 def set_inline(self, name, values, encode=1): 

228 """Converts a list of values into comma separated string and sets value 

229 to that. 

230 """ 

231 if encode: 

232 values = [self._encode(name, value, encode=1) for value in values] 

233 self[name] = self.types_factory["inline"](q_join(values)) 

234 

235 ######################### 

236 # Handling of components 

237 

238 def add_component(self, component: Component): 

239 """Add a subcomponent to this component.""" 

240 self.subcomponents.append(component) 

241 

242 def _walk(self, name, select): 

243 """Walk to given component.""" 

244 result = [] 

245 if (name is None or self.name == name) and select(self): 

246 result.append(self) 

247 for subcomponent in self.subcomponents: 

248 result += subcomponent._walk(name, select) # noqa: SLF001 

249 return result 

250 

251 def walk(self, name=None, select=lambda _: True) -> list[Component]: 

252 """Recursively traverses component and subcomponents. Returns sequence 

253 of same. If name is passed, only components with name will be returned. 

254 

255 :param name: The name of the component or None such as ``VEVENT``. 

256 :param select: A function that takes the component as first argument 

257 and returns True/False. 

258 :returns: A list of components that match. 

259 :rtype: list[Component] 

260 """ 

261 if name is not None: 

262 name = name.upper() 

263 return self._walk(name, select) 

264 

265 ##################### 

266 # Generation 

267 

268 def property_items( 

269 self, 

270 recursive=True, 

271 sorted: bool = True, # noqa: A002, FBT001 

272 ) -> list[tuple[str, object]]: 

273 """Returns properties in this component and subcomponents as: 

274 [(name, value), ...] 

275 """ 

276 v_text = self.types_factory["text"] 

277 properties = [("BEGIN", v_text(self.name).to_ical())] 

278 property_names = self.sorted_keys() if sorted else self.keys() 

279 

280 for name in property_names: 

281 values = self[name] 

282 if isinstance(values, list): 

283 # normally one property is one line 

284 for value in values: 

285 properties.append((name, value)) 

286 else: 

287 properties.append((name, values)) 

288 if recursive: 

289 # recursion is fun! 

290 for subcomponent in self.subcomponents: 

291 properties += subcomponent.property_items(sorted=sorted) 

292 properties.append(("END", v_text(self.name).to_ical())) 

293 return properties 

294 

295 @classmethod 

296 def from_ical(cls, st, multiple:bool=False) -> Self|list[Self]: 

297 """Populates the component recursively from a string.""" 

298 stack = [] # a stack of components 

299 comps = [] 

300 for line in Contentlines.from_ical(st): # raw parsing 

301 if not line: 

302 continue 

303 

304 try: 

305 name, params, vals = line.parts() 

306 except ValueError as e: 

307 # if unable to parse a line within a component 

308 # that ignores exceptions, mark the component 

309 # as broken and skip the line. otherwise raise. 

310 component = stack[-1] if stack else None 

311 if not component or not component.ignore_exceptions: 

312 raise 

313 component.errors.append((None, str(e))) 

314 continue 

315 

316 uname = name.upper() 

317 # check for start of component 

318 if uname == "BEGIN": 

319 # try and create one of the components defined in the spec, 

320 # otherwise get a general Components for robustness. 

321 c_name = vals.upper() 

322 c_class = cls.get_component_class(c_name) 

323 # If component factory cannot resolve ``c_name``, the generic 

324 # ``Component`` class is used which does not have the name set. 

325 # That's opposed to the usage of ``cls``, which represents a 

326 # more concrete subclass with a name set (e.g. VCALENDAR). 

327 component = c_class() 

328 if not getattr(component, "name", ""): # undefined components 

329 component.name = c_name 

330 stack.append(component) 

331 # check for end of event 

332 elif uname == "END": 

333 # we are done adding properties to this component 

334 # so pop it from the stack and add it to the new top. 

335 if not stack: 

336 # The stack is currently empty, the input must be invalid 

337 raise ValueError("END encountered without an accompanying BEGIN!") 

338 

339 component = stack.pop() 

340 if not stack: # we are at the end 

341 comps.append(component) 

342 else: 

343 stack[-1].add_component(component) 

344 if vals == "VTIMEZONE" and "TZID" in component: 

345 tzp.cache_timezone_component(component) 

346 # we are adding properties to the current top of the stack 

347 else: 

348 factory = cls.types_factory.for_property(name) 

349 component = stack[-1] if stack else None 

350 if not component: 

351 # only accept X-COMMENT at the end of the .ics file 

352 # ignore these components in parsing 

353 if uname == "X-COMMENT": 

354 break 

355 raise ValueError( 

356 f'Property "{name}" does not have a parent component.' 

357 ) 

358 datetime_names = ( 

359 "DTSTART", 

360 "DTEND", 

361 "RECURRENCE-ID", 

362 "DUE", 

363 "RDATE", 

364 "EXDATE", 

365 ) 

366 try: 

367 if name == "FREEBUSY": 

368 vals = vals.split(",") 

369 if "TZID" in params: 

370 parsed_components = [ 

371 factory(factory.from_ical(val, params["TZID"])) 

372 for val in vals 

373 ] 

374 else: 

375 parsed_components = [ 

376 factory(factory.from_ical(val)) for val in vals 

377 ] 

378 elif name in datetime_names and "TZID" in params: 

379 parsed_components = [ 

380 factory(factory.from_ical(vals, params["TZID"])) 

381 ] 

382 else: 

383 parsed_components = [factory(factory.from_ical(vals))] 

384 except ValueError as e: 

385 if not component.ignore_exceptions: 

386 raise 

387 component.errors.append((uname, str(e))) 

388 else: 

389 for parsed_component in parsed_components: 

390 parsed_component.params = params 

391 component.add(name, parsed_component, encode=0) 

392 

393 if multiple: 

394 return comps 

395 if len(comps) > 1: 

396 raise ValueError( 

397 cls._format_error( 

398 "Found multiple components where only one is allowed", st 

399 ) 

400 ) 

401 if len(comps) < 1: 

402 raise ValueError( 

403 cls._format_error( 

404 "Found no components where exactly one is required", st 

405 ) 

406 ) 

407 return comps[0] 

408 

409 @staticmethod 

410 def _format_error(error_description, bad_input, elipsis="[...]"): 

411 # there's three character more in the error, ie. ' ' x2 and a ':' 

412 max_error_length = 100 - 3 

413 if len(error_description) + len(bad_input) + len(elipsis) > max_error_length: 

414 truncate_to = max_error_length - len(error_description) - len(elipsis) 

415 return f"{error_description}: {bad_input[:truncate_to]} {elipsis}" 

416 return f"{error_description}: {bad_input}" 

417 

418 def content_line(self, name, value, sorted: bool = True): # noqa: A002, FBT001 

419 """Returns property as content line.""" 

420 params = getattr(value, "params", Parameters()) 

421 return Contentline.from_parts(name, params, value, sorted=sorted) 

422 

423 def content_lines(self, sorted: bool = True): # noqa: A002, FBT001 

424 """Converts the Component and subcomponents into content lines.""" 

425 contentlines = Contentlines() 

426 for name, value in self.property_items(sorted=sorted): 

427 cl = self.content_line(name, value, sorted=sorted) 

428 contentlines.append(cl) 

429 contentlines.append("") # remember the empty string in the end 

430 return contentlines 

431 

432 def to_ical(self, sorted: bool = True): # noqa: A002, FBT001 

433 """ 

434 :param sorted: Whether parameters and properties should be 

435 lexicographically sorted. 

436 """ 

437 

438 content_lines = self.content_lines(sorted=sorted) 

439 return content_lines.to_ical() 

440 

441 def __repr__(self): 

442 """String representation of class with all of it's subcomponents.""" 

443 subs = ", ".join(str(it) for it in self.subcomponents) 

444 return ( 

445 f"{self.name or type(self).__name__}" 

446 f"({dict(self)}{', ' + subs if subs else ''})" 

447 ) 

448 

449 def __eq__(self, other): 

450 if len(self.subcomponents) != len(other.subcomponents): 

451 return False 

452 

453 properties_equal = super().__eq__(other) 

454 if not properties_equal: 

455 return False 

456 

457 # The subcomponents might not be in the same order, 

458 # neither there's a natural key we can sort the subcomponents by nor 

459 # are the subcomponent types hashable, so we cant put them in a set to 

460 # check for set equivalence. We have to iterate over the subcomponents 

461 # and look for each of them in the list. 

462 for subcomponent in self.subcomponents: 

463 if subcomponent not in other.subcomponents: 

464 return False 

465 

466 return True 

467 

468 DTSTAMP = stamp = single_utc_property( 

469 "DTSTAMP", 

470 """RFC 5545: 

471 

472 Conformance: This property MUST be included in the "VEVENT", 

473 "VTODO", "VJOURNAL", or "VFREEBUSY" calendar components. 

474 

475 Description: In the case of an iCalendar object that specifies a 

476 "METHOD" property, this property specifies the date and time that 

477 the instance of the iCalendar object was created. In the case of 

478 an iCalendar object that doesn't specify a "METHOD" property, this 

479 property specifies the date and time that the information 

480 associated with the calendar component was last revised in the 

481 calendar store. 

482 

483 The value MUST be specified in the UTC time format. 

484 

485 In the case of an iCalendar object that doesn't specify a "METHOD" 

486 property, this property is equivalent to the "LAST-MODIFIED" 

487 property. 

488 """, 

489 ) 

490 LAST_MODIFIED = single_utc_property( 

491 "LAST-MODIFIED", 

492 """RFC 5545: 

493 

494 Purpose: This property specifies the date and time that the 

495 information associated with the calendar component was last 

496 revised in the calendar store. 

497 

498 Note: This is analogous to the modification date and time for a 

499 file in the file system. 

500 

501 Conformance: This property can be specified in the "VEVENT", 

502 "VTODO", "VJOURNAL", or "VTIMEZONE" calendar components. 

503 """, 

504 ) 

505 

506 @property 

507 def last_modified(self) -> datetime: 

508 """Datetime when the information associated with the component was last revised. 

509 

510 Since :attr:`LAST_MODIFIED` is an optional property, 

511 this returns :attr:`DTSTAMP` if :attr:`LAST_MODIFIED` is not set. 

512 """ 

513 return self.LAST_MODIFIED or self.DTSTAMP 

514 

515 @last_modified.setter 

516 def last_modified(self, value): 

517 self.LAST_MODIFIED = value 

518 

519 @last_modified.deleter 

520 def last_modified(self): 

521 del self.LAST_MODIFIED 

522 

523 @property 

524 def created(self) -> datetime: 

525 """Datetime when the information associated with the component was created. 

526 

527 Since :attr:`CREATED` is an optional property, 

528 this returns :attr:`DTSTAMP` if :attr:`CREATED` is not set. 

529 """ 

530 return self.CREATED or self.DTSTAMP 

531 

532 @created.setter 

533 def created(self, value): 

534 self.CREATED = value 

535 

536 @created.deleter 

537 def created(self): 

538 del self.CREATED 

539 

540 def is_thunderbird(self) -> bool: 

541 """Whether this component has attributes that indicate that Mozilla Thunderbird created it.""" # noqa: E501 

542 return any(attr.startswith("X-MOZ-") for attr in self.keys()) 

543 

544 @staticmethod 

545 def _utc_now(): 

546 """Return now as UTC value.""" 

547 return datetime.now(timezone.utc) 

548 

549 uid = uid_property 

550 comments = comments_property 

551 

552 CREATED = single_utc_property( 

553 "CREATED", 

554 """CREATED specifies the date and time that the calendar 

555information was created by the calendar user agent in the calendar 

556store. 

557 

558Conformance: 

559 The property can be specified once in "VEVENT", 

560 "VTODO", or "VJOURNAL" calendar components. The value MUST be 

561 specified as a date with UTC time. 

562 

563""", 

564 ) 

565 

566 _validate_new = True 

567 

568 @staticmethod 

569 def _validate_start_and_end(start, end): 

570 """This validates start and end. 

571 

572 Raises: 

573 InvalidCalendar: If the information is not valid 

574 """ 

575 if start is None or end is None: 

576 return 

577 if start > end: 

578 raise InvalidCalendar("end must be after start") 

579 

580 @classmethod 

581 def new( 

582 cls, 

583 created: Optional[date] = None, 

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

585 last_modified: Optional[date] = None, 

586 stamp: Optional[date] = None, 

587 ) -> Component: 

588 """Create a new component. 

589 

590 Arguments: 

591 comments: The :attr:`comments` of the component. 

592 created: The :attr:`created` of the component. 

593 last_modified: The :attr:`last_modified` of the component. 

594 stamp: The :attr:`DTSTAMP` of the component. 

595 

596 Raises: 

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

598 

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

600 """ 

601 component = cls() 

602 component.DTSTAMP = stamp 

603 component.created = created 

604 component.last_modified = last_modified 

605 component.comments = comments 

606 return component 

607 

608 

609__all__ = ["Component"]