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

425 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 | Path, multiple: Literal[False] = False 

531 ) -> Component: ... 

532 

533 @overload 

534 @classmethod 

535 def from_ical( 

536 cls, st: str | bytes | Path, multiple: Literal[True] 

537 ) -> list[Component]: ... 

538 

539 @classmethod 

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

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

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

543 

544 @classmethod 

545 def from_ical( 

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

547 ) -> Component | list[Component]: 

548 """Parse iCalendar data into component instances. 

549 

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

551 

552 Parameters: 

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

554 :class:`pathlib.Path`. 

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

556 

557 Returns: 

558 Component or list of components 

559 

560 See Also: 

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

562 """ 

563 if isinstance(st, Path): 

564 st = st.read_bytes() 

565 parser = cls._get_ical_parser(st) 

566 components = parser.parse() 

567 if multiple: 

568 return components 

569 if len(components) > 1: 

570 raise ValueError( 

571 cls._format_error( 

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

573 ) 

574 ) 

575 if len(components) < 1: 

576 raise ValueError( 

577 cls._format_error( 

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

579 ) 

580 ) 

581 return components[0] 

582 

583 @staticmethod 

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

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

586 max_error_length = 100 - 3 

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

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

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

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

591 

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

593 """Returns property as content line.""" 

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

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

596 

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

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

599 contentlines = Contentlines() 

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

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

602 contentlines.append(cl) 

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

604 return contentlines 

605 

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

607 """ 

608 :param sorted: Whether parameters and properties should be 

609 lexicographically sorted. 

610 """ 

611 

612 content_lines = self.content_lines(sorted=sorted) 

613 return content_lines.to_ical() 

614 

615 def __repr__(self) -> str: 

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

617 

618 Implemented iteratively rather than recursively so that calendars 

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

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

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

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

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

624 """ 

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

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

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

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

629 out: list[str] = [] 

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

631 while stack: 

632 frame = stack.pop() 

633 kind = frame[0] 

634 if kind == "comma": 

635 out.append(", ") 

636 elif kind == "close": 

637 out.append(")") 

638 else: # "open" 

639 node = frame[1] 

640 if isinstance(node, Component): 

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

642 subs = node.subcomponents 

643 if subs: 

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

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

646 # (the first popped comma serves as the separator 

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

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

649 for sub in reversed(subs): 

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

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

652 else: 

653 out.append(")") 

654 else: 

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

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

657 out.append(str(node)) 

658 return "".join(out) 

659 

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

661 if not isinstance(other, Component): 

662 return NotImplemented 

663 

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

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

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

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

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

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

670 # stack instead of recursing. 

671 # 

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

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

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

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

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

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

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

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

680 # candidates are interchangeable.) 

681 stack = [_ComponentEqFrame(self, other)] 

682 child_result = None 

683 while stack: 

684 frame = stack[-1] 

685 if frame.unmatched is None: 

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

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

688 ): 

689 stack.pop() 

690 child_result = False 

691 continue 

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

693 elif child_result is not None: 

694 if child_result: 

695 del frame.unmatched[frame.candidate_index] 

696 frame.a_index += 1 

697 frame.candidate_index = 0 

698 else: 

699 frame.candidate_index += 1 

700 child_result = None 

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

702 stack.pop() 

703 child_result = True 

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

705 stack.pop() 

706 child_result = False 

707 else: 

708 stack.append( 

709 _ComponentEqFrame( 

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

711 frame.unmatched[frame.candidate_index], 

712 ) 

713 ) 

714 return child_result 

715 

716 DTSTAMP = single_utc_property( 

717 "DTSTAMP", 

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

719 

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

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

722 

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

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

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

726 

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

728 

729 Example: 

730 .. code-block:: pycon 

731 

732 >>> from datetime import timezone, datetime 

733 >>> from icalendar import Event 

734 >>> event = Event() 

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

736 >>> event.DTSTAMP 

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

738 

739 See also: 

740 :attr:`CREATED`, :attr:`LAST_MODIFIED`, 

741 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

742 """, 

743 ) 

744 

745 @property 

746 def stamp(self) -> datetime | None: 

747 """Datetime stamp of this component, as a :class:`~datetime.datetime` in UTC. 

748 

749 This is the lowercase property counterpart to, and accessor for, :attr:`DTSTAMP`. 

750 """ 

751 return self.DTSTAMP 

752 

753 @stamp.setter 

754 def stamp(self, value: datetime) -> None: 

755 self.DTSTAMP = value 

756 

757 @stamp.deleter 

758 def stamp(self) -> None: 

759 del self.DTSTAMP 

760 

