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

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

506 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 XML_property, 

21 comments_property, 

22 concepts_property, 

23 links_property, 

24 refids_property, 

25 related_to_property, 

26 single_utc_property, 

27 uid_property, 

28) 

29from icalendar.cal.component_factory import ComponentFactory 

30from icalendar.caselessdict import CaselessDict 

31from icalendar.error import InvalidCalendar, JCalParsingError 

32from icalendar.parser import ( 

33 Contentline, 

34 Contentlines, 

35 Parameters, 

36 q_join, 

37 q_split, 

38) 

39from icalendar.parser.ical.component import ComponentIcalParser 

40from icalendar.parser_tools import DEFAULT_ENCODING 

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

42from icalendar.prop.xml import _preserve_indentation_of_xcal_extension_properties 

43from icalendar.timezone import tzp 

44from icalendar.tools import is_date 

45 

46if TYPE_CHECKING: 

47 from collections.abc import Iterable 

48 

49 from icalendar.compatibility import Self 

50 

51_marker = [] 

52 

53 

54@dataclass 

55class _ComponentEqFrame: 

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

57 

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

59 """ 

60 

61 #: the two components being compared 

62 a: Component 

63 b: Component 

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

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

66 unmatched: list | None = None 

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

68 a_index: int = 0 

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

70 candidate_index: int = 0 

71 

72 

73class Component(CaselessDict): 

74 """Base class for calendar components. 

75 

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

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

78 directly, but rather one of the subclasses. 

79 """ 

80 

81 name: ClassVar[str | None] = None 

82 """The name of the component. 

83 

84 This is defined in each component class. 

85 

86 Example: 

87 

88 .. code-block:: pycon 

89 

90 >>> from icalendar import Calendar 

91 >>> cal = Calendar.new() 

92 >>> cal.name 

93 'VCALENDAR' 

94 

95 """ 

96 

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

98 """These properties are required.""" 

99 

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

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

102 

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

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

105 

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

107 """These properties are mutually exclusive.""" 

108 

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

110 """These properties are inclusive. 

111 

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

113 second one must also occur. 

114 

115 Example: 

116 

117 .. code-block:: python 

118 

119 ('duration', 'repeat') 

120 """ 

121 

122 ignore_exceptions: ClassVar[bool] = False 

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

124 

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

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

127 """ 

128 

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

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

131 

132 _components_factory: ClassVar[ComponentFactory | None] = None 

133 

134 subcomponents: list[Component] 

135 """All subcomponents of this component.""" 

136 

137 errors: list[tuple[str | None, str]] 

138 """The errors that occurred while parsing this component. 

139 

140 Each entry is a ``(name, message)`` tuple. 

141 ``name`` is the name of the property whose value could not be parsed, or 

142 ``None`` if a whole content line was unparseable and therefore skipped. 

143 ``message`` is the description of the problem. 

144 

145 Errors are recorded instead of raised only when the component sets 

146 :attr:`ignore_exceptions` to ``True``, as :class:`~icalendar.cal.event.Event` 

147 does, or when the property is an unregistered ``X-`` property. 

148 A property with an unparseable value is kept as a 

149 :class:`~icalendar.prop.broken.vBroken` value, so :meth:`to_ical` writes it 

150 back unchanged. 

151 

152 Errors are recorded per component: this list stays empty when only a 

153 subcomponent is affected. Use :meth:`walk` to collect all of them. 

154 

155 Example: 

156 

157 .. code-block:: pycon 

158 

159 >>> from icalendar import Calendar, Event 

160 >>> event = Calendar.example("broken_dtstart").events[0] 

161 >>> event.errors 

162 [('DTSTART', "Expected datetime, date, or time. Got: 'INVALID-DATE'")] 

163 

164 A content line that cannot be split into a name, parameters, and a value 

165 is recorded without a property name: 

166 

167 .. code-block:: pycon 

168 

169 >>> Event.example("issue_104_mark_events_broken").errors 

170 [(None, "Content line could not be parsed into parts: 'X': Invalid content line")] 

171 

172 See also: 

173 :attr:`ignore_exceptions`, :meth:`walk`, :doc:`/how-to/parse-errors` 

174 """ 

