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

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

500 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 io import BytesIO 

10from pathlib import Path 

11from typing import IO, TYPE_CHECKING, Any, BinaryIO, ClassVar, Literal, overload 

12from xml.etree.ElementTree import Element, ElementTree, SubElement 

13from xml.etree.ElementTree import indent as indent_xml 

14 

15from icalendar import config 

16from icalendar.attr import ( 

17 CONCEPTS_TYPE_SETTER, 

18 LINKS_TYPE_SETTER, 

19 RELATED_TO_TYPE_SETTER, 

20 comments_property, 

21 concepts_property, 

22 links_property, 

23 refids_property, 

24 related_to_property, 

25 single_utc_property, 

26 uid_property, 

27) 

28from icalendar.cal.component_factory import ComponentFactory 

29from icalendar.caselessdict import CaselessDict 

30from icalendar.error import InvalidCalendar, JCalParsingError 

31from icalendar.parser import ( 

32 Contentline, 

33 Contentlines, 

34 Parameters, 

35 q_join, 

36 q_split, 

37) 

38from icalendar.parser.ical.component import ComponentIcalParser 

39from icalendar.parser_tools import DEFAULT_ENCODING 

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

41from icalendar.timezone import tzp 

42from icalendar.tools import is_date 

43 

44if TYPE_CHECKING: 

45 from collections.abc import Iterable 

46 

47 from icalendar.compatibility import Self 

48 

49_marker = [] 

50 

51 

52@dataclass 

53class _ComponentEqFrame: 

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

55 

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

57 """ 

58 

59 #: the two components being compared 

60 a: Component 

61 b: Component 

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

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

64 unmatched: list | None = None 

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

66 a_index: int = 0 

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

68 candidate_index: int = 0 

69 

70 

71class Component(CaselessDict): 

72 """Base class for calendar components. 

73 

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

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

76 directly, but rather one of the subclasses. 

77 """ 

78 

79 name: ClassVar[str | None] = None 

80 """The name of the component. 

81 

82 This is defined in each component class. 

83 

84 Example: 

85 

86 .. code-block:: pycon 

87 

88 >>> from icalendar import Calendar 

89 >>> cal = Calendar.new() 

90 >>> cal.name 

91 'VCALENDAR' 

92 

93 """ 

94 

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

96 """These properties are required.""" 

97 

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

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

100 

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

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

103 

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

105 """These properties are mutually exclusive.""" 

106 

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

108 """These properties are inclusive. 

109 

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

111 second one must also occur. 

112 

113 Example: 

114 

115 .. code-block:: python 

116 

117 ('duration', 'repeat') 

118 """ 

119 

120 ignore_exceptions: ClassVar[bool] = False 

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

122 

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

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

125 """ 

126 

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

128 """The factory class to parse and wrap properties.""" 

129 

130 _components_factory: ClassVar[ComponentFactory | None] = None 

131 

132 subcomponents: list[Component] 

133 """All subcomponents of this component.""" 

134 

135 @classmethod 

136 def _get_component_factory(cls) -> ComponentFactory: 

137 """Get the component factory.""" 

138 if cls._components_factory is None: 

139 cls._components_factory = ComponentFactory() 

140 return cls._components_factory 

141 

142 @classmethod 

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

144 """Return a component with this name. 

145 

146 Parameters: 

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

148 """ 

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

150 

151 @classmethod 

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

153 """Register a custom component class. 

154 

155 Parameters: 

156 component_class: Component subclass to register. 

157 Must have a ``name`` attribute. 

158 

159 Raises: 

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

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

162 

163 Examples: 

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

165 

166 .. code-block:: pycon 

167 

168 >>> from icalendar import Component 

169 >>> class XExample(Component): 

170 ... name = "X-EXAMPLE" 

171 ... def custom_method(self): 

172 ... return "custom" 

173 >>> Component.register(XExample) 

174 """ 

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

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

177 

178 # Check if already registered 

179 component_factory = cls._get_component_factory() 

180 existing = component_factory.get(component_class.name) 

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

