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

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

423 statements  

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

2 

3from __future__ import annotations 

4 

5import json 

6from copy import deepcopy 

7from dataclasses import dataclass 

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

9from pathlib import Path 

10from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload 

11 

12from icalendar.attr import ( 

13 CONCEPTS_TYPE_SETTER, 

14 LINKS_TYPE_SETTER, 

15 RELATED_TO_TYPE_SETTER, 

16 comments_property, 

17 concepts_property, 

18 links_property, 

19 refids_property, 

20 related_to_property, 

21 single_utc_property, 

22 uid_property, 

23) 

24from icalendar.cal.component_factory import ComponentFactory 

25from icalendar.caselessdict import CaselessDict 

26from icalendar.error import InvalidCalendar, JCalParsingError 

27from icalendar.parser import ( 

28 Contentline, 

29 Contentlines, 

30 Parameters, 

31 q_join, 

32 q_split, 

33) 

34from icalendar.parser.ical.component import ComponentIcalParser 

35from icalendar.parser_tools import DEFAULT_ENCODING 

36from icalendar.prop import VPROPERTY, TypesFactory, vDDDLists, vText, vUnknown 

37from icalendar.timezone import tzp 

38from icalendar.tools import is_date 

39 

40if TYPE_CHECKING: 

41 from collections.abc import Iterable 

42 

43 from icalendar.compatibility import Self 

44 

45_marker = [] 

46 

47 

48@dataclass 

49class _ComponentEqFrame: 

50 """A pending component-equality comparison on the iterative stack. 

51 

52 See ``Component.__eq__`` for how the fields are used. 

53 """ 

54 

55 #: the two components being compared 

56 a: Component 

57 b: Component 

58 #: ``b``'s subcomponents not yet matched against one of ``a``'s. ``None`` 

59 #: until ``a`` and ``b``'s own properties have been compared and found equal. 

60 unmatched: list | None = None 

61 #: index of the ``a`` subcomponent we are currently trying to match 

62 a_index: int = 0 

63 #: index of the unmatched ``b`` subcomponent we are currently testing 

64 candidate_index: int = 0 

65 

66 

67class Component(CaselessDict): 

68 """Base class for calendar components. 

69 

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

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

72 directly, but rather one of the subclasses. 

73 """ 

74 

75 name: ClassVar[str | None] = None 

76 """The name of the component. 

77 

78 This is defined in each component class. 

79 

80 Example: 

81 

82 .. code-block:: pycon 

83 

84 >>> from icalendar import Calendar 

85 >>> cal = Calendar.new() 

86 >>> cal.name 

87 'VCALENDAR' 

88 

89 """ 

90 

91 required: ClassVar[tuple[()]] = () 

92 """These properties are required.""" 

93 

94 singletons: ClassVar[tuple[()]] = () 

95 """These properties must appear only once.""" 

96 

97 multiple: ClassVar[tuple[()]] = () 

98 """These properties may occur more than once.""" 

99 

100 exclusive: ClassVar[tuple[()]] = () 

101 """These properties are mutually exclusive.""" 

102 

103 inclusive: ClassVar[(tuple[str] | tuple[tuple[str, str]])] = () 

104 """These properties are inclusive. 

105 

106 In other words, if the first property in the tuple occurs, then the 

107 second one must also occur. 

108 

109 Example: 

110 

111 .. code-block:: python 

112 

113 ('duration', 'repeat') 

114 """ 

115 

116 ignore_exceptions: ClassVar[bool] = False 

117 """Whether or not to ignore exceptions when parsing. 

118 

119 If ``True``, and this component can't be parsed, then it will silently 

120 ignore it, rather than let the exception propagate upwards. 

121 """ 

122 

123 types_factory: ClassVar[TypesFactory] = TypesFactory.instance() 

124 _components_factory: ClassVar[ComponentFactory | None] = None 

125 

126 subcomponents: list[Component] 

127 """All subcomponents of this component.""" 

128 

129 @classmethod 

130 def _get_component_factory(cls) -> ComponentFactory: 

131 """Get the component factory.""" 

132 if cls._components_factory is None: 

133 cls._components_factory = ComponentFactory() 

134 return cls._components_factory 

135 

136 @classmethod 

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

138 """Return a component with this name. 

139 

140 Parameters: 

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

142 """ 

143 return cls._get_component_factory().get_component_class(name) 

144 

145 @classmethod 

146 def register(cls, component_class: type[Component]) -> None: 

147 """Register a custom component class. 

148 

149 Parameters: 

150 component_class: Component subclass to register. 

151 Must have a ``name`` attribute. 

152 

153 Raises: 

154 ValueError: If ``component_class`` has no ``name`` attribute. 

155 ValueError: If a component with this name is already registered. 

156 

157 Examples: 

158 Create a custom icalendar component with the name ``X-EXAMPLE``: 

159 

160 .. code-block:: pycon 

161 

162 >>> from icalendar import Component 

163 >>> class XExample(Component): 

164 ... name = "X-EXAMPLE" 

165 ... def custom_method(self): 

166 ... return "custom" 

167 >>> Component.register(XExample) 

168 """ 

