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

264 statements  

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

2 

3from __future__ import annotations 

4 

5from datetime import date, datetime, timezone 

6from typing import TYPE_CHECKING, 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.error import InvalidCalendar 

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

13from icalendar.parser_tools import DEFAULT_ENCODING 

14from icalendar.prop import TypesFactory, vDDDLists, vText 

15from icalendar.timezone import tzp 

16 

17if TYPE_CHECKING: 

18 from icalendar.compatibility import Self 

19 

20_marker = [] 

21 

22 

23class Component(CaselessDict): 

24 """Base class for calendar components. 

25 

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

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

28 directly, but rather one of the subclasses. 

29 

30 Attributes: 

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

32 required: These properties are required. 

33 singletons: These properties must only appear once. 

34 multiple: These properties may occur more than once. 

35 exclusive: These properties are mutually exclusive. 

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

37 ignore_exceptions: If True, and we cannot parse this 

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

39 exception propagate upwards. 

40 types_factory: Factory for property types 

41 """ 

42 

43 name = None # should be defined in each component 

44 required = () # These properties are required 

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

46 multiple = () # may occur more than once 

47 exclusive = () # These properties are mutually exclusive 

48 inclusive: ( 

49 tuple[str] | tuple[tuple[str, str]] 

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

51 # ('duration', 'repeat') 

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

53 # component, we will silently ignore 

54 # it, rather than let the exception 

55 # propagate upwards 

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

57 

58 types_factory = TypesFactory() 

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

60 

61 @classmethod 

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

63 """Return a component with this name. 

64 

65 Arguments: 

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

67 """ 

68 if cls._components_factory is None: 

69 cls._components_factory = ComponentFactory() 

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

71 

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

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

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

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

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

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

78 # parsing a property, contains error strings 

79 

80 def __bool__(self): 

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

82 return True 

83 

84 def is_empty(self): 

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

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

87 

88 ############################# 

89 # handling of property values 

90 

91 @classmethod 

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

93 """Encode values to icalendar property values. 

94 

95 :param name: Name of the property. 

96 :type name: string 

97 

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

99 any of the icalendar's own property types. 

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

101 

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

103 available, if encode is set to True. 

104 :type parameters: Dictionary 

105 

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

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

108 or False, if not. 

109 :type encode: Boolean 

110 

111 :returns: icalendar property value 

112 """ 

113 if not encode: 

114 return value 

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

116 # Don't encode already encoded values. 

117 obj = value 

118 else: 

119 klass = cls.types_factory.for_property(name) 

120 obj = klass(value) 

121 if parameters: 

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

123 obj.params = Parameters() 

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

125 if item is None: 

126 if key in obj.params: 

127 del obj.params[key] 

128 else: 

129 obj.params[key] = item 

130 return obj 

131 

132 def add( 

133 self, 

134 name: str, 

135 value, 

136 parameters: dict[str, str] | Parameters = None, 

137 encode: bool = True, # noqa: FBT001 

138 ): 

139 """Add a property. 

140 

141 :param name: Name of the property. 

142 :type name: string 

143 

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

145 any of the icalendar's own property types. 

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

147 

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

149 available, if encode is set to True. 

150 :type parameters: Dictionary 

151 

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

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

154 or False, if not. 

155 :type encode: Boolean 

156 

157 :returns: None 

158 """ 

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

160 "dtstamp", 

161 "created", 

162 "last-modified", 

163 ): 

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

165 value = tzp.localize_utc(value) 

166 

167 # encode value 

168 if ( 

169 encode 

170 and isinstance(value, list) 

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

172 ): 

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

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

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

176 else: 

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

178 

179 # set value 

180 if name in self: 

181 # If property already exists, append it. 

182 oldval = self[name] 

183 if isinstance(oldval, list): 

184 if isinstance(value, list): 

185 value = oldval + value 

186 else: 

187 oldval.append(value) 

188 value = oldval 

189 else: 

190 value = [oldval, value] 

191 self[name] = value 

192 

193 def _decode(self, name, value): 

194 """Internal for decoding property values.""" 

195 

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

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

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

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

200 if isinstance(value, vDDDLists): 

