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

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

340 statements  

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

2 

3from __future__ import annotations 

4 

5import json 

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

7from typing import TYPE_CHECKING, ClassVar 

8 

9from icalendar.attr import ( 

10 CONCEPTS_TYPE_SETTER, 

11 LINKS_TYPE_SETTER, 

12 RELATED_TO_TYPE_SETTER, 

13 comments_property, 

14 concepts_property, 

15 links_property, 

16 refids_property, 

17 related_to_property, 

18 single_utc_property, 

19 uid_property, 

20) 

21from icalendar.cal.component_factory import ComponentFactory 

22from icalendar.caselessdict import CaselessDict 

23from icalendar.error import InvalidCalendar, JCalParsingError 

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

25from icalendar.parser_tools import DEFAULT_ENCODING 

26from icalendar.prop import VPROPERTY, TypesFactory, vDDDLists, vText 

27from icalendar.timezone import tzp 

28from icalendar.tools import is_date 

29 

30if TYPE_CHECKING: 

31 from icalendar.compatibility import Self 

32 

33_marker = [] 

34 

35 

36class Component(CaselessDict): 

37 """Base class for calendar components. 

38 

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

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

41 directly, but rather one of the subclasses. 

42 

43 Attributes: 

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

45 required: These properties are required. 

46 singletons: These properties must only appear once. 

47 multiple: These properties may occur more than once. 

48 exclusive: These properties are mutually exclusive. 

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

50 ignore_exceptions: If True, and we cannot parse this 

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

52 exception propagate upwards. 

53 types_factory: Factory for property types 

54 """ 

55 

56 name = None # should be defined in each component 

57 required = () # These properties are required 

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

59 multiple = () # may occur more than once 

60 exclusive = () # These properties are mutually exclusive 

