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

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

354 statements  

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

2 

3from __future__ import annotations 

4 

5import json 

6from copy import deepcopy 

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

8from pathlib import Path 

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

10 

11from icalendar.attr import ( 

12 CONCEPTS_TYPE_SETTER, 

13 LINKS_TYPE_SETTER, 

14 RELATED_TO_TYPE_SETTER, 

15 comments_property, 

16 concepts_property, 

17 links_property, 

18 refids_property, 

19 related_to_property, 

20 single_utc_property, 

21 uid_property, 

22) 

23from icalendar.cal.component_factory import ComponentFactory 

24from icalendar.caselessdict import CaselessDict 

25from icalendar.error import InvalidCalendar, JCalParsingError 

26from icalendar.parser import ( 

27 Contentline, 

28 Contentlines, 

29 Parameters, 

30 q_join, 

31 q_split, 

32) 

33from icalendar.parser.ical.component import ComponentIcalParser 

34from icalendar.parser_tools import DEFAULT_ENCODING 

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

36from icalendar.timezone import tzp 

37from icalendar.tools import is_date 

38 

39if TYPE_CHECKING: 

40 from collections.abc import Iterable 

41 

42 from icalendar.compatibility import Self 

43 

44_marker = [] 

45 

46 

47class Component(CaselessDict): 

48 """Base class for calendar components. 

49 

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

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

52 directly, but rather one of the subclasses. 

53 """ 

54 

55 name: ClassVar[str | None] = None 

56 """The name of the component. 

57 

58 This should be defined in each component class. 

59 

60 Example: ``VCALENDAR``. 

61 """ 

62 

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

64 """These properties are required.""" 

65 

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

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

68 

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

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

71 

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

73 """These properties are mutually exclusive.""" 

74 

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

76 """These properties are inclusive. 

77 

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

79 second one must also occur. 

80 

81 Example: 

82 

83 .. code-block:: python 

84 

85 ('duration', 'repeat') 

86 """ 

87 

88 ignore_exceptions: ClassVar[bool] = False 

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

90 

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

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

93 """ 

94 

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

96 _components_factory: ClassVar[ComponentFactory | None] = None 

97 

98 subcomponents: list[Component] 

99 """All subcomponents of this component.""" 

100 

101 @classmethod 

102 def _get_component_factory(cls) -> ComponentFactory: 

103 """Get the component factory.""" 

104 if cls._components_factory is None: 

105 cls._components_factory = ComponentFactory() 

106 return cls._components_factory 

107 

108 @classmethod 

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

110 """Return a component with this name. 

111 

112 Parameters: 

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

114 """ 

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

116 

117 @classmethod 

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

119 """Register a custom component class. 

120 

121 Parameters: 

122 component_class: Component subclass to register. 

123 Must have a ``name`` attribute. 

124 

125 Raises: 

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

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

128 

129 Examples: 

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

131 

132 .. code-block:: pycon 

133 

134 >>> from icalendar import Component 

135 >>> class XExample(Component): 

136 ... name = "X-EXAMPLE" 

137 ... def custom_method(self): 

138 ... return "custom" 

139 >>> Component.register(XExample) 

140 """ 

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

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

143 

144 # Check if already registered 

145 component_factory = cls._get_component_factory() 

146 existing = component_factory.get(component_class.name) 

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

148 raise ValueError( 

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

150 f" as {existing}" 

151 ) 

152 

153 component_factory.add_component_class(component_class) 

154 

155 @staticmethod 

156 def _infer_value_type( 

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

158 ) -> str | None: 

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

160 

161 Parameters: 

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

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

164 or :py:class:`list`. 

165 

166 Returns: 

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

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

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

170 """ 

171 if isinstance(value, list): 

172 if not value: 

173 return None 

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

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

176 return "DATE" 

177 # Check if ALL items are time 

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

179 return "TIME" 

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

181 return None 

182 if is_date(value): 

183 return "DATE" 

184 if isinstance(value, time): 

185 return "TIME" 

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

187 return None 

188 

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

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

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

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

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

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

195 # parsing a property, contains error strings 

196 

197 def __bool__(self): 

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

199 return True 

200 

201 def __getitem__(self, key): 

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

203 return super().__getitem__(key) 

204 

205 def get(self, key, default=None): 

206 """Get property value with default.""" 

207 try: 

208 return self[key] 

209 except KeyError: 

210 return default 

211 

212 def is_empty(self): 

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

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

215 

216 ############################# 

217 # handling of property values 

218 

219 @classmethod 

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

221 """Encode values to icalendar property values. 

222 

223 :param name: Name of the property. 

224 :type name: string 

225 

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

227 any of the icalendar's own property types. 

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

229 

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

231 available, if encode is set to True. 

232 :type parameters: Dictionary 

233 

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

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

236 or False, if not. 

237 :type encode: Boolean 

238 

239 :returns: icalendar property value 

240 """ 