201 # TODO: Workaround unfinished decoding 

202 return value 

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

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

205 # Workaround to decode vText properly 

206 if isinstance(decoded, vText): 

207 decoded = decoded.encode(DEFAULT_ENCODING) 

208 return decoded 

209 

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

211 """Returns decoded value of property.""" 

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

213 # -rnix 

214 

215 if name in self: 

216 value = self[name] 

217 if isinstance(value, list): 

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

219 return self._decode(name, value) 

220 if default is _marker: 

221 raise KeyError(name) 

222 return default 

223 

224 ######################################################################## 

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

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

227 

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

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

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

231 if decode: 

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

233 return vals 

234 

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

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

237 to that. 

238 """ 

239 if encode: 

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

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

242 

243 ######################### 

244 # Handling of components 

245 

246 def add_component(self, component: Component): 

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

248 self.subcomponents.append(component) 

249 

250 def _walk(self, name, select): 

251 """Walk to given component.""" 

252 result = [] 

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

254 result.append(self) 

255 for subcomponent in self.subcomponents: 

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

257 return result 

258 

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

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

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

262 

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

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

265 and returns True/False. 

266 :returns: A list of components that match. 

267 :rtype: list[Component] 

268 """ 

269 if name is not None: 

270 name = name.upper() 

271 return self._walk(name, select) 

272 

273 ##################### 

274 # Generation 

275 