61 inclusive: ( 

62 tuple[str] | tuple[tuple[str, str]] 

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

64 # ('duration', 'repeat') 

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

66 # component, we will silently ignore 

67 # it, rather than let the exception 

68 # propagate upwards 

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

70 

71 types_factory = TypesFactory.instance() 

72 _components_factory: ClassVar[ComponentFactory | None] = None 

73 

74 @classmethod 

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

76 """Return a component with this name. 

77 

78 Arguments: 

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

80 """ 

81 if cls._components_factory is None: 

82 cls._components_factory = ComponentFactory() 

83 return cls._components_factory.get_component_class(name) 

84 

85 @staticmethod 

86 def _infer_value_type( 

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

88 ) -> str | None: 

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

90 

91 Args: 

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

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

94 or :py:class:`list`. 

95 

96 Returns: 

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

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

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

100 """ 

101 if isinstance(value, list): 

102 if not value: 

103 return None 

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

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

106 return "DATE" 

107 # Check if ALL items are time 

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

109 return "TIME" 

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

111 return None 

112 if is_date(value): 

113 return "DATE" 

114 if isinstance(value, time): 

115 return "TIME" 

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

117 return None 

118 

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

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

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

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

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

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

125 # parsing a property, contains error strings 

126 

127 def __bool__(self): 

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

129 return True 

130 

131 def is_empty(self): 

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

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

134 

135 ############################# 

136 # handling of property values 

137 

138 @classmethod 

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

140 """Encode values to icalendar property values. 

141 

142 :param name: Name of the property. 

143 :type name: string 

144 

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

146 any of the icalendar's own property types. 

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

148 

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

150 available, if encode is set to True. 

151 :type parameters: Dictionary 

152 

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

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

155 or False, if not. 

156 :type encode: Boolean 

157 

158 :returns: icalendar property value 

159 """ 

160 if not encode: 

161 return value 

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

163 # Don't encode already encoded values. 

164 obj = value 

165 else: 

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

167 value_param = None 

168 if parameters and "VALUE" in parameters: 

169 value_param = parameters["VALUE"] 

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

171 inferred = cls._infer_value_type(value) 

172 if inferred: 

173 value_param = inferred 

174 # Auto-set the VALUE parameter 

175 if parameters is None: 

176 parameters = {} 

177 if "VALUE" not in parameters: 

178 parameters["VALUE"] = inferred 

179 

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

181 obj = klass(value) 

182 if parameters: 

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

184 obj.params = Parameters() 

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

186 if item is None: 

187 if key in obj.params: 

188 del obj.params[key] 

189 else: 

190 obj.params[key] = item 

191 return obj 

192 

193 def add( 

194 self, 

195 name: str, 

196 value, 

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

198 encode: bool = True, # noqa: FBT001 

199 ): 

200 """Add a property. 

201 

202 :param name: Name of the property. 

203 :type name: string 

204 

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

206 any of the icalendar's own property types. 

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

208 

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

210 available, if encode is set to True. 

211 :type parameters: Dictionary 

212 

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

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

215 or False, if not. 

216 :type encode: Boolean 

217 

218 :returns: None 

219 """ 

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

221 "dtstamp", 

222 "created", 

223 "last-modified", 

224 ): 

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

226 value = tzp.localize_utc(value) 

227 

228 # encode value 

229 if ( 

230 encode 

231 and isinstance(value, list) 

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

233 ): 

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

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

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

237 else: 

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

239 

240 # set value 

241 if name in self: 

242 # If property already exists, append it. 

243 oldval = self[name] 

244 if isinstance(oldval, list): 

245 if isinstance(value, list): 

246 value = oldval + value 

247 else: 

248 oldval.append(value) 

249 value = oldval 

250 else: 

251 value = [oldval, value] 

252 self[name] = value 

253 

254 def _decode(self, name, value): 

255 """Internal for decoding property values.""" 

256 

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

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

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

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

261 if isinstance(value, vDDDLists): 

262 # TODO: Workaround unfinished decoding 

263 return value 

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

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

266 # Workaround to decode vText properly 

267 if isinstance(decoded, vText): 

268 decoded = decoded.encode(DEFAULT_ENCODING) 

269 return decoded 

270 

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

272 """Returns decoded value of property.""" 

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

274 # -rnix 

275 

276 if name in self: 

277 value = self[name] 

278 if isinstance(value, list): 

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

280 return self._decode(name, value) 

281 if default is _marker: 

282 raise KeyError(name) 

283 return default 

284 

285 ######################################################################## 

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

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

288 

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

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

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

292 if decode: 

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

294 return vals 

295 

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

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

298 to that. 

299 """ 

300 if encode: 

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

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

303 

304 ######################### 

305 # Handling of components 

306 

307 def add_component(self, component: Component): 

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

309 self.subcomponents.append(component) 

310 

311 def _walk(self, name, select): 

312 """Walk to given component.""" 

313 result = [] 

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

315 result.append(self) 

316 for subcomponent in self.subcomponents: 

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

318 return result 

319 

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

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

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

323 

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

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

326 and returns True/False. 

327 :returns: A list of components that match. 

328 :rtype: list[Component] 

329 """ 

330 if name is not None: 

331 name = name.upper() 

332 return self._walk(name, select) 

333 

334 ##################### 

335 # Generation 

336 

337 def property_items( 

338 self, 

339 recursive=True, 

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

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

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

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

344 """ 

345 v_text = self.types_factory["text"] 

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

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

348 

349 for name in property_names: 

350 values = self[name] 

351 if isinstance(values, list): 

352 # normally one property is one line 

353 for value in values: 

354 properties.append((name, value)) 

355 else: 

356 properties.append((name, values)) 

357 if recursive: 

358 # recursion is fun! 

359 for subcomponent in self.subcomponents: 

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

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

362 return properties 

363 

364 @classmethod 

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

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

367 stack = [] # a stack of components 

368 comps = [] 

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

370 if not line: 

371 continue 

372 

373 try: 

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

375 except ValueError as e: 

376 # if unable to parse a line within a component 

377 # that ignores exceptions, mark the component 

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

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

380 if not component or not component.ignore_exceptions: 

381 raise 

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

383 continue 

384 

385 uname = name.upper() 

386 # check for start of component 

387 if uname == "BEGIN": 

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

389 # otherwise get a general Components for robustness. 

390 c_name = vals.upper() 

391 c_class = cls.get_component_class(c_name) 

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

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

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

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

396 component = c_class() 

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

398 component.name = c_name 

399 stack.append(component) 

400 # check for end of event 

401 elif uname == "END": 

402 # we are done adding properties to this component 

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

404 if not stack: 

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

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

407 

408 component = stack.pop() 

409 if not stack: # we are at the end 

410 comps.append(component) 

411 else: 

412 stack[-1].add_component(component) 

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

414 tzp.cache_timezone_component(component) 

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

416 else: 

417 # Extract VALUE parameter if present 

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

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

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

421 if not component: 

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

423 # ignore these components in parsing 

424 if uname == "X-COMMENT": 

425 break 

426 raise ValueError( 

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

428 ) 

429 datetime_names = ( 

430 "DTSTART", 

431 "DTEND", 

432 "RECURRENCE-ID", 

433 "DUE", 

434 "RDATE", 

435 "EXDATE", 

436 ) 

437 try: 

438 if name == "FREEBUSY": 

439 vals = vals.split(",") 

440 if "TZID" in params: 

441 parsed_components = [ 

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

443 for val in vals 

444 ] 

445 else: 

446 parsed_components = [ 

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

448 ] 

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

450 parsed_components = [ 

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

452 ] 

453 # Workaround broken ICS files with empty RDATE 

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

455 parsed_components = [] 

456 else: 

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

458 except ValueError as e: 

459 if not component.ignore_exceptions: 

460 raise 

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

462 else: 

463 for parsed_component in parsed_components: 

464 parsed_component.params = params 

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

466 

467 if multiple: 

468 return comps 

469 if len(comps) > 1: 

470 raise ValueError( 

471 cls._format_error( 

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

473 ) 

474 ) 

475 if len(comps) < 1: 

476 raise ValueError( 

477 cls._format_error( 

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

479 ) 

480 ) 

481 return comps[0] 

482 

483 @staticmethod 

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

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

486 max_error_length = 100 - 3 

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

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

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

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

491 

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

493 """Returns property as content line.""" 

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

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

496 

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

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

499 contentlines = Contentlines() 

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

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

502 contentlines.append(cl) 

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

504 return contentlines 

505 

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

507 """ 

508 :param sorted: Whether parameters and properties should be 

509 lexicographically sorted. 

510 """ 

511 

512 content_lines = self.content_lines(sorted=sorted) 

513 return content_lines.to_ical() 

514 

515 def __repr__(self): 

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

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

518 return ( 

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

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

521 ) 

522 

523 def __eq__(self, other): 

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

525 return False 

526 

527 properties_equal = super().__eq__(other) 

528 if not properties_equal: 

529 return False 

530 

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

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

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

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

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

536 for subcomponent in self.subcomponents: 

537 if subcomponent not in other.subcomponents: 

538 return False 

539 

540 return True 

541 

542 DTSTAMP = stamp = single_utc_property( 

543 "DTSTAMP", 

544 """RFC 5545: 

545 

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

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

548 

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

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

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

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

553 property specifies the date and time that the information 

554 associated with the calendar component was last revised in the 

555 calendar store. 

556 

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

558 

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

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

561 property. 

562 """, 

563 ) 

564 LAST_MODIFIED = single_utc_property( 

565 "LAST-MODIFIED", 

566 """RFC 5545: 

567 

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

569 information associated with the calendar component was last 

570 revised in the calendar store. 

571 

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

573 file in the file system. 

574 

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

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

577 """, 

578 ) 

579 

580 @property 

581 def last_modified(self) -> datetime: 

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

583 

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

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

586 """ 

587 return self.LAST_MODIFIED or self.DTSTAMP 

588 

589 @last_modified.setter 

590 def last_modified(self, value): 

591 self.LAST_MODIFIED = value 

592 

593 @last_modified.deleter 

594 def last_modified(self): 

595 del self.LAST_MODIFIED 

596 

597 @property 

598 def created(self) -> datetime: 

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

600 

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

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

603 """ 

604 return self.CREATED or self.DTSTAMP 

605 

606 @created.setter 

607 def created(self, value): 

608 self.CREATED = value 

609 

610 @created.deleter 

611 def created(self): 

612 del self.CREATED 

613 

614 def is_thunderbird(self) -> bool: 

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

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

617 

618 @staticmethod 

619 def _utc_now(): 

620 """Return now as UTC value.""" 

621 return datetime.now(timezone.utc) 

622 

623 uid = uid_property 

624 comments = comments_property 

625 links = links_property 

626 related_to = related_to_property 

627 concepts = concepts_property 

628 refids = refids_property 

629 

630 CREATED = single_utc_property( 

631 "CREATED", 

632 """ 

633 CREATED specifies the date and time that the calendar 

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

635 store. 

636 

637 Conformance: 

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

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

640 specified as a date with UTC time. 

641 

642 """, 

643 ) 

644 

645 _validate_new = True 

646 

647 @staticmethod 

648 def _validate_start_and_end(start, end): 

649 """This validates start and end. 

650 

651 Raises: 

652 InvalidCalendar: If the information is not valid 

653 """ 

654 if start is None or end is None: 

655 return 

656 if start > end: 

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

658 

659 @classmethod 

660 def new( 

661 cls, 

662 created: date | None = None, 

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

664 concepts: CONCEPTS_TYPE_SETTER = None, 

665 last_modified: date | None = None, 

666 links: LINKS_TYPE_SETTER = None, 

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

668 related_to: RELATED_TO_TYPE_SETTER = None, 

669 stamp: date | None = None, 

670 ) -> Component: 

671 """Create a new component. 

672 

673 Arguments: 

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

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

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

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

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

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

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

681 

682 Raises: 

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

684 

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

686 """ 

687 component = cls() 

688 component.DTSTAMP = stamp 

689 component.created = created 

690 component.last_modified = last_modified 

691 component.comments = comments 

692 component.links = links 

693 component.related_to = related_to 

694 component.concepts = concepts 

695 component.refids = refids 

696 return component 

697 

698 def to_jcal(self) -> list: 

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

700 

701 Returns: 

702 jCal object 

703 

704 See also :attr:`to_json`. 

705 

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

707 

708 .. code-block:: pycon 

709 

710 >>> from icalendar import Event 

711 >>> from datetime import date 

712 >>> from pprint import pprint 

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

714 >>> pprint(event.to_jcal()) 

715 ['vevent', 

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

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

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

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

720 []] 

721 """ 

722 properties = [] 

723 for key, value in self.items(): 

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

725 properties.append(item.to_jcal(key.lower())) 

726 return [ 

727 self.name.lower(), 

728 properties, 

729 [subcomponent.to_jcal() for subcomponent in self.subcomponents], 

730 ] 

731 

732 def to_json(self) -> str: 

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

734 

735 Returns: 

736 JSON string 

737 

738 See also :attr:`to_jcal`. 

739 """ 

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

741 

742 @classmethod 

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

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

745 

746 Args: 

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

748 

749 Raises: 

750 JCalParsingError: If the jCal provided is invalid. 

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

752 

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

754 

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

756 

757 .. code-block:: pycon 

758 

759 >>> from icalendar import Component 

760 >>> jcal = ["vcalendar", 

761 ... [ 

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

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

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

765 ... ], 

766 ... [ 

767 ... ["vevent", 

768 ... [ 

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

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

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

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

773 ... ], 

774 ... [] 

775 ... ] 

776 ... ] 

777 ... ] 

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

779 >>> print(calendar.name) 

780 VCALENDAR 

781 >>> print(calendar.prodid) 

782 -//Example Inc.//Example Calendar//EN 

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

784 >>> print(event.summary) 

785 Planning meeting 

786 

787 """ 

788 if isinstance(jcal, str): 

789 jcal = json.loads(jcal) 

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

791 raise JCalParsingError( 

792 "A component must be a list with 3 items.", cls, value=jcal 

793 ) 

794 name, properties, subcomponents = jcal 

795 if not isinstance(name, str): 

796 raise JCalParsingError( 

797 "The name must be a string.", cls, path=[0], value=name 

798 ) 

799 if name.upper() != cls.name: 

800 # delegate to correct component class 

801 component_cls = cls.get_component_class(name.upper()) 

802 return component_cls.from_jcal(jcal) 

803 component = cls() 

804 if not isinstance(properties, list): 

805 raise JCalParsingError( 

806 "The properties must be a list.", cls, path=1, value=properties 

807 ) 

808 for i, prop in enumerate(properties): 

809 JCalParsingError.validate_property(prop, cls, path=[1, i]) 

810 prop_name = prop[0] 

811 prop_value = prop[2] 

812 prop_cls: type[VPROPERTY] = cls.types_factory.for_property( 

813 prop_name, prop_value 

814 ) 

815 with JCalParsingError.reraise_with_path_added(1, i): 

816 v_prop = prop_cls.from_jcal(prop) 

817 # if we use the default value for that property, we can delete the 

818 # VALUE parameter 

819 if prop_cls == cls.types_factory.for_property(prop_name): 

820 del v_prop.VALUE 

821 component.add(prop_name, v_prop) 

822 if not isinstance(subcomponents, list): 

823 raise JCalParsingError( 

824 "The subcomponents must be a list.", cls, 2, value=subcomponents 

825 ) 

826 for i, subcomponent in enumerate(subcomponents): 

827 with JCalParsingError.reraise_with_path_added(2, i): 

828 component.subcomponents.append(cls.from_jcal(subcomponent)) 

829 return component 

830 

831 

832__all__ = ["Component"]