761 LAST_MODIFIED = single_utc_property( 

762 "LAST-MODIFIED", 

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

764 

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

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

767 

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

769 components. The value is always in UTC. 

770 

771 Example: 

772 .. code-block:: pycon 

773 

774 >>> from datetime import timezone, datetime 

775 >>> from icalendar import Event 

776 >>> event = Event() 

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

778 >>> event.LAST_MODIFIED 

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

780 

781 See also: 

782 :attr:`CREATED`, :attr:`DTSTAMP`, 

783 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

784 """, 

785 ) 

786 

787 @property 

788 def last_modified(self) -> datetime: 

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

790 

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

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

793 """ 

794 return self.LAST_MODIFIED or self.DTSTAMP 

795 

796 @last_modified.setter 

797 def last_modified(self, value): 

798 self.LAST_MODIFIED = value 

799 

800 @last_modified.deleter 

801 def last_modified(self): 

802 del self.LAST_MODIFIED 

803 

804 @property 

805 def created(self) -> datetime: 

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

807 

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

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

810 """ 

811 return self.CREATED or self.DTSTAMP 

812 

813 @created.setter 

814 def created(self, value): 

815 self.CREATED = value 

816 

817 @created.deleter 

818 def created(self): 

819 del self.CREATED 

820 

821 def is_thunderbird(self) -> bool: 

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

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

824 

825 @staticmethod 

826 def _utc_now() -> datetime: 

827 """Return now as UTC value.""" 

828 return datetime.now(timezone.utc) 

829 

830 uid = uid_property 

831 comments = comments_property 

832 links = links_property 

833 related_to = related_to_property 

834 concepts = concepts_property 

835 refids = refids_property 

836 

837 CREATED = single_utc_property( 

838 "CREATED", 

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

840 

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

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

843 :attr:`DTSTAMP`. 

844 

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

846 The value is always in UTC. 

847 

848 Example: 

849 .. code-block:: pycon 

850 

851 >>> from datetime import timezone, datetime 

852 >>> from icalendar import Event 

853 >>> event = Event() 

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

855 >>> event.CREATED 

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

857 

858 See also: 

859 :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`, 

860 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

861 """, 

862 ) 

863 

864 _validate_new = True 

865 

866 @staticmethod 

867 def _validate_start_and_end(start, end): 

868 """This validates start and end. 

869 

870 Raises: 

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

872 """ 

873 if start is None or end is None: 

874 return 

875 if start > end: 

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

877 

878 @classmethod 

879 def new( 

880 cls, 

881 created: date | None = None, 

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

883 concepts: CONCEPTS_TYPE_SETTER = None, 

884 last_modified: date | None = None, 

885 links: LINKS_TYPE_SETTER = None, 

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

887 related_to: RELATED_TO_TYPE_SETTER = None, 

888 stamp: date | None = None, 

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

890 ) -> Self: 

891 """Create a new component. 

892 

893 Parameters: 

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

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

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

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

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

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

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

901 subcomponents: The subcomponents of the component. 

902 

903 Raises: 

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

905 according to :rfc:`5545`. 

906 

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

908 validation. 

909 """ 

910 component = cls() 

911 component.DTSTAMP = stamp 

912 component.created = created 

913 component.last_modified = last_modified 

914 component.comments = comments 

915 component.links = links 

916 component.related_to = related_to 

917 component.concepts = concepts 

918 component.refids = refids 

919 if subcomponents is not None: 

920 component.subcomponents = ( 

921 subcomponents 

922 if isinstance(subcomponents, list) 

923 else list(subcomponents) 

924 ) 

925 return component 

926 

927 def to_jcal(self) -> list: 

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

929 

930 Returns: 

931 jCal object 

932 

933 See also :attr:`to_json`. 

934 

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

936 

937 .. code-block:: pycon 

938 

939 >>> from icalendar import Event 

940 >>> from datetime import date 

941 >>> from pprint import pprint 

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

943 >>> pprint(event.to_jcal()) 

944 ['vevent', 

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

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

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

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

949 []] 

950 """ 

951 

952 # Iterative tree walk to avoid RecursionError on deeply nested 

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

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

955 properties = [ 

956 item.to_jcal(key.lower()) 

957 for key, value in comp.items() 

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

959 ] 

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

961 

962 root_node = make_node(self) 

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

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

965 while stack: 

966 comp, node = stack.pop() 

967 children = node[2] 

968 for subcomponent in comp.subcomponents: 

969 child_node = make_node(subcomponent) 

970 children.append(child_node) 

971 stack.append((subcomponent, child_node)) 

972 return root_node 

973 

974 def to_json(self) -> str: 

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

976 

977 Returns: 

978 JSON string 

979 

980 See also :attr:`to_jcal`. 

981 """ 

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

983 

984 @classmethod 

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

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

987 

988 Parameters: 

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

990 

991 Raises: 

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

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

994 

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

996 

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

998 

999 .. code-block:: pycon 

1000 