169 if not hasattr(component_class, "name") or component_class.name is None: 

170 raise ValueError(f"{component_class} must have a 'name' attribute") 

171 

172 # Check if already registered 

173 component_factory = cls._get_component_factory() 

174 existing = component_factory.get(component_class.name) 

175 if existing is not None and existing is not component_class: 

176 raise ValueError( 

177 f"Component '{component_class.name}' is already registered" 

178 f" as {existing}" 

179 ) 

180 

181 component_factory.add_component_class(component_class) 

182 

183 @staticmethod 

184 def _infer_value_type( 

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

186 ) -> str | None: 

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

188 

189 Parameters: 

190 value: Python native type, one of :class:`datetime.date`, :class:`datetime.datetime`, 

191 :class:`datetime.timedelta`, :class:`datetime.time`, :class:`tuple`, 

192 or :class:`list`. 

193 

194 Returns: 

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

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

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

198 """ 

199 if isinstance(value, list): 

200 if not value: 

201 return None 

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

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

204 return "DATE" 

205 # Check if ALL items are time 

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

207 return "TIME" 

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

209 return None 

210 if is_date(value): 

211 return "DATE" 

212 if isinstance(value, time): 

213 return "TIME" 

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

215 return None 

216 

217 def __init__(self, *args: Any, **kwargs: Any) -> None: 

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

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

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

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

222 self.errors: list[ 

223 tuple[str | None, str] 

224 ] = [] # If we ignored exception(s) while 

225 # parsing a property, contains error strings 

226 

227 def __bool__(self) -> bool: 

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

229 return True 

230 

231 def __getitem__(self, key) -> VPROPERTY: 

232 """Get property value from the component dictionary.""" 

233 return super().__getitem__(key) 

234 

235 def get(self, key, default=None) -> Any: 

236 """Get property value with default.""" 

237 try: 

238 return self[key] 

239 except KeyError: 

240 return default 

241 

242 def is_empty(self) -> bool: 

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

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

245 

246 ############################# 

247 # handling of property values 

248 

249 @classmethod 

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

251 """Encode values to icalendar property values. 

252 

253 :param name: Name of the property. 

254 :type name: string 

255 

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

257 any of the icalendar's own property types. 

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

259 

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

261 available, if encode is set to True. 

262 :type parameters: Dictionary 

263 

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

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

266 or False, if not. 

267 :type encode: Boolean 

268 

269 :returns: icalendar property value 

270 """ 

271 if not encode: 

272 return value 

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

274 # Don't encode already encoded values. 

275 obj = value 

276 else: 

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

278 value_param = None 

279 if parameters and "VALUE" in parameters: 

280 value_param = parameters["VALUE"] 

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

282 inferred = cls._infer_value_type(value) 

283 if inferred: 

284 value_param = inferred 

285 # Auto-set the VALUE parameter 

286 if parameters is None: 

287 parameters = {} 

288 if "VALUE" not in parameters: 

289 parameters["VALUE"] = inferred 

290 

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

292 obj = klass(value) 

293 if parameters: 

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

295 obj.params = Parameters() 

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

297 if item is None: 

298 if key in obj.params: 

299 del obj.params[key] 

300 else: 

301 obj.params[key] = item 

302 return obj 

303 

304 def add( 

305 self, 

306 name: str, 

307 value, 

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

309 encode: bool = True, 

310 ) -> None: 

311 """Add a property to this component. 

312 

313 If the property already exists, the new value is appended so the 

314 property carries a list of values rather than replacing the previous 

315 one. When ``name`` is ``DTSTAMP``, ``CREATED``, or ``LAST-MODIFIED`` 

316 and ``value`` is a ``datetime``, the value is converted to UTC as the 

317 RFC requires. 

318 

319 Parameters: 

320 name: Name of the property. 

321 value: 

322 Value of the property. Either a basic Python type or any of 

323 icalendar's own property types. 

324 parameters: 

325 Property parameter dictionary for the value. Only consulted 

326 when ``encode`` is ``True``. 

327 encode: 

328 ``True`` if the value should be encoded to one of icalendar's 

329 own property types (fallback is ``vText``); ``False`` to 

330 store the value as-is. 

331 

332 Returns: 

333 ``None`` 

334 

335 Example: 

336 

337 >>> from icalendar import Event 

338 >>> event = Event() 

339 >>> event.add("summary", "Team sync") 

340 >>> event["summary"] 

341 vText(b'Team sync') 

342 