241 if not encode: 

242 return value 

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

244 # Don't encode already encoded values. 

245 obj = value 

246 else: 

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

248 value_param = None 

249 if parameters and "VALUE" in parameters: 

250 value_param = parameters["VALUE"] 

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

252 inferred = cls._infer_value_type(value) 

253 if inferred: 

254 value_param = inferred 

255 # Auto-set the VALUE parameter 

256 if parameters is None: 

257 parameters = {} 

258 if "VALUE" not in parameters: 

259 parameters["VALUE"] = inferred 

260 

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

262 obj = klass(value) 

263 if parameters: 

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

265 obj.params = Parameters() 

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

267 if item is None: 

268 if key in obj.params: 

269 del obj.params[key] 

270 else: 

271 obj.params[key] = item 

272 return obj 

273 

274 def add( 

275 self, 

276 name: str, 

277 value, 

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

279 encode: bool = True, 

280 ): 

281 """Add a property. 

282 

283 :param name: Name of the property. 

284 :type name: string 

285 

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

287 any of the icalendar's own property types. 

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

289 

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

291 available, if encode is set to True. 

292 :type parameters: Dictionary 

293 

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

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

296 or False, if not. 

297 :type encode: Boolean 

298 

299 :returns: None 

300 """ 

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

302 "dtstamp", 

303 "created", 

304 "last-modified", 

305 ): 

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

307 value = tzp.localize_utc(value) 

308 

309 # encode value 

310 if ( 

311 encode 

312 and isinstance(value, list) 

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

314 ): 

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

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

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

318 else: 

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

320 

321 # set value 

322 if name in self: 

323 # If property already exists, append it. 

324 oldval = self[name] 

325 if isinstance(oldval, list): 

326 if isinstance(value, list): 

327 value = oldval + value 

328 else: 

329 oldval.append(value) 

330 value = oldval 

331 else: 

332 value = [oldval, value] 

333 self[name] = value 

334 

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

336 """Internal for decoding property values.""" 

337 

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

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

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

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

342 if hasattr(value, "ical_value"): 

343 return value.ical_value 

344 if isinstance(value, vDDDLists): 

345 # TODO: Workaround unfinished decoding 

346 return value 

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

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

349 # Workaround to decode vText properly 

350 if isinstance(decoded, vText): 

351 decoded = decoded.encode(DEFAULT_ENCODING) 

352 return decoded 

353 

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

355 """Returns decoded value of property. 

356 

357 A component maps keys to icalendar property value types. 

358 This function returns values compatible to native Python types. 

359 """ 

360 if name in self: 

361 value = self[name] 

362 if isinstance(value, list): 

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

364 return self._decode(name, value) 

365 if default is _marker: 

366 raise KeyError(name) 

367 return default 

368 

369 ######################################################################## 

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

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

372 

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

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

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

376 if decode: 

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

378 return vals 

379 

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

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

382 to that. 

383 """ 

384 if encode: 

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

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

387 

388 ######################### 

389 # Handling of components 

390 

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

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

393 self.subcomponents.append(component) 

394 

395 def _walk( 

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

397 ) -> list[Component]: 

398 """Walk to given component.""" 

399 result = [] 

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

401 result.append(self) 

402 for subcomponent in self.subcomponents: 

403 result += subcomponent._walk(name, select) 

404 return result 

405 

406 def walk( 

407 self, 

408 name: str | None = None, 

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

410 ) -> list[Component]: 

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

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

413 

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

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

416 and returns True/False. 

417 :returns: A list of components that match. 

418 :rtype: list[Component] 

419 """ 

420 if name is not None: 

421 name = name.upper() 

422 return self._walk(name, select) 

423 

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

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

426 

427 Parameters: 

428 uid: The UID of the component. 

429 

430 Returns: 

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

432 """ 

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

434 

435 ##################### 

436 # Generation 

437 

438 def property_items( 

439 self, 

440 recursive=True, 

441 sorted: bool = True, 

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

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

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

445 """ 

446 v_text = self.types_factory["text"] 

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

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

449 

450 for name in property_names: 

451 values = self[name] 

452 if isinstance(values, list): 

453 # normally one property is one line 

454 for value in values: 

455 properties.append((name, value)) 

456 else: 

457 properties.append((name, values)) 

458 if recursive: 

459 # recursion is fun! 

460 for subcomponent in self.subcomponents: 

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

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

463 return properties 

464 

465 @overload 

466 @classmethod 

467 def from_ical( 

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

469 ) -> Component: ... 

470 

471 @overload 

472 @classmethod 

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

474 

475 @classmethod 

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

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

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

479 

480 @classmethod 

481 def from_ical( 

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

483 ) -> Component | list[Component]: 