276 def property_items( 

277 self, 

278 recursive=True, 

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

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

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

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

283 """ 

284 v_text = self.types_factory["text"] 

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

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

287 

288 for name in property_names: 

289 values = self[name] 

290 if isinstance(values, list): 

291 # normally one property is one line 

292 for value in values: 

293 properties.append((name, value)) 

294 else: 

295 properties.append((name, values)) 

296 if recursive: 

297 # recursion is fun! 

298 for subcomponent in self.subcomponents: 

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

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

301 return properties 

302 

303 @classmethod 

304 def from_ical(cls, st, multiple: bool = False) -> Self | list[Self]: # noqa: FBT001 

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

306 stack = [] # a stack of components 

307 comps = [] 

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

309 if not line: 

310 continue 

311 

312 try: 

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

314 except ValueError as e: 

315 # if unable to parse a line within a component 

316 # that ignores exceptions, mark the component 

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

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

319 if not component or not component.ignore_exceptions: 

320 raise 

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

322 continue 

323 

324 uname = name.upper() 

325 # check for start of component 

326 if uname == "BEGIN": 

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

328 # otherwise get a general Components for robustness. 

329 c_name = vals.upper() 

330 c_class = cls.get_component_class(c_name) 

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

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

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

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

335 component = c_class() 

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

337 component.name = c_name 

338 stack.append(component) 

339 # check for end of event 

340 elif uname == "END": 

341 # we are done adding properties to this component 

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

343 if not stack: 

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

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

346 

347 component = stack.pop() 

348 if not stack: # we are at the end 

349 comps.append(component) 

350 else: 

351 stack[-1].add_component(component) 

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

353 tzp.cache_timezone_component(component) 

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

355 else: 

356 factory = cls.types_factory.for_property(name) 

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

358 if not component: 

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

360 # ignore these components in parsing 

361 if uname == "X-COMMENT": 

362 break 

363 raise ValueError( 

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

365 ) 

366 datetime_names = ( 

367 "DTSTART", 

368 "DTEND", 

369 "RECURRENCE-ID", 

370 "DUE", 

371 "RDATE", 

372 "EXDATE", 

373 ) 

374 try: 

375 if name == "FREEBUSY": 

376 vals = vals.split(",") 

377 if "TZID" in params: 

378 parsed_components = [ 

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

380 for val in vals 

381 ] 

382 else: 

383 parsed_components = [ 

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

385 ] 

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

387 parsed_components = [ 

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

389 ] 

390 # Workaround broken ICS files with empty RDATE 

391 elif name == "RDATE" and vals == "": 

392 parsed_components = [] 

393 else: 

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

395 except ValueError as e: 

396 if not component.ignore_exceptions: 

397 raise 

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

399 else: 

400 for parsed_component in parsed_components: 

401 parsed_component.params = params 

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

403 

404 if multiple: 

405 return comps 

406 if len(comps) > 1: 

407 raise ValueError( 

408 cls._format_error( 

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

410 ) 

411 ) 

412 if len(comps) < 1: 

413 raise ValueError( 

414 cls._format_error( 

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

416 ) 

417 ) 

418 return comps[0] 

419 

420 @staticmethod 

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

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

423 max_error_length = 100 - 3 

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

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

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

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

428 

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

430 """Returns property as content line.""" 

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

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

433 

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

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

436 contentlines = Contentlines() 

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

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

439 contentlines.append(cl) 

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

441 return contentlines 

442 

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

444 """ 

445 :param sorted: Whether parameters and properties should be 

446 lexicographically sorted. 

447 """ 

448 

449 content_lines = self.content_lines(sorted=sorted) 

450 return content_lines.to_ical() 

451 

452 def __repr__(self): 

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

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

455 return ( 

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

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

458 ) 

459 

460 def __eq__(self, other): 

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

462 return False 

463 

464 properties_equal = super().__eq__(other) 

465 if not properties_equal: 

466 return False 

467 

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

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

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

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

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

473 for subcomponent in self.subcomponents: 

474 if subcomponent not in other.subcomponents: 

475 return False 

476 

477 return True 

478 

479 DTSTAMP = stamp = single_utc_property( 

480 "DTSTAMP", 

481 """RFC 5545: 

482 

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

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

485 

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

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

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

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

490 property specifies the date and time that the information 

491 associated with the calendar component was last revised in the 

492 calendar store. 

493 

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

495 

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

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

498 property. 

499 """, 

500 ) 

501 LAST_MODIFIED = single_utc_property( 

502 "LAST-MODIFIED", 

503 """RFC 5545: 

504 

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

506 information associated with the calendar component was last 

507 revised in the calendar store. 

508 

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

510 file in the file system. 

511 

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

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

514 """, 

515 ) 

516 

517 @property 

518 def last_modified(self) -> datetime: 

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

520 

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

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

523 """ 

524 return self.LAST_MODIFIED or self.DTSTAMP 

525 

526 @last_modified.setter 

527 def last_modified(self, value): 

528 self.LAST_MODIFIED = value 

529 

530 @last_modified.deleter 

531 def last_modified(self): 

532 del self.LAST_MODIFIED 

533 

534 @property 

535 def created(self) -> datetime: 

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

537 

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

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

540 """ 

541 return self.CREATED or self.DTSTAMP 

542 

543 @created.setter 

544 def created(self, value): 

545 self.CREATED = value 

546 

547 @created.deleter 

548 def created(self): 

549 del self.CREATED 

550 

551 def is_thunderbird(self) -> bool: 

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

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

554 

555 @staticmethod 

556 def _utc_now(): 

557 """Return now as UTC value.""" 

558 return datetime.now(timezone.utc) 

559 

560 uid = uid_property 

561 comments = comments_property 

562 

563 CREATED = single_utc_property( 

564 "CREATED", 

565 """ 

566 CREATED specifies the date and time that the calendar 

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

568 store. 

569 

570 Conformance: 

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

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

573 specified as a date with UTC time. 

574 

575 """, 

576 ) 

577 

578 _validate_new = True 

579 

580 @staticmethod 

581 def _validate_start_and_end(start, end): 

582 """This validates start and end. 

583 

584 Raises: 

585 InvalidCalendar: If the information is not valid 

586 """ 

587 if start is None or end is None: 

588 return 

589 if start > end: 

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

591 

592 @classmethod 

593 def new( 

594 cls, 

595 created: Optional[date] = None, 

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

597 last_modified: Optional[date] = None, 

598 stamp: Optional[date] = None, 

599 ) -> Component: 

600 """Create a new component. 

601 

602 Arguments: 

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

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

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

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

607 

608 Raises: 

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

610 

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

612 """ 

613 component = cls() 

614 component.DTSTAMP = stamp 

615 component.created = created 

616 component.last_modified = last_modified 

617 component.comments = comments 

618 return component 

619 

620 

621__all__ = ["Component"]