343 """ 

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

345 "dtstamp", 

346 "created", 

347 "last-modified", 

348 ): 

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

350 value = tzp.localize_utc(value) 

351 

352 # encode value 

353 if ( 

354 encode 

355 and isinstance(value, list) 

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

357 ): 

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

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

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

361 else: 

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

363 

364 # set value 

365 if name in self: 

366 # If property already exists, append it. 

367 oldval = self[name] 

368 if isinstance(oldval, list): 

369 if isinstance(value, list): 

370 value = oldval + value 

371 else: 

372 oldval.append(value) 

373 value = oldval 

374 else: 

375 value = [oldval, value] 

376 self[name] = value 

377 

378 def _decode(self, name: str, value: VPROPERTY): 

379 """Internal for decoding property values.""" 

380 

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

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

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

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

385 if hasattr(value, "ical_value"): 

386 return value.ical_value 

387 if isinstance(value, vDDDLists): 

388 # TODO: Workaround unfinished decoding 

389 return value 

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

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

392 # Workaround to decode vText properly. vUnknown is not a vText 

393 # subclass (RFC 7265), but its value is decoded the same way here. 

394 if isinstance(decoded, (vText, vUnknown)): 

395 decoded = decoded.encode(DEFAULT_ENCODING) 

396 return decoded 

397 

398 def decoded(self, name: str, default: Any = _marker) -> Any: 

399 """Returns decoded value of property. 

400 

401 A component maps keys to icalendar property value types. 

402 This function returns values compatible to native Python types. 

403 """ 

404 if name in self: 

405 value = self[name] 

406 if isinstance(value, list): 

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

408 return self._decode(name, value) 

409 if default is _marker: 

410 raise KeyError(name) 

411 return default 

412 

413 ######################################################################## 

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

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

416 

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

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

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

420 if decode: 

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

422 return vals 

423 

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

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

426 to that. 

427 """ 

428 if encode: 

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

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

431 

432 ######################### 

433 # Handling of components 

434 

435 def add_component(self, component: Component) -> None: 

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

437 self.subcomponents.append(component) 

438 

439 def _walk( 

440 self, name: str | None, select: callable[[Component], bool] 

441 ) -> list[Component]: 

442 """Walk to given component.""" 

443 result = [] 

444 stack = [self] 

445 while stack: 

446 component = stack.pop() 

447 if (name is None or component.name == name) and select(component): 

448 result.append(component) 

449 stack.extend(reversed(component.subcomponents)) 

450 return result 

451 

452 def walk( 

453 self, 

454 name: str | None = None, 

455 select: callable[[Component], bool] = lambda _: True, 

456 ) -> list[Component]: 

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

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

459 

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

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

462 and returns True/False. 

463 :returns: A list of components that match. 

464 :rtype: list[Component] 

465 """ 

466 if name is not None: 

467 name = name.upper() 

468 return self._walk(name, select) 

469 

470 def with_uid(self, uid: str) -> list[Component]: 

471 """Return a list of components with the given UID. 

472 

473 Parameters: 

474 uid: The UID of the component. 

475 

476 Returns: 

477 list[Component]: List of components with the given UID. 

478 """ 

479 return self.walk(select=lambda c: c.uid == uid) 

480 

481 ##################### 

482 # Generation 

483 

484 def property_items( 

485 self, 

486 recursive: bool = True, 

487 sorted: bool = True, 

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

489 """Returns properties in this component and subcomponents as a list. 

490 

491 The list contains ``(name, value)`` tuples. 

492 """ 

493 # Iterative implementation to avoid RecursionError 

494 result = [] 

495 v_text = self.types_factory["text"] 

496 # Stack stores (component, state) 

497 # state: True means we are processing the END of the component 

498 # state: False means we are processing the BEGIN and properties of the component 

499 stack = [(self, False)] 

500 while stack: 

501 comp, is_end = stack.pop() 

502 if is_end: 

503 result.append(("END", v_text(comp.name).to_ical())) 

504 else: 

505 result.append(("BEGIN", v_text(comp.name).to_ical())) 

506 property_names = comp.sorted_keys() if sorted else comp.keys() 

507 

508 for name in property_names: 

509 values = comp[name] 

510 if isinstance(values, list): 

511 # normally one property is one line 

512 for value in values: 

513 result.append((name, value)) 

514 else: 

515 result.append((name, values)) 

516 

517 # Push the END marker for this component 

518 stack.append((comp, True)) 

519 # Push subcomponents if recursion is enabled 

520 if recursive: 

521 # Push in reverse order to maintain original order in result 

522 for subcomponent in reversed(comp.subcomponents): 

523 stack.append((subcomponent, False)) 

524 

525 return result 

526 

527 @overload 

528 @classmethod 

529 def from_ical( 

530 cls, st: str | bytes, multiple: Literal[False] = False 

531 ) -> Component: ... 

532 

533 @overload 

534 @classmethod 

535 def from_ical(cls, st: str | bytes, multiple: Literal[True]) -> list[Component]: ... 

536 

537 @classmethod 

538 def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser: 

539 """Get the iCal parser for the given input string.""" 

540 return ComponentIcalParser(st, cls._get_component_factory(), cls.types_factory) 

541 

542 @classmethod 

543 def from_ical( 

544 cls, st: str | bytes | Path, multiple: bool = False 

545 ) -> Component | list[Component]: 

546 """Parse iCalendar data into component instances. 