175 

176 @classmethod 

177 def _get_component_factory(cls) -> ComponentFactory: 

178 """Get the component factory.""" 

179 if cls._components_factory is None: 

180 cls._components_factory = ComponentFactory() 

181 return cls._components_factory 

182 

183 @classmethod 

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

185 """Return a component with this name. 

186 

187 Parameters: 

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

189 """ 

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

191 

192 @classmethod 

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

194 """Register a custom component class. 

195 

196 Parameters: 

197 component_class: Component subclass to register. 

198 Must have a ``name`` attribute. 

199 

200 Raises: 

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

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

203 

204 Examples: 

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

206 

207 .. code-block:: pycon 

208 

209 >>> from icalendar import Component 

210 >>> class XExample(Component): 

211 ... name = "X-EXAMPLE" 

212 ... def custom_method(self): 

213 ... return "custom" 

214 >>> Component.register(XExample) 

215 """ 

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

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

218 

219 # Check if already registered 

220 component_factory = cls._get_component_factory() 

221 existing = component_factory.get(component_class.name) 

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

223 raise ValueError( 

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

225 f" as {existing}" 

226 ) 

227 

228 component_factory.add_component_class(component_class) 

229 

230 @staticmethod 

231 def _infer_value_type( 

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

233 ) -> str | None: 

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

235 

236 Parameters: 

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

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

239 or :class:`list`. 

240 

241 Returns: 

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

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

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

245 """ 

246 if isinstance(value, list): 

247 if not value: 

248 return None 

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

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

251 return "DATE" 

252 # Check if ALL items are time 

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

254 return "TIME" 

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

256 return None 

257 if is_date(value): 

258 return "DATE" 

259 if isinstance(value, time): 

260 return "TIME" 

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

262 return None 

263 

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

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

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

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

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

269 self.errors: list[tuple[str | None, str]] = [] 

270 

271 def __bool__(self) -> bool: 

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

273 return True 

274 

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

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

277 return super().__getitem__(key) 

278 

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

280 """Get property value with default.""" 

281 try: 

282 return self[key] 

283 except KeyError: 

284 return default 

285 

286 def is_empty(self) -> bool: 

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

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

289 

290 ############################# 

291 # handling of property values 

292 

293 @classmethod 

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

295 """Encode values to icalendar property values. 

296 

297 Parameters: 

298 name (str): Required. Name of the property. 

299 value (obj): Required. Value of the property. Either a native Python type or 

300 any of icalendar's own property types. 

301 encode (bool): ``True`` if the value should be encoded to one of 

302 icalendar's own property types (falls back to ``vText``), 

303 or ``False`` if not. 

304 parameters (dict or None): Property parameter dictionary for the value. 

305 Only used if ``encode`` is set to ``True``. 

306 

307 Returns: 

308 The encoded icalendar property value. 

309 """ 

310 if not encode: 

311 return value 

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

313 # Don't encode already encoded values. 

314 obj = value 

315 else: 

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

317 value_param = None 

318 if parameters and "VALUE" in parameters: 

319 value_param = parameters["VALUE"] 

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

321 inferred = cls._infer_value_type(value) 

322 if inferred: 

323 value_param = inferred 

324 # Auto-set the VALUE parameter 

325 if parameters is None: 

326 parameters = {} 

327 if "VALUE" not in parameters: 

328 parameters["VALUE"] = inferred 

329 

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

331 obj = klass(value) 

332 if parameters: 

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

334 obj.params = Parameters() 

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

336 if item is None: 

337 if key in obj.params: 

338 del obj.params[key] 

339 else: 

340 obj.params[key] = item 

341 return obj 

342 

343 def add( 

344 self, 

345 name: str, 

346 value, 

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

348 encode: bool = True, 

349 ) -> None: 

350 """Add a property to this component. 