182 raise ValueError( 

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

184 f" as {existing}" 

185 ) 

186 

187 component_factory.add_component_class(component_class) 

188 

189 @staticmethod 

190 def _infer_value_type( 

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

192 ) -> str | None: 

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

194 

195 Parameters: 

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

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

198 or :class:`list`. 

199 

200 Returns: 

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

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

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

204 """ 

205 if isinstance(value, list): 

206 if not value: 

207 return None 

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

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

210 return "DATE" 

211 # Check if ALL items are time 

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

213 return "TIME" 

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

215 return None 

216 if is_date(value): 

217 return "DATE" 

218 if isinstance(value, time): 

219 return "TIME" 

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

221 return None 

222 

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

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

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

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

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

228 self.errors: list[ 

229 tuple[str | None, str] 

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

231 # parsing a property, contains error strings 

232 

233 def __bool__(self) -> bool: 

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

235 return True 

236 

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

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

239 return super().__getitem__(key) 

240 

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

242 """Get property value with default.""" 

243 try: 

244 return self[key] 

245 except KeyError: 

246 return default 

247 

248 def is_empty(self) -> bool: 

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

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

251 

252 ############################# 

253 # handling of property values 

254 

255 @classmethod 

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

257 """Encode values to icalendar property values. 

258 

259 :param name: Name of the property. 

260 :type name: string 

261 

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

263 any of the icalendar's own property types. 

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

265 

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

267 available, if encode is set to True. 

268 :type parameters: Dictionary 

269 

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

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

272 or False, if not. 

273 :type encode: Boolean 

274 

275 :returns: icalendar property value 

276 """ 

277 if not encode: 

278 return value 

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

280 # Don't encode already encoded values. 

281 obj = value 

282 else: 

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

284 value_param = None 

285 if parameters and "VALUE" in parameters: 

286 value_param = parameters["VALUE"] 

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

288 inferred = cls._infer_value_type(value) 

289 if inferred: 

290 value_param = inferred 

291 # Auto-set the VALUE parameter 

292 if parameters is None: 

293 parameters = {} 

294 if "VALUE" not in parameters: 

295 parameters["VALUE"] = inferred 

296 

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

298 obj = klass(value) 

299 if parameters: 

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

301 obj.params = Parameters() 

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

303 if item is None: 

304 if key in obj.params: 

305 del obj.params[key] 

306 else: 

307 obj.params[key] = item 

308 return obj 

309 

310 def add( 

311 self, 

312 name: str, 

313 value, 

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

315 encode: bool = True, 

316 ) -> None: 

317 """Add a property to this component. 

318 

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

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

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

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

323 RFC requires. 

324 

325 Parameters: 

326 name: Name of the property. 

327 value: 

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

329 icalendar's own property types. 

330 parameters: 

331 Property parameter dictionary for the value. Only consulted 

332 when ``encode`` is ``True``. 

333 encode: 

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

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

336 store the value as-is. 

337 

338 Returns: 

339 ``None`` 

340 

341 Example: 

342 

343 >>> from icalendar import Event 

344 >>> event = Event() 

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

346 >>> event["summary"] 

347 vText(b'Team sync') 

348 

349 """ 

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

351 "dtstamp", 

352 "created", 

353 "last-modified", 

354 ): 

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

356 value = tzp.localize_utc(value) 

357 

358 # encode value 

359 if ( 

360 encode 

361 and isinstance(value, list) 

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

363 ): 

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

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

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

367 else: 

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

369 

370 # set value 

371 if name in self: 

372 # If property already exists, append it. 

373 oldval = self[name] 

374 if isinstance(oldval, list): 

375 if isinstance(value, list): 

376 value = oldval + value 

377 else: 

378 oldval.append(value) 

379 value = oldval 

380 else: 

381 value = [oldval, value] 

382 self[name] = value 

383 

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

385 """Internal for decoding property values.""" 

386 

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

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

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

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

391 if hasattr(value, "ical_value"): 

392 return value.ical_value 

393 if isinstance(value, vDDDLists): 

394 # TODO: Workaround unfinished decoding 

395 return value 

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

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

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

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

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

401 decoded = decoded.encode(DEFAULT_ENCODING) 

402 return decoded 

403 

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

405 """Returns decoded value of property. 