547 

548 Handles standard and custom components (``X-*``, IANA-registered). 

549 

550 Parameters: 

551 st: iCalendar data as bytes or string, or a path to an iCalendar file as 

552 :class:`pathlib.Path` or string. 

553 multiple: If ``True``, returns list. If ``False``, returns single component. 

554 

555 Returns: 

556 Component or list of components 

557 

558 See Also: 

559 :doc:`/how-to/custom-components` for examples of parsing custom components 

560 """ 

561 if isinstance(st, Path): 

562 st = st.read_bytes() 

563 elif isinstance(st, str) and "\n" not in st and "\r" not in st: 

564 # A string is only probed as a file path when it contains no line 

565 # breaks. Valid iCalendar data is always folded with CRLF line 

566 # endings (RFC 5545), so real calendar content never reaches this 

567 # branch and is never read from disk. File paths, conversely, do 

568 # not contain line breaks on the platforms we support. 

569 try: 

570 is_file = Path(st).is_file() 

571 except (OSError, ValueError): 

572 # The string is not usable as a path on this platform (e.g. it 

573 # is too long, or contains characters the OS rejects such as an 

574 # embedded null byte). Treat it as calendar data, not a file, so 

575 # the parser raises a consistent ValueError across platforms. 

576 is_file = False 

577 if is_file: 

578 st = Path(st).read_bytes() 

579 parser = cls._get_ical_parser(st) 

580 components = parser.parse() 

581 if multiple: 

582 return components 

583 if len(components) > 1: 

584 raise ValueError( 

585 cls._format_error( 

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

587 ) 

588 ) 

589 if len(components) < 1: 

590 raise ValueError( 

591 cls._format_error( 

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

593 ) 

594 ) 

595 return components[0] 

596 

597 @staticmethod 

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

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

600 max_error_length = 100 - 3 

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

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

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

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

605 

606 def content_line(self, name, value, sorted: bool = True): 

607 """Returns property as content line.""" 

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

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

610 

611 def content_lines(self, sorted: bool = True): 

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

613 contentlines = Contentlines() 

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

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

616 contentlines.append(cl) 

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

618 return contentlines 

619 

620 def to_ical(self, sorted: bool = True): 

621 """ 

622 :param sorted: Whether parameters and properties should be 

623 lexicographically sorted. 

624 """ 

625 

626 content_lines = self.content_lines(sorted=sorted) 

627 return content_lines.to_ical() 

628 

629 def __repr__(self) -> str: 

630 """String representation of class with all of its subcomponents. 

631 

632 Implemented iteratively rather than recursively so that calendars 

633 with deeply nested subcomponents do not raise ``RecursionError``. 

634 A pathological ``.ics`` payload of only ~13 KB can otherwise nest 

635 ``BEGIN:VEVENT`` ~500 levels and crash any caller that performs 

636 ``repr()``/``str()``/``f"{cal}"`` on the parsed calendar 

637 (e.g. logging, error reporting, debug pages). 

