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

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

300 statements  

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

2 

3from __future__ import annotations 

4 

5from datetime import date, datetime, time, timedelta, timezone 

6from typing import TYPE_CHECKING, ClassVar 

7 

8from icalendar.attr import ( 

9 CONCEPTS_TYPE_SETTER, 

10 LINKS_TYPE_SETTER, 

11 RELATED_TO_TYPE_SETTER, 

12 comments_property, 

13 concepts_property, 

14 links_property, 

15 refids_property, 

16 related_to_property, 

17 single_utc_property, 

18 uid_property, 

19) 

20from icalendar.cal.component_factory import ComponentFactory 

21from icalendar.caselessdict import CaselessDict 

22from icalendar.error import InvalidCalendar 

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

24from icalendar.parser_tools import DEFAULT_ENCODING 

25from icalendar.prop import TypesFactory, vDDDLists, vText 

26from icalendar.timezone import tzp 

27from icalendar.tools import is_date 

28 

29if TYPE_CHECKING: 

30 from icalendar.compatibility import Self 

31 

32_marker = [] 

33 

34 

35class Component(CaselessDict): 

36 """Base class for calendar components. 

37 

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

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

40 directly, but rather one of the subclasses. 

41 

42 Attributes: 

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

44 required: These properties are required. 

45 singletons: These properties must only appear once. 

46 multiple: These properties may occur more than once. 

47 exclusive: These properties are mutually exclusive. 

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

49 ignore_exceptions: If True, and we cannot parse this 

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

51 exception propagate upwards. 

52 types_factory: Factory for property types 

53 """ 

54 

55 name = None # should be defined in each component 

56 required = () # These properties are required 

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

58 multiple = () # may occur more than once 

59 exclusive = () # These properties are mutually exclusive 

60 inclusive: ( 

61 tuple[str] | tuple[tuple[str, str]] 

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

63 # ('duration', 'repeat') 

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

65 # component, we will silently ignore 

66 # it, rather than let the exception 

67 # propagate upwards 

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

69 

70 types_factory = TypesFactory() 

71 _components_factory: ClassVar[ComponentFactory | None] = None 

72 

73 @classmethod 

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

75 """Return a component with this name. 

76 

77 Arguments: 

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

79 """ 

80 if cls._components_factory is None: 

81 cls._components_factory = ComponentFactory() 

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

83 

84 @staticmethod 

85 def _infer_value_type( 

86 value: date | datetime | timedelta | time | tuple | list, 

87 ) -> str | None: 

88 """Infer the ``VALUE`` parameter from a Python type. 

89 

90 Args: 

91 value: Python native type, one of :py:class:`date`, :py:mod:`datetime`, 

92 :py:class:`timedelta`, :py:mod:`time`, :py:class:`tuple`, 

93 or :py:class:`list`. 

94 

95 Returns: 

96 str or None: The ``VALUE`` parameter string, for example, "DATE", 

97 "TIME", or other string, or ``None`` 

98 if no specific ``VALUE`` is needed. 

99 """ 

100 if isinstance(value, list): 

101 if not value: 

102 return None 

103 # Check if ALL items are date (but not datetime) 

104 if all(is_date(item) for item in value): 

105 return "DATE" 

106 # Check if ALL items are time 

107 if all(isinstance(item, time) for item in value): 

108 return "TIME" 

109 # Mixed types or other types - don't infer 

110 return None 

111 if is_date(value): 

112 return "DATE" 

113 if isinstance(value, time): 

114 return "TIME" 

115 # Don't infer PERIOD - it's too risky and vPeriod already handles it 

116 return None 

117 

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

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

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

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

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

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

124 # parsing a property, contains error strings 

125 

126 def __bool__(self): 

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

128 return True 

129 

130 def is_empty(self): 

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

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

133 

134 ############################# 

135 # handling of property values 

136 

137 @classmethod 

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

139 """Encode values to icalendar property values. 

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: icalendar property value 

158 """ 

159 if not encode: 

160 return value 

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

162 # Don't encode already encoded values. 

163 obj = value 

164 else: 

165 # Extract VALUE parameter if present, or infer it from the Python type 

166 value_param = None 

167 if parameters and "VALUE" in parameters: 

168 value_param = parameters["VALUE"] 

169 elif not isinstance(value, cls.types_factory.all_types): 

170 inferred = cls._infer_value_type(value) 

171 if inferred: 

172 value_param = inferred 

173 # Auto-set the VALUE parameter 

174 if parameters is None: 

175 parameters = {} 

176 if "VALUE" not in parameters: 

177 parameters["VALUE"] = inferred 

178 

179 klass = cls.types_factory.for_property(name, value_param) 

180 obj = klass(value) 

181 if parameters: 

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

183 obj.params = Parameters() 

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

185 if item is None: 

186 if key in obj.params: 

187 del obj.params[key] 

188 else: 

189 obj.params[key] = item 

190 return obj 

191 

192 def add( 

193 self, 

194 name: str, 

195 value, 

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

197 encode: bool = True, # noqa: FBT001 

198 ): 

199 """Add a property. 

200 

201 :param name: Name of the property. 

202 :type name: string 

203 

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

205 any of the icalendar's own property types. 

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

207 

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

209 available, if encode is set to True. 

210 :type parameters: Dictionary 

211 

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

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

214 or False, if not. 

215 :type encode: Boolean 

216 

217 :returns: None 

218 """ 

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

220 "dtstamp", 

221 "created", 

222 "last-modified", 

223 ): 

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