406 

407 A component maps keys to icalendar property value types. 

408 This function returns values compatible to native Python types. 

409 """ 

410 if name in self: 

411 value = self[name] 

412 if isinstance(value, list): 

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

414 return self._decode(name, value) 

415 if default is _marker: 

416 raise KeyError(name) 

417 return default 

418 

419 ######################################################################## 

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

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

422 

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

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

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

426 if decode: 

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

428 return vals 

429 

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

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

432 to that. 

433 """ 

434 if encode: 

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

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

437 

438 ######################### 

439 # Handling of components 

440 

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

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

443 self.subcomponents.append(component) 

444 

445 def _walk( 

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

447 ) -> list[Component]: 

448 """Walk to given component.""" 

449 result = [] 

450 stack = [self] 

451 while stack: 

452 component = stack.pop() 

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

454 result.append(component) 

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

456 return result 

457 

458 def walk( 

459 self, 

460 name: str | None = None, 

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

462 ) -> list[Component]: 

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

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

465 

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

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

468 and returns True/False. 

469 :returns: A list of components that match. 

470 :rtype: list[Component] 

471 """ 

472 if name is not None: 

473 name = name.upper() 

474 return self._walk(name, select) 

475 

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

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

478 

479 Parameters: 

480 uid: The UID of the component. 

481 

482 Returns: 

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

484 """ 

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

486 

487 def _validate_name(self): 

488 """Make sure the component has a name. 

489 

490 Raises: 

491 ValueError: If this component does not have a name. 

492 """ 

493 if self.name is None: 

494 raise ValueError("This component needs a name for serialization.", self) 

495 

496 ##################### 

497 # Generation 

498 

499 def property_items( 

500 self, 

501 recursive: bool = True, 

502 sorted: bool = True, 

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

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

505 

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

507 """ 

508 # Iterative implementation to avoid RecursionError 

509 result = [] 

510 v_text = self.types_factory["text"] 

511 # Stack stores (component, state) 

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

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

514 stack = [(self, False)] 

515 while stack: 

516 comp, is_end = stack.pop() 

517 comp._validate_name() 

518 if is_end: 

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

520 else: 

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

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

523 

524 for name in property_names: 

525 values = comp[name] 

526 if isinstance(values, list): 

527 # normally one property is one line 

528 for value in values: 

529 result.append((name, value)) 

530 else: 

531 result.append((name, values)) 

532 

533 # Push the END marker for this component 

534 stack.append((comp, True)) 

535 # Push subcomponents if recursion is enabled 

536 if recursive: 

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

538 for subcomponent in reversed(comp.subcomponents): 

539 stack.append((subcomponent, False)) 

540 

541 return result 

542 

543 @overload 

544 @classmethod 

545 def from_ical( 

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

547 ) -> Component: ... 

548 

549 @overload 

550 @classmethod 

551 def from_ical( 

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

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

554 

555 @classmethod 

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

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

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

559 

560 @classmethod 

561 def from_ical( 

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

563 ) -> Component | list[Component]: 

564 """Parse iCalendar data into component instances. 

565 

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

567 

568 Parameters: 

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

570 :class:`pathlib.Path`. 

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

572 

573 Returns: 

574 Component or list of components 

575 

576 See Also: 

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

578 """ 

579 if isinstance(st, Path): 

580 st = st.read_bytes() 

581 parser = cls._get_ical_parser(st) 

582 components = parser.parse() 

583 if multiple: 

584 return components 

585 if len(components) > 1: 

586 raise ValueError( 

587 cls._format_error( 

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

589 ) 

590 ) 

591 if len(components) < 1: 

592 raise ValueError( 

593 cls._format_error( 

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

595 ) 

596 ) 

597 return components[0] 

598 

599 @staticmethod 

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

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

602 max_error_length = 100 - 3 

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

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

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

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

607 

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

609 """Returns property as content line.""" 

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

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

612 

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

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

615 contentlines = Contentlines() 

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

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

618 contentlines.append(cl) 

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

620 return contentlines 

621 

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

623 """ 

624 :param sorted: Whether parameters and properties should be 

625 lexicographically sorted. 

626 """ 

627 

628 content_lines = self.content_lines(sorted=sorted) 

629 return content_lines.to_ical() 

630 

631 def __repr__(self) -> str: 

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

633 

634 Implemented iteratively rather than recursively so that calendars 

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

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

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

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

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

640 """ 

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

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

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

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

645 out: list[str] = [] 

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

647 while stack: 

648 frame = stack.pop() 

649 kind = frame[0] 

650 if kind == "comma": 

651 out.append(", ") 

652 elif kind == "close": 

653 out.append(")") 

654 else: # "open" 

655 node = frame[1] 

656 if isinstance(node, Component): 

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

658 subs = node.subcomponents 

659 if subs: 

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

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

662 # (the first popped comma serves as the separator 

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

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

665 for sub in reversed(subs): 

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

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

668 else: 

669 out.append(")") 

670 else: 

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

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

673 out.append(str(node)) 

674 return "".join(out) 

675 

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

677 if not isinstance(other, Component): 

678 return NotImplemented 

679 

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

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

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

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

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

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

686 # stack instead of recursing. 

687 # 

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

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

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

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

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

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

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

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

696 # candidates are interchangeable.) 

697 stack = [_ComponentEqFrame(self, other)] 

698 child_result = None 

699 while stack: 

700 frame = stack[-1] 

701 if frame.unmatched is None: 

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

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

704 ): 