638 """ 

639 # Stack-based traversal. Each frame is one of: 

640 # ("open", component) -> emit "Name({props}" and schedule children 

641 # ("close",) -> emit ")" 

642 # ("comma",) -> emit ", " 

643 out: list[str] = [] 

644 stack: list[tuple] = [("open", self)] 

645 while stack: 

646 frame = stack.pop() 

647 kind = frame[0] 

648 if kind == "comma": 

649 out.append(", ") 

650 elif kind == "close": 

651 out.append(")") 

652 else: # "open" 

653 node = frame[1] 

654 if isinstance(node, Component): 

655 out.append(f"{node.name or type(node).__name__}({dict(node)}") 

656 subs = node.subcomponents 

657 if subs: 

658 # Defer ")" then push children in reverse so that 

659 # popping yields original order, with ", " separators 

660 # (the first popped comma serves as the separator 

661 # between the component's dict and its first child). 

662 stack.append(("close",)) 

663 for sub in reversed(subs): 

664 stack.append(("open", sub)) 

665 stack.append(("comma",)) 

666 else: 

667 out.append(")") 

668 else: 

669 # Should not normally occur (subcomponents are Components), 

670 # but be safe and fall back to non-recursive str(). 

671 out.append(str(node)) 

672 return "".join(out) 

673 

674 def __eq__(self, other: Component) -> bool: 

675 if not isinstance(other, Component): 

676 return NotImplemented 

677 

678 # Two components are equal when their own properties are equal and their 

679 # subcomponents are equal as a multiset: order does not matter, and each 

680 # nested pair is compared the same way recursively. Subcomponents are 

681 # neither sortable nor hashable, so we can't use a set; we have to match 

682 # each one by searching. Done recursively that search is exponential for 

683 # deeply nested components (GHSA-cv84-9p8j-fj68), so we walk an explicit 

684 # stack instead of recursing. 

685 # 

686 # Each frame holds the pair being compared plus b's subcomponents 

687 # still unmatched. child_result carries the outcome of the comparison 

688 # that just finished back up to its parent frame: a successful child 

689 # match removes that subcomponent from unmatched and advances to the 

690 # next a subcomponent, while a failure tries the next candidate. 

691 # Exhausting a's subcomponents means every one found a partner -> 

692 # equal; running out of candidates for some subcomponent -> not equal. 

693 # (Greedy matching is sufficient because equality is transitive, so equal 

694 # candidates are interchangeable.) 

695 stack = [_ComponentEqFrame(self, other)] 

696 child_result = None 

697 while stack: 

698 frame = stack[-1] 

699 if frame.unmatched is None: 

700 if len(frame.a.subcomponents) != len(frame.b.subcomponents) or not ( 

701 CaselessDict.__eq__(frame.a, frame.b) 

702 ): 

703 stack.pop() 

704 child_result = False 

705 continue 

706 frame.unmatched = list(frame.b.subcomponents) 

707 elif child_result is not None: 

708 if child_result: 

709 del frame.unmatched[frame.candidate_index] 

710 frame.a_index += 1 

711 frame.candidate_index = 0 

712 else: 

713 frame.candidate_index += 1 

714 child_result = None 

715 if frame.a_index >= len(frame.a.subcomponents): 

716 stack.pop() 

717 child_result = True 

718 elif frame.candidate_index >= len(frame.unmatched): 

719 stack.pop() 

720 child_result = False 

721 else: 

722 stack.append( 

723 _ComponentEqFrame( 

724 frame.a.subcomponents[frame.a_index], 

725 frame.unmatched[frame.candidate_index], 

726 ) 

727 ) 

728 return child_result 

729 

730 DTSTAMP = stamp = single_utc_property( 

731 "DTSTAMP", 

732 """The UTC datetime stamp recording when this component instance was created or last revised. 

733 

734 This property is defined in :rfc:`5545#section-3.8.7.2`. It's required 

735 in ``VEVENT``, ``VTODO``, ``VJOURNAL``, and ``VFREEBUSY`` components. 

736 

737 When the calendar object carries a ``METHOD`` property, such as for 

738 scheduling, this value is the creation time of *this particular revision*. 

739 Without a ``METHOD`` property, it's equivalent to :attr:`LAST_MODIFIED`. 

740 

741 The value is always in UTC. It's also accessible as :attr:`stamp`. 

742 

743 Example: 

744 .. code-block:: pycon 

745 

746 >>> from datetime import timezone, datetime 

747 >>> from icalendar import Event 

748 >>> event = Event() 

749 >>> event.DTSTAMP = datetime(2024, 6, 1, 12, 0, 0, tzinfo=timezone.utc) 

750 >>> event.DTSTAMP 

751 datetime.datetime(2024, 6, 1, 12, 0, tzinfo=ZoneInfo(key='UTC')) 

752 

753 See also: 

754 :attr:`CREATED`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`, 

755 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

756 """, 

757 ) 

758 

759 LAST_MODIFIED = single_utc_property( 

760 "LAST-MODIFIED", 

761 """The UTC datetime when this component's information was last revised, per :rfc:`5545#section-3.8.7.3`. 

762 

763 It's analogous to a file's modification timestamp. This property is optional. 

764 When it's absent, :attr:`last_modified` falls back to :attr:`DTSTAMP`. 

765 

766 This property is applicable to ``VEVENT``, ``VTODO``, ``VJOURNAL``, and ``VTIMEZONE`` 

767 components. The value is always in UTC. 

768 

769 Example: 

770 .. code-block:: pycon 

771 

772 >>> from datetime import timezone, datetime 

773 >>> from icalendar import Event 

774 >>> event = Event() 

775 >>> event.LAST_MODIFIED = datetime(2024, 6, 1, 9, 0, 0, tzinfo=timezone.utc) 

776 >>> event.LAST_MODIFIED 

777 datetime.datetime(2024, 6, 1, 9, 0, tzinfo=ZoneInfo(key='UTC')) 

778 

779 See also: 