351 

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

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

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

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

356 RFC requires. 

357 

358 Parameters: 

359 name: Name of the property. 

360 value: 

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

362 icalendar's own property types. 

363 parameters: 

364 Property parameter dictionary for the value. Only consulted 

365 when ``encode`` is ``True``. 

366 encode: 

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

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

369 store the value as-is. 

370 

371 Returns: 

372 ``None`` 

373 

374 Example: 

375 

376 >>> from icalendar import Event 

377 >>> event = Event() 

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

379 >>> event["summary"] 

380 vText(b'Team sync') 

381 

382 """ 

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

384 "dtstamp", 

385 "created", 

386 "last-modified", 

387 ): 

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

389 value = tzp.localize_utc(value) 

390 

391 # encode value 

392 if ( 

393 encode 

394 and isinstance(value, list) 

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

396 ): 

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

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

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

400 else: 

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

402 

403 # set value 

404 if name in self: 

405 # If property already exists, append it. 

406 oldval = self[name] 

407 if isinstance(oldval, list): 

408 if isinstance(value, list): 

409 value = oldval + value 

410 else: 

411 oldval.append(value) 

412 value = oldval 

413 else: 

414 value = [oldval, value] 

415 self[name] = value 

416 

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

418 """Internal for decoding property values.""" 

419 

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

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

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

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

424 if hasattr(value, "ical_value"): 

425 return value.ical_value 

426 if isinstance(value, vDDDLists): 

427 # TODO: Workaround unfinished decoding 

428 return value 

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

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

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

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

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

434 decoded = decoded.encode(DEFAULT_ENCODING) 

435 return decoded 

436 

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

438 """Returns decoded value of property. 

439 

440 A component maps keys to icalendar property value types. 

441 This function returns values compatible to native Python types. 

442 """ 

443 if name in self: 

444 value = self[name] 

445 if isinstance(value, list): 

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

447 return self._decode(name, value) 

448 if default is _marker: 

449 raise KeyError(name) 

450 return default 

451 

452 ######################################################################## 

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

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

455 

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

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

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

459 if decode: 

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

461 return vals 

462 

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

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

465 to that. 

466 """ 

467 if encode: 

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

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

470 

471 ######################### 

472 # Handling of components 

473 

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

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

476 self.subcomponents.append(component) 

477 

478 def _walk( 

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

480 ) -> list[Component]: 

481 """Walk to given component.""" 

482 result = [] 

483 stack = [self] 

484 while stack: 

485 component = stack.pop() 

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

487 result.append(component) 

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

489 return result 

490 

491 def walk( 

492 self, 

493 name: str | None = None, 

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

495 ) -> list[Component]: 

496 """Return this component and its subcomponents that match the filters. 

497 

498 A component is kept only if it matches both ``name`` and ``select``. 

499 The name match is case-insensitive, so ``"vevent"`` finds ``VEVENT`` 

500 components. 

501 

502 Parameters: 

503 name: Component name to keep, such as ``"VEVENT"``; ``None`` keeps 

504 every name. 

505 select: Function that takes a component and returns ``True`` to 

506 keep it. By default, every component is kept. 

507 

508 Returns: 

509 list[Component]: The matching components, starting with this one 

510 if it matches. 

511 """ 

512 if name is not None: 

513 name = name.upper() 

514 return self._walk(name, select) 

515 

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

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

518 

519 Parameters: 

520 uid: The UID of the component. 

521 

522 Returns: 

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

524 """ 

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

526 

527 def _validate_name(self): 

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

529 

530 Raises: 

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

532 """ 

533 if self.name is None: 

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

535 

536 ##################### 

537 # Generation 

538 