484 """Parse iCalendar data into component instances. 

485 

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

487 

488 Parameters: 

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

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

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

492 

493 Returns: 

494 Component or list of components 

495 

496 See Also: 

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

498 """ 

499 if isinstance(st, Path): 

500 st = st.read_bytes() 

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

502 path = Path(st) 

503 try: 

504 is_file = path.is_file() 

505 except OSError: 

506 is_file = False 

507 if is_file: 

508 st = path.read_bytes() 

509 parser = cls._get_ical_parser(st) 

510 components = parser.parse() 

511 if multiple: 

512 return components 

513 if len(components) > 1: 

514 raise ValueError( 

515 cls._format_error( 

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

517 ) 

518 ) 

519 if len(components) < 1: 

520 raise ValueError( 

521 cls._format_error( 

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

523 ) 

524 ) 

525 return components[0] 

526 

527 @staticmethod 

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

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

530 max_error_length = 100 - 3 

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

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

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

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

535 

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

537 """Returns property as content line.""" 

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

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

540 

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

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

543 contentlines = Contentlines() 

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

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

546 contentlines.append(cl) 

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

548 return contentlines 

549 

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

551 """ 

552 :param sorted: Whether parameters and properties should be 

553 lexicographically sorted. 

554 """ 

555 

556 content_lines = self.content_lines(sorted=sorted) 

557 return content_lines.to_ical() 

558 

559 def __repr__(self): 

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

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

562 return ( 

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

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

565 ) 

566 

567 def __eq__(self, other): 

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

569 return False 

570 

571 properties_equal = super().__eq__(other) 

572 if not properties_equal: 

573 return False 

574 

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

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

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

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

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

580 for subcomponent in self.subcomponents: 

581 if subcomponent not in other.subcomponents: 

582 return False 

583 

584 # We now know the other component's subcomponents are not a strict subset 

585 # of this component's. However, we still need to check the other way around. 

586 for subcomponent in other.subcomponents: 

587 if subcomponent not in self.subcomponents: 

588 return False 

589 

590 return True 

591 

592 DTSTAMP = stamp = single_utc_property( 

593 "DTSTAMP", 

594 """RFC 5545: 

595 

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

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

598 

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

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

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

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

603 property specifies the date and time that the information 

604 associated with the calendar component was last revised in the 

605 calendar store. 

606 

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

608 

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

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

611 property. 

612 """, 

613 ) 

614 LAST_MODIFIED = single_utc_property( 

615 "LAST-MODIFIED", 

616 """RFC 5545: 

617 

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

619 information associated with the calendar component was last 

620 revised in the calendar store. 

621 

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

623 file in the file system. 

624 

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

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

627 """, 

628 ) 

629 

630 @property 

631 def last_modified(self) -> datetime: 

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

633 

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

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

636 """ 

637 return self.LAST_MODIFIED or self.DTSTAMP 

638 

639 @last_modified.setter 

640 def last_modified(self, value): 

641 self.LAST_MODIFIED = value 

642 

643 @last_modified.deleter 

644 def last_modified(self): 

645 del self.LAST_MODIFIED 

646 

647 @property 

648 def created(self) -> datetime: 

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

650 

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

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

653 """ 

654 return self.CREATED or self.DTSTAMP 

655 

656 @created.setter 

657 def created(self, value): 

658 self.CREATED = value 

659 

660 @created.deleter 

661 def created(self): 

662 del self.CREATED 

663 

664 def is_thunderbird(self) -> bool: 

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

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

667 

668 @staticmethod 

669 def _utc_now(): 

670 """Return now as UTC value.""" 

671 return datetime.now(timezone.utc) 

672 

673 uid = uid_property 

674 comments = comments_property 

675 links = links_property 

676 related_to = related_to_property 

677 concepts = concepts_property 

678 refids = refids_property 

679 

680 CREATED = single_utc_property( 

681 "CREATED", 

682 """ 

683 CREATED specifies the date and time that the calendar 

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

685 store. 

686 

687 Conformance: 

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

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

690 specified as a date with UTC time. 

691 

692 """, 

693 ) 

694 

695 _validate_new = True 

696 

697 @staticmethod 

698 def _validate_start_and_end(start, end): 

699 """This validates start and end. 

700 

701 Raises: 

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

703 """ 

704 if start is None or end is None: 

705 return 

706 if start > end: 

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

708 

709 @classmethod 

710 def new( 

711 cls, 

712 created: date | None = None, 

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

714 concepts: CONCEPTS_TYPE_SETTER = None, 

715 last_modified: date | None = None, 

716 links: LINKS_TYPE_SETTER = None, 

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

718 related_to: RELATED_TO_TYPE_SETTER = None, 

719 stamp: date | None = None, 

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

721 ) -> Component: 