780 :attr:`CREATED`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`, 

781 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

782 """, 

783 ) 

784 

785 @property 

786 def last_modified(self) -> datetime: 

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

788 

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

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

791 """ 

792 return self.LAST_MODIFIED or self.DTSTAMP 

793 

794 @last_modified.setter 

795 def last_modified(self, value): 

796 self.LAST_MODIFIED = value 

797 

798 @last_modified.deleter 

799 def last_modified(self): 

800 del self.LAST_MODIFIED 

801 

802 @property 

803 def created(self) -> datetime: 

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

805 

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

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

808 """ 

809 return self.CREATED or self.DTSTAMP 

810 

811 @created.setter 

812 def created(self, value): 

813 self.CREATED = value 

814 

815 @created.deleter 

816 def created(self): 

817 del self.CREATED 

818 

819 def is_thunderbird(self) -> bool: 

820 """Whether this component has attributes that indicate that Mozilla Thunderbird created it.""" 

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

822 

823 @staticmethod 

824 def _utc_now() -> datetime: 

825 """Return now as UTC value.""" 

826 return datetime.now(timezone.utc) 

827 

828 uid = uid_property 

829 comments = comments_property 

830 links = links_property 

831 related_to = related_to_property 

832 concepts = concepts_property 

833 refids = refids_property 

834 

835 CREATED = single_utc_property( 

836 "CREATED", 

837 """The UTC datetime when this calendar component was first created, per :rfc:`5545#section-3.8.7.1`. 

838 

839 This property records when the calendar user agent originally stored the component. 

840 This property is optional. When it's absent, :attr:`created` falls back to 

841 :attr:`DTSTAMP`. 

842 

843 This property is applicable to ``VEVENT``, ``VTODO``, and ``VJOURNAL`` components. 

844 The value is always in UTC. 

845 

846 Example: 

847 .. code-block:: pycon 

848 

849 >>> from datetime import timezone, datetime 

850 >>> from icalendar import Event 

851 >>> event = Event() 

852 >>> event.CREATED = datetime(2024, 1, 1, 8, 0, 0, tzinfo=timezone.utc) 

853 >>> event.CREATED 

854 datetime.datetime(2024, 1, 1, 8, 0, tzinfo=ZoneInfo(key='UTC')) 

855 

856 See also: 