539 def property_items( 

540 self, 

541 recursive: bool = True, 

542 sorted: bool = True, 

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

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

545 

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

547 """ 

548 # Iterative implementation to avoid RecursionError 

549 result = [] 

550 v_text = self.types_factory["text"] 

551 # Stack stores (component, state) 

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

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

554 stack = [(self, False)] 

555 while stack: 

556 comp, is_end = stack.pop() 

557 comp._validate_name() 

558 if is_end: 

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

560 else: 

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

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

563 

564 for name in property_names: 

565 values = comp[name] 

566 if isinstance(values, list): 

567 # normally one property is one line 

568 for value in values: 

569 result.append((name, value)) 

570 else: 

571 result.append((name, values)) 

572 

573 # Push the END marker for this component 

574 stack.append((comp, True)) 

575 # Push subcomponents if recursion is enabled 

576 if recursive: 

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

578 for subcomponent in reversed(comp.subcomponents): 

579 stack.append((subcomponent, False)) 

580 

581 return result 

582 

583 @overload 

584 @classmethod 

585 def from_ical( 

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

587 ) -> Component: ... 

588 

589 @overload 

590 @classmethod 

591 def from_ical( 

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

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

594 

595 @classmethod 

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

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

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

599 

600 @classmethod 

601 def from_ical( 

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

603 ) -> Component | list[Component]: 

604 """Parse iCalendar data into component instances. 

605 

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

607 

608 Parameters: 

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

610 :class:`pathlib.Path`. 

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

612 

613 Returns: 

614 Component or list of components 

615 

616 See Also: 

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

618 """ 

619 if isinstance(st, Path): 

620 st = st.read_bytes() 

621 parser = cls._get_ical_parser(st) 

622 components = parser.parse() 

623 if multiple: 

624 return components 

625 if len(components) > 1: 

626 raise ValueError( 

627 cls._format_error( 

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

629 ) 

630 ) 

631 if len(components) < 1: 

632 raise ValueError( 

633 cls._format_error( 

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

635 ) 

636 ) 

637 return components[0] 

638 

639 @staticmethod 

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

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

642 max_error_length = 100 - 3 

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

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

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

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

647 

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

649 """Returns property as content line.""" 

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

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

652 

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

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

655 contentlines = Contentlines() 

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

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

658 contentlines.append(cl) 

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

660 return contentlines 

661 

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

663 """ 

664 :param sorted: Whether parameters and properties should be 

665 lexicographically sorted. 

666 """ 

667 

668 content_lines = self.content_lines(sorted=sorted) 

669 return content_lines.to_ical() 

670 

671 def __repr__(self) -> str: 

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

673 

674 Implemented iteratively rather than recursively so that calendars 

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

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

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

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

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

680 """ 

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

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

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

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

685 out: list[str] = [] 

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

687 while stack: 

688 frame = stack.pop() 

689 kind = frame[0] 

690 if kind == "comma": 

691 out.append(", ") 

692 elif kind == "close": 

693 out.append(")") 

694 else: # "open" 

695 node = frame[1] 

696 if isinstance(node, Component): 

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

698 subs = node.subcomponents 

699 if subs: 

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

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

702 # (the first popped comma serves as the separator 

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

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

705 for sub in reversed(subs): 

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

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

708 else: 

709 out.append(")") 

710 else: 

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

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

713 out.append(str(node)) 

714 return "".join(out) 

715 

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

717 if not isinstance(other, Component): 

718 return NotImplemented 

719 

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

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

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

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

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

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

726 # stack instead of recursing. 

727 # 

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

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

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

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

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

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

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

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

736 # candidates are interchangeable.) 

737 stack = [_ComponentEqFrame(self, other)] 

738 child_result = None 

739 while stack: 

740 frame = stack[-1] 

741 if frame.unmatched is None: 

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

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

744 ): 

745 stack.pop() 

746 child_result = False 

747 continue 

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

749 elif child_result is not None: 

750 if child_result: 

751 del frame.unmatched[frame.candidate_index] 

752 frame.a_index += 1 

753 frame.candidate_index = 0 

754 else: 

755 frame.candidate_index += 1 

756 child_result = None 

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

758 stack.pop() 

759 child_result = True 

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

761 stack.pop() 

762 child_result = False 

763 else: 