1001 >>> from icalendar import Component 

1002 >>> jcal = ["vcalendar", 

1003 ... [ 

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

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

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

1007 ... ], 

1008 ... [ 

1009 ... ["vevent", 

1010 ... [ 

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

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

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

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

1015 ... ], 

1016 ... [] 

1017 ... ] 

1018 ... ] 

1019 ... ] 

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

1021 >>> print(calendar.name) 

1022 VCALENDAR 

1023 >>> print(calendar.prodid) 

1024 -//Example Inc.//Example Calendar//EN 

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

1026 >>> print(event.summary) 

1027 Planning meeting 

1028 

1029 """ 

1030 if isinstance(jcal, str): 

1031 jcal = json.loads(jcal) 

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

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

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

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

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

1037 root, root_subcomponents = _node_from_jcal(jcal, cls) 

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

1039 while stack: 

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

1041 for i, subcomponent in enumerate(subcomponents): 

1042 child_prefix = [*prefix, 2, i] 

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

1044 # implementation. This also preserves the error value and 

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

1046 with JCalParsingError.reraise_with_path_added(*child_prefix): 

1047 child, child_subcomponents = _node_from_jcal( 

1048 subcomponent, type(parent) 

1049 ) 

1050 parent.subcomponents.append(child) 

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

1052 return root 

1053 

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

1055 """Copy the component. 

1056 

1057 Parameters: 

1058 recursive: 

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

1060 and all its properties. 

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

1062 

1063 Returns: 

1064 A copy of the component. 

1065 

1066 Examples: 

1067 

1068 Create a shallow copy of a component: 

1069 

1070 .. code-block:: pycon 

1071 

1072 >>> from icalendar import Event 

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

1074 >>> event_copy = event.copy() 

1075 >>> str(event_copy.description) 

1076 'Event to be copied' 

1077 

1078 Shallow copies lose their subcomponents: 

1079 

1080 .. code-block:: pycon 

1081 

1082 >>> from icalendar import Calendar 

1083 >>> calendar = Calendar.example() 

1084 >>> len(calendar.subcomponents) 

1085 3 

1086 >>> calendar_copy = calendar.copy() 

1087 >>> len(calendar_copy.subcomponents) 

1088 0 

1089 

1090 A recursive copy also copies all the subcomponents: 

1091 

1092 .. code-block:: pycon 

1093 

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

1095 >>> len(full_calendar_copy.subcomponents) 

1096 3 

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

1098 True 

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

1100 False 

1101 

1102 """ 

1103 if recursive: 

1104 return deepcopy(self) 

1105 return super().copy() 

1106 

1107 def is_lazy(self) -> bool: 

1108 """This component is fully parsed.""" 

1109 return False 

1110 

1111 def parse(self) -> Self: 

1112 """Return the fully parsed component. 

1113 

1114 For non-lazy components, this returns self. 

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

1116 """ 

1117 return self 

1118 

1119 

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

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

1122 

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

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

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

1126 

1127 Parameters: 

1128 jcal: The jCal list for one component. 

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

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

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

1132 

1133 Returns: 

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

1135 returned for the caller to walk iteratively. 

1136 

1137 Raises: 

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

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

1140 """ 

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

1142 raise JCalParsingError( 

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

1144 ) 

1145 name, properties, subcomponents = jcal 

1146 if not isinstance(name, str): 

1147 raise JCalParsingError( 

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

1149 ) 

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

1151 # delegate to correct component class 

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

1153 else: 

1154 component_cls = starting_cls 

1155 component = component_cls() 

1156 if not isinstance(properties, list): 

1157 raise JCalParsingError( 

1158 "The properties must be a list.", 

1159 component_cls, 

1160 path=1, 

1161 value=properties, 

1162 ) 

1163 for i, prop in enumerate(properties): 

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

1165 prop_name = prop[0] 

1166 prop_value = prop[2] 

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

1168 prop_name, prop_value 

1169 ) 

1170 with JCalParsingError.reraise_with_path_added(1, i): 

1171 v_prop = prop_cls.from_jcal(prop) 

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

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

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

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

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

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

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

1179 # (RFC 7265, section 5.2). 

1180 default_type = component_cls.types_factory.default_value_type(prop_name) 

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

1182 "unknown", 

1183 default_type, 

1184 ): 

1185 v_prop.VALUE = prop_value.upper() 

1186 elif "VALUE" in v_prop.params: 

1187 del v_prop.VALUE 

1188 component.add(prop_name, v_prop) 

1189 if not isinstance(subcomponents, list): 

1190 raise JCalParsingError( 

1191 "The subcomponents must be a list.", 

1192 component_cls, 

1193 2, 

1194 value=subcomponents, 

1195 ) 

1196 return component, subcomponents 

1197 

1198 

1199__all__ = ["Component"]