857 :attr:`CREATED`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`, 

858 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

859 """, 

860 ) 

861 

862 _validate_new = True 

863 

864 @staticmethod 

865 def _validate_start_and_end(start, end): 

866 """This validates start and end. 

867 

868 Raises: 

869 ~error.InvalidCalendar: If the information is not valid 

870 """ 

871 if start is None or end is None: 

872 return 

873 if start > end: 

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

875 

876 @classmethod 

877 def new( 

878 cls, 

879 created: date | None = None, 

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

881 concepts: CONCEPTS_TYPE_SETTER = None, 

882 last_modified: date | None = None, 

883 links: LINKS_TYPE_SETTER = None, 

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

885 related_to: RELATED_TO_TYPE_SETTER = None, 

886 stamp: date | None = None, 

887 subcomponents: Iterable[Component] | None = None, 

888 ) -> Component: 

889 """Create a new component. 

890 

891 Parameters: 

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

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

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

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

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

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

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

899 subcomponents: The subcomponents of the component. 

900 

901 Raises: 

902 ~error.InvalidCalendar: If the content is not valid 

903 according to :rfc:`5545`. 

904 

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

906 validation. 

907 """ 

908 component = cls() 

909 component.DTSTAMP = stamp 

910 component.created = created 

911 component.last_modified = last_modified 

912 component.comments = comments 

913 component.links = links 

914 component.related_to = related_to 

915 component.concepts = concepts 

916 component.refids = refids 

917 if subcomponents is not None: 

918 component.subcomponents = ( 

919 subcomponents 

920 if isinstance(subcomponents, list) 

921 else list(subcomponents) 

922 ) 

923 return component 

924 

925 def to_jcal(self) -> list: 

926 """Convert this component to a jCal object. 

927 

928 Returns: 

929 jCal object 

930 

931 See also :attr:`to_json`. 

932 

933 In this example, we create a simple VEVENT component and convert it to jCal: 

934 

935 .. code-block:: pycon 

936 

937 >>> from icalendar import Event 

938 >>> from datetime import date 

939 >>> from pprint import pprint 

940 >>> event = Event.new(summary="My Event", start=date(2025, 11, 22)) 

941 >>> pprint(event.to_jcal()) 

942 ['vevent', 

943 [['dtstamp', {}, 'date-time', '2025-05-17T08:06:12Z'], 

944 ['summary', {}, 'text', 'My Event'], 

945 ['uid', {}, 'text', 'd755cef5-2311-46ed-a0e1-6733c9e15c63'], 

946 ['dtstart', {}, 'date', '2025-11-22']], 

947 []] 

948 """ 

949 

950 # Iterative tree walk to avoid RecursionError on deeply nested 

951 # components, mirroring the iterative iCal parser/serializer (GH #1370). 

952 def make_node(comp: Component) -> list: 

953 properties = [ 

954 item.to_jcal(key.lower()) 

955 for key, value in comp.items() 

956 for item in (value if isinstance(value, list) else [value]) 

957 ] 

958 return [comp.name.lower(), properties, []] 

959 

960 root_node = make_node(self) 

961 # stack of (component, jCal node) pairs still to expand 

962 stack: list[tuple[Component, list]] = [(self, root_node)] 

963 while stack: 

964 comp, node = stack.pop() 

965 children = node[2] 

966 for subcomponent in comp.subcomponents: 

967 child_node = make_node(subcomponent) 

968 children.append(child_node) 

969 stack.append((subcomponent, child_node)) 

970 return root_node 

971 

972 def to_json(self) -> str: 

973 """Return this component as a jCal JSON string. 

974 

975 Returns: 

976 JSON string 

977 

978 See also :attr:`to_jcal`. 

979 """ 

980 return json.dumps(self.to_jcal()) 

981 

982 @classmethod 

983 def from_jcal(cls, jcal: str | list) -> Component: 

984 """Create a component from a jCal list. 

985 

986 Parameters: 

987 jcal: jCal list or JSON string according to :rfc:`7265`. 

988 

989 Raises: 

990 ~error.JCalParsingError: If the jCal provided is invalid. 

991 ~json.JSONDecodeError: If the provided string is not valid JSON. 

992 

993 This reverses :func:`to_json` and :func:`to_jcal`. 

994 

995 The following code parses an example from :rfc:`7265`: 

996 

997 .. code-block:: pycon 

998 

999 >>> from icalendar import Component 

1000 >>> jcal = ["vcalendar", 

1001 ... [ 

1002 ... ["calscale", {}, "text", "GREGORIAN"], 

1003 ... ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"], 

1004 ... ["version", {}, "text", "2.0"] 

1005 ... ], 

1006 ... [ 

1007 ... ["vevent", 

1008 ... [ 

1009 ... ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"], 

1010 ... ["dtstart", {}, "date", "2008-10-06"], 

1011 ... ["summary", {}, "text", "Planning meeting"], 

1012 ... ["uid", {}, "text", "4088E990AD89CB3DBB484909"] 

1013 ... ], 

1014 ... [] 

1015 ... ] 

1016 ... ] 

1017 ... ] 

1018 >>> calendar = Component.from_jcal(jcal) 

1019 >>> print(calendar.name) 

1020 VCALENDAR 

1021 >>> print(calendar.prodid) 

1022 -//Example Inc.//Example Calendar//EN 

1023 >>> event = calendar.events[0] 

1024 >>> print(event.summary) 

1025 Planning meeting 

1026 

1027 """ 

1028 if isinstance(jcal, str): 

1029 jcal = json.loads(jcal) 

1030 # Iterative tree build to avoid RecursionError on deeply nested jCal, 

1031 # mirroring the iterative iCal parser (GH #1370). ``_node_from_jcal`` 

1032 # parses a single component (without its subcomponents); the stack walks 

1033 # the subcomponent tree, accumulating the jCal error path ([2, i] per 

1034 # nesting level) so error messages match the recursive implementation. 

1035 root, root_subcomponents = _node_from_jcal(jcal, cls) 

1036 stack: list[tuple[Component, list, list]] = [(root, root_subcomponents, [])] 

1037 while stack: 

1038 parent, subcomponents, prefix = stack.pop() 

1039 for i, subcomponent in enumerate(subcomponents): 

1040 child_prefix = [*prefix, 2, i] 

1041 # Prepend the full nesting path so errors match the recursive 

1042 # implementation. This also preserves the error value and 

1043 # traceback, like the nested context managers did before. 

1044 with JCalParsingError.reraise_with_path_added(*child_prefix): 

1045 child, child_subcomponents = _node_from_jcal( 

1046 subcomponent, type(parent) 

1047 ) 

1048 parent.subcomponents.append(child) 

1049 stack.append((child, child_subcomponents, child_prefix)) 

1050 return root 

1051 

1052 def copy(self, recursive: bool = False) -> Self: 

1053 """Copy the component. 

1054 

1055 Parameters: 

1056 recursive: 

1057 If ``True``, this creates copies of the component, its subcomponents, 

1058 and all its properties. 

1059 If ``False``, this only creates a shallow copy of the component. 

1060 

1061 Returns: 

1062 A copy of the component. 

1063 

1064 Examples: 

1065 

1066 Create a shallow copy of a component: 

1067 

1068 .. code-block:: pycon 

1069 

1070 >>> from icalendar import Event 

1071 >>> event = Event.new(description="Event to be copied") 

1072 >>> event_copy = event.copy() 

1073 >>> str(event_copy.description) 

1074 'Event to be copied' 

1075 

1076 Shallow copies lose their subcomponents: 

1077 

1078 .. code-block:: pycon 

1079 

1080 >>> from icalendar import Calendar 

1081 >>> calendar = Calendar.example() 

1082 >>> len(calendar.subcomponents) 

1083 3 

1084 >>> calendar_copy = calendar.copy() 

1085 >>> len(calendar_copy.subcomponents) 

1086 0 

1087 

1088 A recursive copy also copies all the subcomponents: 

1089 

1090 .. code-block:: pycon 

1091 

1092 >>> full_calendar_copy = calendar.copy(recursive=True) 

1093 >>> len(full_calendar_copy.subcomponents) 

1094 3 

1095 >>> full_calendar_copy.events[0] == calendar.events[0] 

1096 True 

1097 >>> full_calendar_copy.events[0] is calendar.events[0] 

1098 False 

1099 

1100 """ 

1101 if recursive: 

1102 return deepcopy(self) 

1103 return super().copy() 

1104 

1105 def is_lazy(self) -> bool: 

1106 """This component is fully parsed.""" 

1107 return False 

1108 

1109 def parse(self) -> Self: 

1110 """Return the fully parsed component. 

1111 

1112 For non-lazy components, this returns self. 

1113 For lazy components, this parses the component and returns the result. 

1114 """ 

1115 return self 

1116 

1117 

1118def _node_from_jcal(jcal, starting_cls: type[Component]) -> tuple[Component, list]: 

1119 """Parse a single jCal component without recursing into subcomponents. 

1120 

1121 Module-level helper for :meth:`Component.from_jcal`: it has no ties to a 

1122 class or instance (the relevant class is passed in as ``starting_cls``), so 

1123 it is a plain function rather than a (static) method. 

1124 

1125 Parameters: 

1126 jcal: The jCal list for one component. 

1127 starting_cls: The class used as the parser for structural validation 

1128 before the component type is resolved from its name (the entry 

1129 class for the root, the parent's resolved class for a child). 

1130 

1131 Returns: 

1132 A ``(component, raw_subcomponents)`` tuple. The raw subcomponents are 

1133 returned for the caller to walk iteratively. 

1134 

1135 Raises: 

1136 ~error.JCalParsingError: If this component node is invalid. The path 

1137 is relative to this node; callers prepend the nesting path. 

1138 """ 

1139 if not isinstance(jcal, list) or len(jcal) != 3: 

1140 raise JCalParsingError( 

1141 "A component must be a list with 3 items.", starting_cls, value=jcal 

1142 ) 

1143 name, properties, subcomponents = jcal 

1144 if not isinstance(name, str): 

1145 raise JCalParsingError( 

1146 "The name must be a string.", starting_cls, path=[0], value=name 

1147 ) 

1148 if name.upper() != starting_cls.name: 

1149 # delegate to correct component class 

1150 component_cls = starting_cls.get_component_class(name.upper()) 

1151 else: 

1152 component_cls = starting_cls 

1153 component = component_cls() 

1154 if not isinstance(properties, list): 

1155 raise JCalParsingError( 

1156 "The properties must be a list.", 

1157 component_cls, 

1158 path=1, 

1159 value=properties, 

1160 ) 

1161 for i, prop in enumerate(properties): 

1162 JCalParsingError.validate_property(prop, component_cls, path=[1, i]) 

1163 prop_name = prop[0] 

1164 prop_value = prop[2] 

1165 prop_cls: type[VPROPERTY] = component_cls.types_factory.for_property( 

1166 prop_name, prop_value 

1167 ) 

1168 with JCalParsingError.reraise_with_path_added(1, i): 

1169 v_prop = prop_cls.from_jcal(prop) 

1170 # jCal encodes the value type in the type field (``prop[2]``) 

1171 # instead of as a ``VALUE`` parameter (RFC 7265). Restore that 

1172 # parameter when the type differs from the property's default, so 

1173 # explicit value types such as ``RDATE;VALUE=PERIOD`` or 

1174 # ``TRIGGER;VALUE=DATE-TIME`` survive the round-trip (GH #1426). 

1175 # A type equal to the default needs no VALUE parameter, and the 

1176 # reserved ``unknown`` type must never become ``VALUE=UNKNOWN`` 

1177 # (RFC 7265, section 5.2). 

1178 default_type = component_cls.types_factory.default_value_type(prop_name) 

1179 if isinstance(prop_value, str) and prop_value.lower() not in ( 

1180 "unknown", 

1181 default_type, 

1182 ): 

1183 v_prop.VALUE = prop_value.upper() 

1184 elif "VALUE" in v_prop.params: 

1185 del v_prop.VALUE 

1186 component.add(prop_name, v_prop) 

1187 if not isinstance(subcomponents, list): 

1188 raise JCalParsingError( 

1189 "The subcomponents must be a list.", 

1190 component_cls, 

1191 2, 

1192 value=subcomponents, 

1193 ) 

1194 return component, subcomponents 

1195 

1196 

1197__all__ = ["Component"]