764 stack.append( 

765 _ComponentEqFrame( 

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

767 frame.unmatched[frame.candidate_index], 

768 ) 

769 ) 

770 return child_result 

771 

772 DTSTAMP = single_utc_property( 

773 "DTSTAMP", 

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

775 

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

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

778 

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

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

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

782 

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

784 

785 Example: 

786 .. code-block:: pycon 

787 

788 >>> from datetime import timezone, datetime 

789 >>> from icalendar import Event 

790 >>> event = Event() 

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

792 >>> event.DTSTAMP 

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

794 

795 See also: 

796 :attr:`CREATED`, :attr:`LAST_MODIFIED`, 

797 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

798 """, 

799 ) 

800 

801 @property 

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

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

804 

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

806 """ 

807 return self.DTSTAMP 

808 

809 @stamp.setter 

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

811 self.DTSTAMP = value 

812 

813 @stamp.deleter 

814 def stamp(self) -> None: 

815 del self.DTSTAMP 

816 

817 LAST_MODIFIED = single_utc_property( 

818 "LAST-MODIFIED", 

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

820 

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

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

823 

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

825 components. The value is always in UTC. 

826 

827 Example: 

828 .. code-block:: pycon 

829 

830 >>> from datetime import timezone, datetime 

831 >>> from icalendar import Event 

832 >>> event = Event() 

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

834 >>> event.LAST_MODIFIED 

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

836 

837 See also: 

838 :attr:`CREATED`, :attr:`DTSTAMP`, 

839 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

840 """, 

841 ) 

842 

843 @property 

844 def last_modified(self) -> datetime: 

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

846 

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

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

849 """ 

850 return self.LAST_MODIFIED or self.DTSTAMP 

851 

852 @last_modified.setter 

853 def last_modified(self, value): 

854 self.LAST_MODIFIED = value 

855 

856 @last_modified.deleter 

857 def last_modified(self): 

858 del self.LAST_MODIFIED 

859 

860 @property 

861 def created(self) -> datetime: 

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

863 

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

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

866 """ 

867 return self.CREATED or self.DTSTAMP 

868 

869 @created.setter 

870 def created(self, value): 

871 self.CREATED = value 

872 

873 @created.deleter 

874 def created(self): 

875 del self.CREATED 

876 

877 def is_thunderbird(self) -> bool: 

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

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

880 

881 @staticmethod 

882 def _utc_now() -> datetime: 

883 """Return now as UTC value.""" 

884 return datetime.now(timezone.utc) 

885 

886 uid = uid_property 

887 comments = comments_property 

888 links = links_property 

889 related_to = related_to_property 

890 concepts = concepts_property 

891 refids = refids_property 

892 XML = XML_property 

893 

894 CREATED = single_utc_property( 

895 "CREATED", 

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

897 

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

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

900 :attr:`DTSTAMP`. 

901 

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

903 The value is always in UTC. 

904 

905 Example: 

906 .. code-block:: pycon 

907 

908 >>> from datetime import timezone, datetime 

909 >>> from icalendar import Event 

910 >>> event = Event() 

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

912 >>> event.CREATED 

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

914 

915 See also: 

916 :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`, 

917 :attr:`created`, :attr:`stamp`, :attr:`last_modified` 

918 """, 

919 ) 

920 

921 _validate_new = True 

922 

923 @staticmethod 

924 def _validate_start_and_end(start, end): 

925 """This validates start and end. 

926 

927 Raises: 

928 ~icalendar.error.InvalidCalendar: If the information is not valid 

929 """ 

930 if start is None or end is None: 

931 return 

932 if start > end: 

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

934 

935 @classmethod 

936 def new( 

937 cls, 

938 created: date | None = None, 

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

940 concepts: CONCEPTS_TYPE_SETTER = None, 

941 last_modified: date | None = None, 

942 links: LINKS_TYPE_SETTER = None, 

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

944 related_to: RELATED_TO_TYPE_SETTER = None, 

945 stamp: date | None = None, 

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

947 ) -> Self: 

948 """Create a new component. 

949 

950 Parameters: 

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

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

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

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

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

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

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

958 subcomponents: The subcomponents of the component. 

959 

960 Raises: 

961 ~icalendar.error.InvalidCalendar: If the content is not valid 

962 according to :rfc:`5545`. 

963 

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

965 validation. 

966 """ 

967 component = cls() 

968 component.DTSTAMP = stamp 

969 component.created = created 

970 component.last_modified = last_modified 

971 component.comments = comments 

972 component.links = links 

973 component.related_to = related_to 

974 component.concepts = concepts 

975 component.refids = refids 

976 if subcomponents is not None: 

977 component.subcomponents = ( 

978 subcomponents 

979 if isinstance(subcomponents, list) 

980 else list(subcomponents) 

981 ) 

982 return component 

983 

984 def to_jcal(self) -> list: 

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

986 

987 Returns: 

988 jCal object 

989 

990 Raises: 

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

992 

993 See also :attr:`to_json`. 

994 

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

996 

997 .. code-block:: pycon 

998 

999 >>> from icalendar import Event 

1000 >>> from datetime import date 

1001 >>> from pprint import pprint 

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

1003 >>> pprint(event.to_jcal()) 

1004 ['vevent', 

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

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

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

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

1009 []] 

1010 """ 

1011 

1012 # Iterative tree walk to avoid RecursionError on deeply nested 

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

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

1015 properties = [ 

1016 item.to_jcal(key.lower()) 

1017 for key, value in comp.sorted_items() 

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

1019 ] 

1020 comp._validate_name() 

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

1022 

1023 root_node = make_node(self) 

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

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

1026 while stack: 

1027 comp, node = stack.pop() 

1028 children = node[2] 

1029 for subcomponent in comp.subcomponents: 

1030 child_node = make_node(subcomponent) 

1031 children.append(child_node) 

1032 stack.append((subcomponent, child_node)) 

1033 return root_node 

1034 

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

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

1037 

1038 Parameters: 

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

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

1041 

1042 Returns: 

1043 JSON string 

1044 

1045 Raises: 

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

1047 

1048 See also :attr:`to_jcal`. 

1049 """ 

1050 return json.dumps( 

1051 self.to_jcal(), 

1052 indent=indent, 

1053 sort_keys=True, 

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

1055 ) 

1056 

1057 @classmethod 

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

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

1060 

1061 Parameters: 

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

1063 

1064 Raises: 

1065 ~icalendar.error.JCalParsingError: If the jCal provided is invalid. 

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

1067 

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

1069 

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

1071 

1072 .. code-block:: pycon 

1073 

1074 >>> from icalendar import Component 

1075 >>> jcal = ["vcalendar", 

1076 ... [ 

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

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

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

1080 ... ], 

1081 ... [ 

1082 ... ["vevent", 

1083 ... [ 

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

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

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

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

1088 ... ], 

1089 ... [] 

1090 ... ] 

1091 ... ] 

1092 ... ] 

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

1094 >>> print(calendar.name) 

1095 VCALENDAR 

1096 >>> print(calendar.prodid) 

1097 -//Example Inc.//Example Calendar//EN 

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

1099 >>> print(event.summary) 

1100 Planning meeting 

1101 

1102 """ 

1103 if isinstance(jcal, str): 

1104 jcal = json.loads(jcal) 

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

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

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

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

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

1110 root, root_subcomponents = _node_from_jcal(jcal, cls) 

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

1112 while stack: 

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

1114 for i, subcomponent in enumerate(subcomponents): 

1115 child_prefix = [*prefix, 2, i] 

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

1117 # implementation. This also preserves the error value and 

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

1119 with JCalParsingError.reraise_with_path_added(*child_prefix): 

1120 child, child_subcomponents = _node_from_jcal( 

1121 subcomponent, type(parent) 

1122 ) 

1123 parent.subcomponents.append(child) 

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

1125 return root 

1126 

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

1128 """Copy the component. 

1129 

1130 Parameters: 

1131 recursive: 

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

1133 and all its properties. 

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

1135 

1136 Returns: 

1137 A copy of the component. 

1138 

1139 Examples: 

1140 

1141 Create a shallow copy of a component: 

1142 

1143 .. code-block:: pycon 

1144 

1145 >>> from icalendar import Event 

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

1147 >>> event_copy = event.copy() 

1148 >>> str(event_copy.description) 

1149 'Event to be copied' 

1150 

1151 Shallow copies lose their subcomponents: 

1152 

1153 .. code-block:: pycon 

1154 

1155 >>> from icalendar import Calendar 

1156 >>> calendar = Calendar.example() 

1157 >>> len(calendar.subcomponents) 

1158 3 

1159 >>> calendar_copy = calendar.copy() 

1160 >>> len(calendar_copy.subcomponents) 

1161 0 

1162 

1163 A recursive copy also copies all the subcomponents: 

1164 

1165 .. code-block:: pycon 

1166 

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

1168 >>> len(full_calendar_copy.subcomponents) 

1169 3 

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

1171 True 

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

1173 False 

1174 

1175 """ 

1176 if recursive: 

1177 return deepcopy(self) 

1178 return super().copy() 

1179 

1180 def is_lazy(self) -> bool: 

1181 """This component is fully parsed.""" 

1182 return False 

1183 

1184 def parse(self) -> Self: 

1185 """Return the fully parsed component. 

1186 

1187 For non-lazy components, this returns self. 

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

1189 """ 

1190 return self 

1191 

1192 @overload 

1193 def to_xcal( 

1194 self, 

1195 destination: Element | IO[bytes], 

1196 ) -> None: ... 

1197 

1198 @overload 

1199 def to_xcal( 

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

1201 ) -> None: ... 

1202 

1203 @overload 

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

1205 

1206 def to_xcal( 

1207 self, 

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

1209 *, 

1210 indent: int | str | None = None, 

1211 ) -> bytes | None: 

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

1213 

1214 Parameters: 

1215 destination: The iCalendar stream or a file. 

1216 indent: The indentation for pretty printing the XML. 

1217 

1218 Returns: 

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

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

1221 

1222 Raises: 

1223 TypeError: If ``destination`` is neither an XML Element nor a file-like 

1224 object, or if ``indent`` is neither an integer nor a string. 

1225 ~icalendar.error.InvalidCalendar: If an XML property cannot be written 

1226 to xCal. See :meth:`vXml.to_xcal() <icalendar.prop.xml.vXml.to_xcal>`. 

1227 

1228 Example: 

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

1230 

1231 .. code-block:: pycon 

1232 

1233 >>> from icalendar import Calendar 

1234 >>> cal = Calendar.new() 

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

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

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

1238 <vcalendar> 

1239 <properties> 

1240 <version> 

1241 <text>2.0</text> 

1242 </version> 

1243 <prodid> 

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

1245 </prodid> 

1246 <uid> 

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

1248 </uid> 

1249 </properties> 

1250 </vcalendar> 

1251 </icalendar> 

1252 """ 

1253 from io import BytesIO 

1254 

1255 input_is_element = isinstance(destination, Element) 

1256 if input_is_element: 

1257 stream = destination 

1258 else: 

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

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

1261 raise TypeError( 

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

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

1264 ) 

1265 self.to_xcal_element(stream) 

1266 if indent is not None: 

1267 if isinstance(indent, int): 

1268 indent = " " * indent 

1269 elif not isinstance(indent, str): 

1270 raise TypeError( 

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

1272 ) 

1273 if input_is_element: 

1274 return None 

1275 return_bytes = destination is None 

1276 file = BytesIO() if return_bytes else destination 

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

1278 if indent is not None: 

1279 file.write(b"\n") 

1280 with _preserve_indentation_of_xcal_extension_properties(stream): 

1281 indent_xml(stream, space=indent) 

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

1283 if indent is not None: 

1284 file.write(b"\n") 

1285 if return_bytes: 

1286 return file.getvalue() 

1287 return None 

1288 

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

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

1291 self._validate_name() 

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

1293 if len(self) > 0: 

1294 e_properties = SubElement(e_component, "properties") 

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

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

1297 prop: VPROPERTY 

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

1299 prop.to_xcal(e_property) 

1300 if self.subcomponents: 

1301 e_components = SubElement(e_component, "components") 

1302 for subcomponent in self.subcomponents: 

1303 subcomponent.to_xcal(e_components) 

1304 

1305 @classmethod 

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

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

1308 

1309 Parameters: 

1310 xcal: The xCal element or data to parse. 

1311 

1312 Returns: 

1313 The parsed component. 

1314 

1315 Raises: 

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

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

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

1319 

1320 Example: 

1321 

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

1323 

1324 >>> from icalendar import Calendar 

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

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

1327 ... <vcalendar> 

1328 ... <properties> 

1329 ... <prodid> 

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

1331 ... </prodid> 

1332 ... <version> 

1333 ... <text>2.0</text> 

1334 ... </version> 

1335 ... </properties> 

1336 ... </vcalendar> 

1337 ... </icalendar> 

1338 ... ''' 

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

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

1341 True 

1342 

1343 """ 

1344 from icalendar.parser.xcal.component import XCalComponentParser 

1345 

1346 if isinstance(xcal, Path): 

1347 xcal = xcal.open("rb") 

1348 elif isinstance(xcal, bytes): 

1349 xcal = BytesIO(xcal) 

1350 if hasattr(xcal, "read"): 

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

1352 elif isinstance(xcal, Element): 

1353 element = xcal 

1354 else: 

1355 raise TypeError( 

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

1357 ) 

1358 parser = XCalComponentParser( 

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

1360 ) 

1361 components = [] 

1362 while not parser.is_finished(): 

1363 component = parser.parse_component() 

1364 components.append(component) 

1365 return components 

1366 

1367 

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

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

1370 

1371 Module-level helper for :meth:`~icalendar.cal.component.Component.from_jcal`: it has no ties to a 

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

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

1374 

1375 Parameters: 

1376 jcal: The jCal list for one component. 

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

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

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

1380 

1381 Returns: 

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

1383 returned for the caller to walk iteratively. 

1384 

1385 Raises: 

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

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

1388 """ 

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

1390 raise JCalParsingError( 

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

1392 ) 

1393 name, properties, subcomponents = jcal 

1394 if not isinstance(name, str): 

1395 raise JCalParsingError( 

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

1397 ) 

1398 JCalParsingError.validate_jcal_token(name, "component name", starting_cls, [0]) 

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

1400 # delegate to correct component class 

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

1402 else: 

1403 component_cls = starting_cls 

1404 component = component_cls() 

1405 if not isinstance(properties, list): 

1406 raise JCalParsingError( 

1407 "The properties must be a list.", 

1408 component_cls, 

1409 path=1, 

1410 value=properties, 

1411 ) 

1412 for i, prop in enumerate(properties): 

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

1414 prop_name = prop[0] 

1415 prop_value = prop[2] 

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

1417 prop_name, prop_value 

1418 ) 

1419 with JCalParsingError.reraise_with_path_added(1, i): 

1420 v_prop = prop_cls.from_jcal(prop) 

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

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

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

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

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

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

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

1428 # (RFC 7265, section 5.2). 

1429 default_type = component_cls.types_factory.default_value_type(prop_name) 

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

1431 "unknown", 

1432 default_type, 

1433 ): 

1434 v_prop.VALUE = prop_value.upper() 

1435 elif "VALUE" in v_prop.params: 

1436 del v_prop.VALUE 

1437 component.add(prop_name, v_prop) 

1438 if not isinstance(subcomponents, list): 

1439 raise JCalParsingError( 

1440 "The subcomponents must be a list.", 

1441 component_cls, 

1442 2, 

1443 value=subcomponents, 

1444 ) 

1445 return component, subcomponents 

1446 

1447 

1448__all__ = ["Component"]