722 """Create a new component. 

723 

724 Parameters: 

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

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

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

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

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

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

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

732 subcomponents: The subcomponents of the component. 

733 

734 Raises: 

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

736 according to :rfc:`5545`. 

737 

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

739 validation. 

740 """ 

741 component = cls() 

742 component.DTSTAMP = stamp 

743 component.created = created 

744 component.last_modified = last_modified 

745 component.comments = comments 

746 component.links = links 

747 component.related_to = related_to 

748 component.concepts = concepts 

749 component.refids = refids 

750 if subcomponents is not None: 

751 component.subcomponents = ( 

752 subcomponents 

753 if isinstance(subcomponents, list) 

754 else list(subcomponents) 

755 ) 

756 return component 

757 

758 def to_jcal(self) -> list: 

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

760 

761 Returns: 

762 jCal object 

763 

764 See also :attr:`to_json`. 

765 

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

767 

768 .. code-block:: pycon 

769 

770 >>> from icalendar import Event 

771 >>> from datetime import date 

772 >>> from pprint import pprint 

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

774 >>> pprint(event.to_jcal()) 

775 ['vevent', 

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

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

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

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

780 []] 

781 """ 

782 properties = [] 

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

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

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

786 return [ 

787 self.name.lower(), 

788 properties, 

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

790 ] 

791 

792 def to_json(self) -> str: 

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

794 

795 Returns: 

796 JSON string 

797 

798 See also :attr:`to_jcal`. 

799 """ 

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

801 

802 @classmethod 

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

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

805 

806 Parameters: 

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

808 

809 Raises: 

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

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

812 

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

814 

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

816 

817 .. code-block:: pycon 

818 

819 >>> from icalendar import Component 

820 >>> jcal = ["vcalendar", 

821 ... [ 

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

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

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

825 ... ], 

826 ... [ 

827 ... ["vevent", 

828 ... [ 

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

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

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

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

833 ... ], 

834 ... [] 

835 ... ] 

836 ... ] 

837 ... ] 

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

839 >>> print(calendar.name) 

840 VCALENDAR 

841 >>> print(calendar.prodid) 

842 -//Example Inc.//Example Calendar//EN 

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

844 >>> print(event.summary) 

845 Planning meeting 

846 

847 """ 

848 if isinstance(jcal, str): 

849 jcal = json.loads(jcal) 

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

851 raise JCalParsingError( 

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

853 ) 

854 name, properties, subcomponents = jcal 

855 if not isinstance(name, str): 

856 raise JCalParsingError( 

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

858 ) 

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

860 # delegate to correct component class 

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

862 return component_cls.from_jcal(jcal) 

863 component = cls() 

864 if not isinstance(properties, list): 

865 raise JCalParsingError( 

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

867 ) 

868 for i, prop in enumerate(properties): 

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

870 prop_name = prop[0] 

871 prop_value = prop[2] 

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

873 prop_name, prop_value 

874 ) 

875 with JCalParsingError.reraise_with_path_added(1, i): 

876 v_prop = prop_cls.from_jcal(prop) 

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

878 # VALUE parameter 

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

880 del v_prop.VALUE 

881 component.add(prop_name, v_prop) 

882 if not isinstance(subcomponents, list): 

883 raise JCalParsingError( 

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

885 ) 

886 for i, subcomponent in enumerate(subcomponents): 

887 with JCalParsingError.reraise_with_path_added(2, i): 

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

889 return component 

890 

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

892 """Copy the component. 

893 

894 Parameters: 

895 recursive: 

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

897 and all its properties. 

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

899 

900 Returns: 

901 A copy of the component. 

902 

903 Examples: 

904 

905 Create a shallow copy of a component: 

906 

907 .. code-block:: pycon 

908 

909 >>> from icalendar import Event 

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

911 >>> event_copy = event.copy() 

912 >>> str(event_copy.description) 

913 'Event to be copied' 

914 

915 Shallow copies lose their subcomponents: 

916 

917 .. code-block:: pycon 

918 

919 >>> from icalendar import Calendar 

920 >>> calendar = Calendar.example() 

921 >>> len(calendar.subcomponents) 

922 3 

923 >>> calendar_copy = calendar.copy() 

924 >>> len(calendar_copy.subcomponents) 

925 0 

926 

927 A recursive copy also copies all the subcomponents: 

928 

929 .. code-block:: pycon 

930 

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

932 >>> len(full_calendar_copy.subcomponents) 

933 3 

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

935 True 

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

937 False 

938 

939 """ 

940 if recursive: 

941 return deepcopy(self) 

942 return super().copy() 

943 

944 def is_lazy(self) -> bool: 

945 """This component is fully parsed.""" 

946 return False 

947 

948 def parse(self) -> Self: 

949 """Return the fully parsed component. 

950 

951 For non-lazy components, this returns self. 

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

953 """ 

954 return self 

955 

956 

957__all__ = ["Component"]