225 value = tzp.localize_utc(value) 

226 

227 # encode value 

228 if ( 

229 encode 

230 and isinstance(value, list) 

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

232 ): 

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

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

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

236 else: 

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

238 

239 # set value 

240 if name in self: 

241 # If property already exists, append it. 

242 oldval = self[name] 

243 if isinstance(oldval, list): 

244 if isinstance(value, list): 

245 value = oldval + value 

246 else: 

247 oldval.append(value) 

248 value = oldval 

249 else: 

250 value = [oldval, value] 

251 self[name] = value 

252 

253 def _decode(self, name, value): 

254 """Internal for decoding property values.""" 

255 

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

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

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

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

260 if isinstance(value, vDDDLists): 

261 # TODO: Workaround unfinished decoding 

262 return value 

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

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

265 # Workaround to decode vText properly 

266 if isinstance(decoded, vText): 

267 decoded = decoded.encode(DEFAULT_ENCODING) 

268 return decoded 

269 

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

271 """Returns decoded value of property.""" 

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

273 # -rnix 

274 

275 if name in self: 

276 value = self[name] 

277 if isinstance(value, list): 

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

279 return self._decode(name, value) 

280 if default is _marker: 

281 raise KeyError(name) 

282 return default 

283 

284 ######################################################################## 

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

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

287 

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

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

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

291 if decode: 

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

293 return vals 

294 

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

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

297 to that. 

298 """ 

299 if encode: 

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

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

302 

303 ######################### 

304 # Handling of components 

305 

306 def add_component(self, component: Component): 

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

308 self.subcomponents.append(component) 

309 

310 def _walk(self, name, select): 

311 """Walk to given component.""" 

312 result = [] 

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

314 result.append(self) 

315 for subcomponent in self.subcomponents: 

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

317 return result 

318 

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

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

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

322 

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

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

325 and returns True/False. 

326 :returns: A list of components that match. 

327 :rtype: list[Component] 

328 """ 

329 if name is not None: 

330 name = name.upper() 

331 return self._walk(name, select) 

332 

333 ##################### 

334 # Generation 

335 