705 stack.pop() 

706 child_result = False 

707 continue 

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

709 elif child_result is not None: 

710 if child_result: 

711 del frame.unmatched[frame.candidate_index] 

712 frame.a_index += 1 

713 frame.candidate_index = 0 

714 else: 

715 frame.candidate_index += 1 

716 child_result = None 

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

718 stack.pop() 

719 child_result = True 

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

721 stack.pop() 

722 child_result = False 

723 else: 

724 stack.append( 

725 _ComponentEqFrame( 

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

727 frame.unmatched[frame.candidate_index], 

728 ) 

729 ) 

730 return child_result 

731 

732 DTSTAMP = single_utc_property( 

733 "DTSTAMP", 

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

735 

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

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

738 

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

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

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

742 

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

744 

745 Example: 

746 .. code-block:: pycon 

747 

748 >>> from datetime import timezone, datetime 

749 >>> from icalendar import Event 

750 >>> event = Event() 

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

752 >>> event.DTSTAMP 

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

754 

755 See also: 

756 :attr:`CREATED`, :attr:`LAST_MODIFIED`, 

757 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

758 """, 

759 ) 

760 

761 @property 

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

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

764 

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

766 """ 

767 return self.DTSTAMP 

768 

769 @stamp.setter 

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

771 self.DTSTAMP = value 

772 

773 @stamp.deleter 

774 def stamp(self) -> None: 

775 del self.DTSTAMP 

776 

777 LAST_MODIFIED = single_utc_property( 

778 "LAST-MODIFIED", 

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

780 

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

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

783 

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

785 components. The value is always in UTC. 

786 

787 Example: 

788 .. code-block:: pycon 

789 

790 >>> from datetime import timezone, datetime 

791 >>> from icalendar import Event 

792 >>> event = Event() 

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

794 >>> event.LAST_MODIFIED 

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

796 

797 See also: 

798 :attr:`CREATED`, :attr:`DTSTAMP`, 

799 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

800 """, 

801 ) 

802 

803 @property 

804 def last_modified(self) -> datetime: 

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

806 

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

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

809 """ 

810 return self.LAST_MODIFIED or self.DTSTAMP 

811 

812 @last_modified.setter 

813 def last_modified(self, value): 

814 self.LAST_MODIFIED = value 

815 

816 @last_modified.deleter 

817 def last_modified(self): 

818 del self.LAST_MODIFIED 

819 

820 @property 

821 def created(self) -> datetime: 

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

823 

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

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

826 """ 

827 return self.CREATED or self.DTSTAMP 

828 

829 @created.setter 

830 def created(self, value): 

831 self.CREATED = value 

832 

833 @created.deleter 

834 def created(self): 

835 del self.CREATED 

836 

837 def is_thunderbird(self) -> bool: 

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

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

840 

841 @staticmethod 

842 def _utc_now() -> datetime: 

843 """Return now as UTC value.""" 

844 return datetime.now(timezone.utc) 

845 

846 uid = uid_property 

847 comments = comments_property 

848 links = links_property 

849 related_to = related_to_property 

850 concepts = concepts_property 

851 refids = refids_property 

852 

853 CREATED = single_utc_property( 

854 "CREATED", 

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

856 

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

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

859 :attr:`DTSTAMP`. 

860 

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

862 The value is always in UTC. 

863 

864 Example: 

865 .. code-block:: pycon 

866 

867 >>> from datetime import timezone, datetime 

868 >>> from icalendar import Event 

869 >>> event = Event() 

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

871 >>> event.CREATED 

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

873 

874 See also: 

875 :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`, 

876 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

877 """, 

878 ) 

879 

880 _validate_new = True 

881 

882 @staticmethod 

883 def _validate_start_and_end(start, end): 

884 """This validates start and end. 

885 

886 Raises: 

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

888 """ 

889 if start is None or end is None: 

890 return 

891 if start > end: 

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

893 

894 @classmethod 

895 def new( 

896 cls, 

897 created: date | None = None, 

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

899 concepts: CONCEPTS_TYPE_SETTER = None, 

900 last_modified: date | None = None, 

901 links: LINKS_TYPE_SETTER = None, 

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

903 related_to: RELATED_TO_TYPE_SETTER = None, 

904 stamp: date | None = None, 

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

906 ) -> Self: 

907 """Create a new component. 

908 

909 Parameters: 

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

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

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

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

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

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

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

917 subcomponents: The subcomponents of the component. 

918 

919 Raises: 

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

921 according to :rfc:`5545`. 

922 

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

924 validation. 

925 """ 

926 component = cls() 

927 component.DTSTAMP = stamp 

928 component.created = created 

929 component.last_modified = last_modified 

930 component.comments = comments 

931 component.links = links 

932 component.related_to = related_to 

933 component.concepts = concepts 

934 component.refids = refids 

935 if subcomponents is not None: 

936 component.subcomponents = ( 

937 subcomponents 

938 if isinstance(subcomponents, list) 

939 else list(subcomponents) 

940 ) 

941 return component 

942 

943 def to_jcal(self) -> list: 

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

945 

946 Returns: 

947 jCal object 

948 

949 Raises: 

950 ValueError: If a component does not have a name. 

951 

952 See also :attr:`to_json`. 

953 

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

955 

956 .. code-block:: pycon 

957 

958 >>> from icalendar import Event 

959 >>> from datetime import date 

960 >>> from pprint import pprint 

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

962 >>> pprint(event.to_jcal()) 

963 ['vevent', 

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

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

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

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

968 []] 

969 """ 

970 

971 # Iterative tree walk to avoid RecursionError on deeply nested 

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

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

974 properties = [ 

975 item.to_jcal(key.lower()) 

976 for key, value in comp.sorted_items() 

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

978 ] 

979 comp._validate_name() 

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

981 

982 root_node = make_node(self) 

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

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

985 while stack: 

986 comp, node = stack.pop() 

987 children = node[2] 

988 for subcomponent in comp.subcomponents: 

989 child_node = make_node(subcomponent) 

990 children.append(child_node) 

991 stack.append((subcomponent, child_node)) 

992 return root_node 

993 

994 def to_json(self, indent: int | str | None = None) -> str: 

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

996 

997 Parameters: 

998 indent: If a non-negative integer or string is provided, 

999 then JSON will be pretty-printed with that indent level. 

1000 

1001 Returns: 

1002 JSON string 

1003 

1004 Raises: 

1005 ValueError: If a component does not have a name. 

1006 

1007 See also :attr:`to_jcal`. 

1008 """ 

1009 return json.dumps( 

1010 self.to_jcal(), 

1011 indent=indent, 

1012 sort_keys=True, 

1013 separators=(",", ":") if indent is None else None, 

1014 ) 

1015 

1016 @classmethod 

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

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

1019 

1020 Parameters: 

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

1022 

1023 Raises: 

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

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

1026 

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

1028 

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

1030 

1031 .. code-block:: pycon 

1032 

1033 >>> from icalendar import Component 

1034 >>> jcal = ["vcalendar", 

1035 ... [ 

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

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

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

1039 ... ], 

1040 ... [ 

1041 ... ["vevent", 

1042 ... [ 

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

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

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

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

1047 ... ], 

1048 ... [] 

1049 ... ] 

1050 ... ] 

1051 ... ] 

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

1053 >>> print(calendar.name) 

1054 VCALENDAR 

1055 >>> print(calendar.prodid) 

1056 -//Example Inc.//Example Calendar//EN 

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

1058 >>> print(event.summary) 

1059 Planning meeting 

1060 

1061 """ 

1062 if isinstance(jcal, str): 

1063 jcal = json.loads(jcal) 

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

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

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

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

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

1069 root, root_subcomponents = _node_from_jcal(jcal, cls) 

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

1071 while stack: 

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

1073 for i, subcomponent in enumerate(subcomponents): 

1074 child_prefix = [*prefix, 2, i] 

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

1076 # implementation. This also preserves the error value and 

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

1078 with JCalParsingError.reraise_with_path_added(*child_prefix): 

1079 child, child_subcomponents = _node_from_jcal( 

1080 subcomponent, type(parent) 

1081 ) 

1082 parent.subcomponents.append(child) 

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

1084 return root 

1085 

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

1087 """Copy the component. 

1088 

1089 Parameters: 

1090 recursive: 

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

1092 and all its properties. 

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

1094 

1095 Returns: 

1096 A copy of the component. 

1097 

1098 Examples: 

1099 

1100 Create a shallow copy of a component: 

1101 

1102 .. code-block:: pycon 

1103 

1104 >>> from icalendar import Event 

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

1106 >>> event_copy = event.copy() 

1107 >>> str(event_copy.description) 

1108 'Event to be copied' 

1109 

1110 Shallow copies lose their subcomponents: 

1111 

1112 .. code-block:: pycon 

1113 

1114 >>> from icalendar import Calendar 

1115 >>> calendar = Calendar.example() 

1116 >>> len(calendar.subcomponents) 

1117 3 

1118 >>> calendar_copy = calendar.copy() 

1119 >>> len(calendar_copy.subcomponents) 

1120 0 

1121 

1122 A recursive copy also copies all the subcomponents: 

1123 

1124 .. code-block:: pycon 

1125 

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

1127 >>> len(full_calendar_copy.subcomponents) 

1128 3 

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

1130 True 

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

1132 False 

1133 

1134 """ 

1135 if recursive: 

1136 return deepcopy(self) 

1137 return super().copy() 

1138 

1139 def is_lazy(self) -> bool: 

1140 """This component is fully parsed.""" 

1141 return False 

1142 

1143 def parse(self) -> Self: 

1144 """Return the fully parsed component. 

1145 

1146 For non-lazy components, this returns self. 

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

1148 """ 

1149 return self 

1150 

1151 @overload 

1152 def to_xcal( 

1153 self, 

1154 destination: Element | IO[bytes], 

1155 ) -> None: ... 

1156 

1157 @overload 

1158 def to_xcal( 

1159 self, destination: Element | IO[bytes], *, indent: int | str | None = None 

1160 ) -> None: ... 

1161 

1162 @overload 

1163 def to_xcal(self, *, indent: int | str | None = None) -> bytes: ... 

1164 

1165 def to_xcal( 

1166 self, 

1167 destination: Element | IO[bytes] | None = None, 

1168 *, 

1169 indent: int | str | None = None, 

1170 ) -> bytes | None: 

1171 """The xCal representation of this component according to :rfc:`6321`. 

1172 

1173 Parameters: 

1174 destination: The iCalendar stream or a file. 

1175 indent: The indentation for pretty printing the XML. 

1176 

1177 Returns: 

1178 - ``None`` if an argument is passed. 

1179 - :class:`bytes` if called with no arguments. 

1180 

1181 Example: 

1182 This example shows how to convert a new calendar to XML: 

1183 

1184 .. code-block:: pycon 

1185 

1186 >>> from icalendar import Calendar 

1187 >>> cal = Calendar.new() 

1188 >>> print(cal.to_xcal(indent=2).decode()) 

1189 <?xml version="1.0" encoding="UTF-8"?> 

1190 <icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> 

1191 <vcalendar> 

1192 <properties> 

1193 <version> 

1194 <text>2.0</text> 

1195 </version> 

1196 <prodid> 

1197 <text>-//collective//icalendar//7.0.0//EN</text> 

1198 </prodid> 

1199 <uid> 

1200 <text>d755cef5-2311-46ed-a0e1-6733c9e15c63</text> 

1201 </uid> 

1202 </properties> 

1203 </vcalendar> 

1204 </icalendar> 

1205 """ 

1206 from io import BytesIO 

1207 

1208 input_is_element = isinstance(destination, Element) 

1209 if input_is_element: 

1210 stream = destination 

1211 else: 

1212 stream = Element("icalendar", xmlns="urn:ietf:params:xml:ns:icalendar-2.0") 

1213 if destination is not None and not hasattr(destination, "write"): 

1214 raise TypeError( 

1215 "Expected an XML Element or a file-like " 

1216 f"object for destination, got {destination!r}" 

1217 ) 

1218 self.to_xcal_element(stream) 

1219 if indent is not None: 

1220 if isinstance(indent, int): 

1221 indent = " " * indent 

1222 elif not isinstance(indent, str): 

1223 raise TypeError( 

1224 f"Expected an integer or string for indent, got {type(indent)}" 

1225 ) 

1226 if input_is_element: 

1227 return None 

1228 return_bytes = destination is None 

1229 file = BytesIO() if return_bytes else destination 

1230 file.write(b'<?xml version="1.0" encoding="UTF-8"?>') 

1231 if indent is not None: 

1232 file.write(b"\n") 

1233 indent_xml(stream, space=indent) 

1234 ElementTree(stream).write(file, "utf-8", xml_declaration=False, method="xml") 

1235 if indent is not None: 

1236 file.write(b"\n") 

1237 if return_bytes: 

1238 return file.getvalue() 

1239 return None 

1240 

1241 def to_xcal_element(self, element: Element, /) -> None: 

1242 """Add the xCal representation of this component according to :rfc:`6321`.""" 

1243 self._validate_name() 

1244 e_component = SubElement(element, self.name.lower()) 

1245 if len(self) > 0: 

1246 e_properties = SubElement(e_component, "properties") 

1247 for key, prop_list in self.sorted_items(): 

1248 for prop in prop_list if isinstance(prop_list, list) else [prop_list]: 

1249 prop: VPROPERTY 

1250 e_property = SubElement(e_properties, key.lower()) 

1251 prop.to_xcal(e_property) 

1252 if self.subcomponents: 

1253 e_components = SubElement(e_component, "components") 

1254 for subcomponent in self.subcomponents: 

1255 subcomponent.to_xcal(e_components) 

1256 

1257 @classmethod 

1258 def from_xcal(cls, xcal: Element | bytes | Path | BinaryIO) -> list[Self]: 

1259 """Parse xCal according to :rfc:`6321`. 

1260 

1261 Parameters: 

1262 xcal: The xCal element or data to parse. 

1263 

1264 Returns: 

1265 The parsed component. 

1266 

1267 Raises: 

1268 ~icalendar.error.XCalParsingError: If the provided XML does not conform with :rfc:`6321`. 

1269 TypeError: If ``xcal`` is not one of the valid types. 

1270 xml.etree.ElementTree.ParseError: If the provided data is not valid XML. 

1271 

1272 Example: 

1273 

1274 Parse an xCal object from a bytes object in XML format. 

1275 

1276 >>> from icalendar import Calendar 

1277 >>> xml = b'''<?xml version="1.0" encoding="utf-8"?> 

1278 ... <icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> 

1279 ... <vcalendar> 

1280 ... <properties> 

1281 ... <prodid> 

1282 ... <text>-//Example Inc.//Example Client//EN</text> 

1283 ... </prodid> 

1284 ... <version> 

1285 ... <text>2.0</text> 

1286 ... </version> 

1287 ... </properties> 

1288 ... </vcalendar> 

1289 ... </icalendar> 

1290 ... ''' 

1291 >>> cal = Calendar.from_xcal(xml)[0] 

1292 >>> cal.prodid == '-//Example Inc.//Example Client//EN' 

1293 True 

1294 

1295 """ 

1296 from icalendar.parser.xcal.component import XCalComponentParser 

1297 

1298 if isinstance(xcal, Path): 

1299 xcal = xcal.open("rb") 

1300 elif isinstance(xcal, bytes): 

1301 xcal = BytesIO(xcal) 

1302 if hasattr(xcal, "read"): 

1303 element = config.parse_xml(xcal).getroot() 

1304 elif isinstance(xcal, Element): 

1305 element = xcal 

1306 else: 

1307 raise TypeError( 

1308 f"Expected an XML Element, bytes, Path, or file object. Got {xcal}" 

1309 ) 

1310 parser = XCalComponentParser( 

1311 element, cls._get_component_factory(), cls.types_factory 

1312 ) 

1313 components = [] 

1314 while not parser.is_finished(): 

1315 component = parser.parse_component() 

1316 components.append(component) 

1317 return components 

1318 

1319 

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

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

1322 

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

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

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

1326 

1327 Parameters: 

1328 jcal: The jCal list for one component. 

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

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

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

1332 

1333 Returns: 

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

1335 returned for the caller to walk iteratively. 

1336 

1337 Raises: 

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

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

1340 """ 

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

1342 raise JCalParsingError( 

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

1344 ) 

1345 name, properties, subcomponents = jcal 

1346 if not isinstance(name, str): 

1347 raise JCalParsingError( 

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

1349 ) 

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

1351 # delegate to correct component class 

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

1353 else: 

1354 component_cls = starting_cls 

1355 component = component_cls() 

1356 if not isinstance(properties, list): 

1357 raise JCalParsingError( 

1358 "The properties must be a list.", 

1359 component_cls, 

1360 path=1, 

1361 value=properties, 

1362 ) 

1363 for i, prop in enumerate(properties): 

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

1365 prop_name = prop[0] 

1366 prop_value = prop[2] 

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

1368 prop_name, prop_value 

1369 ) 

1370 with JCalParsingError.reraise_with_path_added(1, i): 

1371 v_prop = prop_cls.from_jcal(prop) 

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

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

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

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

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

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

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

1379 # (RFC 7265, section 5.2). 

1380 default_type = component_cls.types_factory.default_value_type(prop_name) 

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

1382 "unknown", 

1383 default_type, 

1384 ): 

1385 v_prop.VALUE = prop_value.upper() 

1386 elif "VALUE" in v_prop.params: 

1387 del v_prop.VALUE 

1388 component.add(prop_name, v_prop) 

1389 if not isinstance(subcomponents, list): 

1390 raise JCalParsingError( 

1391 "The subcomponents must be a list.", 

1392 component_cls, 

1393 2, 

1394 value=subcomponents, 

1395 ) 

1396 return component, subcomponents 

1397 

1398 

1399__all__ = ["Component"]