336 def property_items( 

337 self, 

338 recursive=True, 

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

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

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

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

343 """ 

344 v_text = self.types_factory["text"] 

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

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

347 

348 for name in property_names: 

349 values = self[name] 

350 if isinstance(values, list): 

351 # normally one property is one line 

352 for value in values: 

353 properties.append((name, value)) 

354 else: 

355 properties.append((name, values)) 

356 if recursive: 

357 # recursion is fun! 

358 for subcomponent in self.subcomponents: 

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

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

361 return properties 

362 

363 @classmethod 

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

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

366 stack = [] # a stack of components 

367 comps = [] 

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

369 if not line: 

370 continue 

371 

372 try: 

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

374 except ValueError as e: 

375 # if unable to parse a line within a component 

376 # that ignores exceptions, mark the component 

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

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

379 if not component or not component.ignore_exceptions: 

380 raise 

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

382 continue 

383 

384 uname = name.upper() 

385 # check for start of component 

386 if uname == "BEGIN": 

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

388 # otherwise get a general Components for robustness. 

389 c_name = vals.upper() 

390 c_class = cls.get_component_class(c_name) 

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

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

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

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

395 component = c_class() 

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

397 component.name = c_name 

398 stack.append(component) 

399 # check for end of event 

400 elif uname == "END": 

401 # we are done adding properties to this component 

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

403 if not stack: 

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

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

406 

407 component = stack.pop() 

408 if not stack: # we are at the end 

409 comps.append(component) 

410 else: 

411 stack[-1].add_component(component) 

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

413 tzp.cache_timezone_component(component) 

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

415 else: 

416 # Extract VALUE parameter if present 

417 value_param = params.get("VALUE") if params else None 

418 factory = cls.types_factory.for_property(name, value_param) 

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

420 if not component: 

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

422 # ignore these components in parsing 

423 if uname == "X-COMMENT": 

424 break 

425 raise ValueError( 

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

427 ) 

428 datetime_names = ( 

429 "DTSTART", 

430 "DTEND", 

431 "RECURRENCE-ID", 

432 "DUE", 

433 "RDATE", 

434 "EXDATE", 

435 ) 

436 try: 

437 if name == "FREEBUSY": 

438 vals = vals.split(",") 

439 if "TZID" in params: 

440 parsed_components = [ 

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

442 for val in vals 

443 ] 

444 else: 

445 parsed_components = [ 

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

447 ] 

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

449 parsed_components = [ 

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

451 ] 

452 # Workaround broken ICS files with empty RDATE 

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

454 parsed_components = [] 

455 else: 

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

457 except ValueError as e: 

458 if not component.ignore_exceptions: 

459 raise 

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

461 else: 

462 for parsed_component in parsed_components: 

463 parsed_component.params = params 

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

465 

466 if multiple: 

467 return comps 

468 if len(comps) > 1: 

469 raise ValueError( 

470 cls._format_error( 

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

472 ) 

473 ) 

474 if len(comps) < 1: 

475 raise ValueError( 

476 cls._format_error( 

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

478 ) 

479 ) 

480 return comps[0] 

481 

482 @staticmethod 

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

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

485 max_error_length = 100 - 3 

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

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

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

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

490 

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

492 """Returns property as content line.""" 

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

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

495 

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

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

498 contentlines = Contentlines() 

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

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

501 contentlines.append(cl) 

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

503 return contentlines 

504 

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

506 """ 

507 :param sorted: Whether parameters and properties should be 

508 lexicographically sorted. 

509 """ 

510 

511 content_lines = self.content_lines(sorted=sorted) 

512 return content_lines.to_ical() 

513 

514 def __repr__(self): 

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

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

517 return ( 

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

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

520 ) 

521 

522 def __eq__(self, other): 

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

524 return False 

525 

526 properties_equal = super().__eq__(other) 

527 if not properties_equal: 

528 return False 

529 

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

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

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

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

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

535 for subcomponent in self.subcomponents: 

536 if subcomponent not in other.subcomponents: 

537 return False 

538 

539 return True 

540 

541 DTSTAMP = stamp = single_utc_property( 

542 "DTSTAMP", 

543 """RFC 5545: 

544 

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

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

547 

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

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

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

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

552 property specifies the date and time that the information 

553 associated with the calendar component was last revised in the 

554 calendar store. 

555 

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

557 

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

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

560 property. 

561 """, 

562 ) 

563 LAST_MODIFIED = single_utc_property( 

564 "LAST-MODIFIED", 

565 """RFC 5545: 

566 

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

568 information associated with the calendar component was last 

569 revised in the calendar store. 

570 

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

572 file in the file system. 

573 

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

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

576 """, 

577 ) 

578 

579 @property 

580 def last_modified(self) -> datetime: 

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

582 

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

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

585 """ 

586 return self.LAST_MODIFIED or self.DTSTAMP 

587 

588 @last_modified.setter 

589 def last_modified(self, value): 

590 self.LAST_MODIFIED = value 

591 

592 @last_modified.deleter 

593 def last_modified(self): 

594 del self.LAST_MODIFIED 

595 

596 @property 

597 def created(self) -> datetime: 

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

599 

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

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

602 """ 

603 return self.CREATED or self.DTSTAMP 

604 

605 @created.setter 

606 def created(self, value): 

607 self.CREATED = value 

608 

609 @created.deleter 

610 def created(self): 

611 del self.CREATED 

612 

613 def is_thunderbird(self) -> bool: 

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

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

616 

617 @staticmethod 

618 def _utc_now(): 

619 """Return now as UTC value.""" 

620 return datetime.now(timezone.utc) 

621 

622 uid = uid_property 

623 comments = comments_property 

624 links = links_property 

625 related_to = related_to_property 

626 concepts = concepts_property 

627 refids = refids_property 

628 

629 CREATED = single_utc_property( 

630 "CREATED", 

631 """ 

632 CREATED specifies the date and time that the calendar 

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

634 store. 

635 

636 Conformance: 

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

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

639 specified as a date with UTC time. 

640 

641 """, 

642 ) 

643 

644 _validate_new = True 

645 

646 @staticmethod 

647 def _validate_start_and_end(start, end): 

648 """This validates start and end. 

649 

650 Raises: 

651 InvalidCalendar: If the information is not valid 

652 """ 

653 if start is None or end is None: 

654 return 

655 if start > end: 

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

657 

658 @classmethod 

659 def new( 

660 cls, 

661 created: date | None = None, 

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

663 concepts: CONCEPTS_TYPE_SETTER = None, 

664 last_modified: date | None = None, 

665 links: LINKS_TYPE_SETTER = None, 

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

667 related_to: RELATED_TO_TYPE_SETTER = None, 

668 stamp: date | None = None, 

669 ) -> Component: 

670 """Create a new component. 

671 

672 Arguments: 

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

674 concepts: The :attr:`concepts` of the component. 

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

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

677 links: The :attr:`links` of the component. 

678 related_to: The :attr:`related_to` of the component. 

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

680 

681 Raises: 

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

683 

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

685 """ 

686 component = cls() 

687 component.DTSTAMP = stamp 

688 component.created = created 

689 component.last_modified = last_modified 

690 component.comments = comments 

691 component.links = links 

692 component.related_to = related_to 

693 component.concepts = concepts 

694 component.refids = refids 

695 return component 

696 

697 

698__all__ = ["Component"]