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

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

362 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 is defined in each component class. 

59 

60 Example: 

61 

62 .. code-block:: pycon 

63 

64 >>> from icalendar import Calendar 

65 >>> cal = Calendar.new() 

66 >>> cal.name 

67 'VCALENDAR' 

68 

69 """ 

70 

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

72 """These properties are required.""" 

73 

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

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

76 

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

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

79 

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

81 """These properties are mutually exclusive.""" 

82 

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

84 """These properties are inclusive. 

85 

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

87 second one must also occur. 

88 

89 Example: 

90 

91 .. code-block:: python 

92 

93 ('duration', 'repeat') 

94 """ 

95 

96 ignore_exceptions: ClassVar[bool] = False 

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

98 

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

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

101 """ 

102 

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

104 _components_factory: ClassVar[ComponentFactory | None] = None 

105 

106 subcomponents: list[Component] 

107 """All subcomponents of this component.""" 

108 

109 @classmethod 

110 def _get_component_factory(cls) -> ComponentFactory: 

111 """Get the component factory.""" 

112 if cls._components_factory is None: 

113 cls._components_factory = ComponentFactory() 

114 return cls._components_factory 

115 

116 @classmethod 

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

118 """Return a component with this name. 

119 

120 Parameters: 

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

122 """ 

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

124 

125 @classmethod 

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

127 """Register a custom component class. 

128 

129 Parameters: 

130 component_class: Component subclass to register. 

131 Must have a ``name`` attribute. 

132 

133 Raises: 

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

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

136 

137 Examples: 

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

139 

140 .. code-block:: pycon 

141 

142 >>> from icalendar import Component 

143 >>> class XExample(Component): 

144 ... name = "X-EXAMPLE" 

145 ... def custom_method(self): 

146 ... return "custom" 

147 >>> Component.register(XExample) 

148 """ 

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

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

151 

152 # Check if already registered 

153 component_factory = cls._get_component_factory() 

154 existing = component_factory.get(component_class.name) 

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

156 raise ValueError( 

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

158 f" as {existing}" 

159 ) 

160 

161 component_factory.add_component_class(component_class) 

162 

163 @staticmethod 

164 def _infer_value_type( 

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

166 ) -> str | None: 

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

168 

169 Parameters: 

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

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

172 or :class:`list`. 

173 

174 Returns: 

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

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

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

178 """ 

179 if isinstance(value, list): 

180 if not value: 

181 return None 

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

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

184 return "DATE" 

185 # Check if ALL items are time 

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

187 return "TIME" 

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

189 return None 

190 if is_date(value): 

191 return "DATE" 

192 if isinstance(value, time): 

193 return "TIME" 

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

195 return None 

196 

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

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

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

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

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

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

203 # parsing a property, contains error strings 

204 

205 def __bool__(self): 

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

207 return True 

208 

209 def __getitem__(self, key): 

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

211 return super().__getitem__(key) 

212 

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

214 """Get property value with default.""" 

215 try: 

216 return self[key] 

217 except KeyError: 

218 return default 

219 

220 def is_empty(self): 

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

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

223 

224 ############################# 

225 # handling of property values 

226 

227 @classmethod 

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

229 """Encode values to icalendar property values. 

230 

231 :param name: Name of the property. 

232 :type name: string 

233 

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

235 any of the icalendar's own property types. 

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

237 

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

239 available, if encode is set to True. 

240 :type parameters: Dictionary 

241 

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

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

244 or False, if not. 

245 :type encode: Boolean 

246 

247 :returns: icalendar property value 

248 """ 

249 if not encode: 

250 return value 

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

252 # Don't encode already encoded values. 

253 obj = value 

254 else: 

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

256 value_param = None 

257 if parameters and "VALUE" in parameters: 

258 value_param = parameters["VALUE"] 

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

260 inferred = cls._infer_value_type(value) 

261 if inferred: 

262 value_param = inferred 

263 # Auto-set the VALUE parameter 

264 if parameters is None: 

265 parameters = {} 

266 if "VALUE" not in parameters: 

267 parameters["VALUE"] = inferred 

268 

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

270 obj = klass(value) 

271 if parameters: 

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

273 obj.params = Parameters() 

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

275 if item is None: 

276 if key in obj.params: 

277 del obj.params[key] 

278 else: 

279 obj.params[key] = item 

280 return obj 

281 

282 def add( 

283 self, 

284 name: str, 

285 value, 

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

287 encode: bool = True, 

288 ): 

289 """Add a property. 

290 

291 :param name: Name of the property. 

292 :type name: string 

293 

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

295 any of the icalendar's own property types. 

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

297 

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

299 available, if encode is set to True. 

300 :type parameters: Dictionary 

301 

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

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

304 or False, if not. 

305 :type encode: Boolean 

306 

307 :returns: None 

308 """ 

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

310 "dtstamp", 

311 "created", 

312 "last-modified", 

313 ): 

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

315 value = tzp.localize_utc(value) 

316 

317 # encode value 

318 if ( 

319 encode 

320 and isinstance(value, list) 

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

322 ): 

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

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

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

326 else: 

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

328 

329 # set value 

330 if name in self: 

331 # If property already exists, append it. 

332 oldval = self[name] 

333 if isinstance(oldval, list): 

334 if isinstance(value, list): 

335 value = oldval + value 

336 else: 

337 oldval.append(value) 

338 value = oldval 

339 else: 

340 value = [oldval, value] 

341 self[name] = value 

342 

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

344 """Internal for decoding property values.""" 

345 

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

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

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

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

350 if hasattr(value, "ical_value"): 

351 return value.ical_value 

352 if isinstance(value, vDDDLists): 

353 # TODO: Workaround unfinished decoding 

354 return value 

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

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

357 # Workaround to decode vText properly 

358 if isinstance(decoded, vText): 

359 decoded = decoded.encode(DEFAULT_ENCODING) 

360 return decoded 

361 

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

363 """Returns decoded value of property. 

364 

365 A component maps keys to icalendar property value types. 

366 This function returns values compatible to native Python types. 

367 """ 

368 if name in self: 

369 value = self[name] 

370 if isinstance(value, list): 

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

372 return self._decode(name, value) 

373 if default is _marker: 

374 raise KeyError(name) 

375 return default 

376 

377 ######################################################################## 

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

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

380 

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

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

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

384 if decode: 

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

386 return vals 

387 

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

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

390 to that. 

391 """ 

392 if encode: 

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

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

395 

396 ######################### 

397 # Handling of components 

398 

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

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

401 self.subcomponents.append(component) 

402 

403 def _walk( 

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

405 ) -> list[Component]: 

406 """Walk to given component.""" 

407 result = [] 

408 stack = [self] 

409 while stack: 

410 component = stack.pop() 

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

412 result.append(component) 

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

414 return result 

415 

416 def walk( 

417 self, 

418 name: str | None = None, 

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

420 ) -> list[Component]: 

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

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

423 

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

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

426 and returns True/False. 

427 :returns: A list of components that match. 

428 :rtype: list[Component] 

429 """ 

430 if name is not None: 

431 name = name.upper() 

432 return self._walk(name, select) 

433 

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

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

436 

437 Parameters: 

438 uid: The UID of the component. 

439 

440 Returns: 

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

442 """ 

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

444 

445 ##################### 

446 # Generation 

447 

448 def property_items( 

449 self, 

450 recursive: bool = True, 

451 sorted: bool = True, 

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

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

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

455 """ 

456 # Iterative implementation to avoid RecursionError 

457 result = [] 

458 v_text = self.types_factory["text"] 

459 # Stack stores (component, state) 

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

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

462 stack = [(self, False)] 

463 while stack: 

464 comp, is_end = stack.pop() 

465 if is_end: 

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

467 else: 

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

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

470 

471 for name in property_names: 

472 values = comp[name] 

473 if isinstance(values, list): 

474 # normally one property is one line 

475 for value in values: 

476 result.append((name, value)) 

477 else: 

478 result.append((name, values)) 

479 

480 # Push the END marker for this component 

481 stack.append((comp, True)) 

482 # Push subcomponents if recursion is enabled 

483 if recursive: 

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

485 for subcomponent in reversed(comp.subcomponents): 

486 stack.append((subcomponent, False)) 

487 

488 return result 

489 

490 @overload 

491 @classmethod 

492 def from_ical( 

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

494 ) -> Component: ... 

495 

496 @overload 

497 @classmethod 

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

499 

500 @classmethod 

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

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

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

504 

505 @classmethod 

506 def from_ical( 

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

508 ) -> Component | list[Component]: 

509 """Parse iCalendar data into component instances. 

510 

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

512 

513 Parameters: 

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

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

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

517 

518 Returns: 

519 Component or list of components 

520 

521 See Also: 

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

523 """ 

524 if isinstance(st, Path): 

525 st = st.read_bytes() 

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

527 path = Path(st) 

528 try: 

529 is_file = path.is_file() 

530 except OSError: 

531 is_file = False 

532 if is_file: 

533 st = path.read_bytes() 

534 parser = cls._get_ical_parser(st) 

535 components = parser.parse() 

536 if multiple: 

537 return components 

538 if len(components) > 1: 

539 raise ValueError( 

540 cls._format_error( 

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

542 ) 

543 ) 

544 if len(components) < 1: 

545 raise ValueError( 

546 cls._format_error( 

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

548 ) 

549 ) 

550 return components[0] 

551 

552 @staticmethod 

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

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

555 max_error_length = 100 - 3 

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

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

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

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

560 

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

562 """Returns property as content line.""" 

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

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

565 

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

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

568 contentlines = Contentlines() 

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

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

571 contentlines.append(cl) 

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

573 return contentlines 

574 

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

576 """ 

577 :param sorted: Whether parameters and properties should be 

578 lexicographically sorted. 

579 """ 

580 

581 content_lines = self.content_lines(sorted=sorted) 

582 return content_lines.to_ical() 

583 

584 def __repr__(self): 

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

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

587 return ( 

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

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

590 ) 

591 

592 def __eq__(self, other): 

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

594 return False 

595 

596 properties_equal = super().__eq__(other) 

597 if not properties_equal: 

598 return False 

599 

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

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

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

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

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

605 for subcomponent in self.subcomponents: 

606 if subcomponent not in other.subcomponents: 

607 return False 

608 

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

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

611 for subcomponent in other.subcomponents: 

612 if subcomponent not in self.subcomponents: 

613 return False 

614 

615 return True 

616 

617 DTSTAMP = stamp = single_utc_property( 

618 "DTSTAMP", 

619 """RFC 5545: 

620 

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

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

623 

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

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

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

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

628 property specifies the date and time that the information 

629 associated with the calendar component was last revised in the 

630 calendar store. 

631 

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

633 

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

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

636 property. 

637 """, 

638 ) 

639 LAST_MODIFIED = single_utc_property( 

640 "LAST-MODIFIED", 

641 """RFC 5545: 

642 

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

644 information associated with the calendar component was last 

645 revised in the calendar store. 

646 

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

648 file in the file system. 

649 

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

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

652 """, 

653 ) 

654 

655 @property 

656 def last_modified(self) -> datetime: 

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

658 

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

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

661 """ 

662 return self.LAST_MODIFIED or self.DTSTAMP 

663 

664 @last_modified.setter 

665 def last_modified(self, value): 

666 self.LAST_MODIFIED = value 

667 

668 @last_modified.deleter 

669 def last_modified(self): 

670 del self.LAST_MODIFIED 

671 

672 @property 

673 def created(self) -> datetime: 

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

675 

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

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

678 """ 

679 return self.CREATED or self.DTSTAMP 

680 

681 @created.setter 

682 def created(self, value): 

683 self.CREATED = value 

684 

685 @created.deleter 

686 def created(self): 

687 del self.CREATED 

688 

689 def is_thunderbird(self) -> bool: 

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

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

692 

693 @staticmethod 

694 def _utc_now(): 

695 """Return now as UTC value.""" 

696 return datetime.now(timezone.utc) 

697 

698 uid = uid_property 

699 comments = comments_property 

700 links = links_property 

701 related_to = related_to_property 

702 concepts = concepts_property 

703 refids = refids_property 

704 

705 CREATED = single_utc_property( 

706 "CREATED", 

707 """ 

708 CREATED specifies the date and time that the calendar 

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

710 store. 

711 

712 Conformance: 

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

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

715 specified as a date with UTC time. 

716 

717 """, 

718 ) 

719 

720 _validate_new = True 

721 

722 @staticmethod 

723 def _validate_start_and_end(start, end): 

724 """This validates start and end. 

725 

726 Raises: 

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

728 """ 

729 if start is None or end is None: 

730 return 

731 if start > end: 

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

733 

734 @classmethod 

735 def new( 

736 cls, 

737 created: date | None = None, 

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

739 concepts: CONCEPTS_TYPE_SETTER = None, 

740 last_modified: date | None = None, 

741 links: LINKS_TYPE_SETTER = None, 

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

743 related_to: RELATED_TO_TYPE_SETTER = None, 

744 stamp: date | None = None, 

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

746 ) -> Component: 

747 """Create a new component. 

748 

749 Parameters: 

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

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

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

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

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

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

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

757 subcomponents: The subcomponents of the component. 

758 

759 Raises: 

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

761 according to :rfc:`5545`. 

762 

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

764 validation. 

765 """ 

766 component = cls() 

767 component.DTSTAMP = stamp 

768 component.created = created 

769 component.last_modified = last_modified 

770 component.comments = comments 

771 component.links = links 

772 component.related_to = related_to 

773 component.concepts = concepts 

774 component.refids = refids 

775 if subcomponents is not None: 

776 component.subcomponents = ( 

777 subcomponents 

778 if isinstance(subcomponents, list) 

779 else list(subcomponents) 

780 ) 

781 return component 

782 

783 def to_jcal(self) -> list: 

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

785 

786 Returns: 

787 jCal object 

788 

789 See also :attr:`to_json`. 

790 

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

792 

793 .. code-block:: pycon 

794 

795 >>> from icalendar import Event 

796 >>> from datetime import date 

797 >>> from pprint import pprint 

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

799 >>> pprint(event.to_jcal()) 

800 ['vevent', 

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

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

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

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

805 []] 

806 """ 

807 properties = [] 

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

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

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

811 return [ 

812 self.name.lower(), 

813 properties, 

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

815 ] 

816 

817 def to_json(self) -> str: 

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

819 

820 Returns: 

821 JSON string 

822 

823 See also :attr:`to_jcal`. 

824 """ 

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

826 

827 @classmethod 

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

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

830 

831 Parameters: 

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

833 

834 Raises: 

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

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

837 

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

839 

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

841 

842 .. code-block:: pycon 

843 

844 >>> from icalendar import Component 

845 >>> jcal = ["vcalendar", 

846 ... [ 

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

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

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

850 ... ], 

851 ... [ 

852 ... ["vevent", 

853 ... [ 

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

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

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

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

858 ... ], 

859 ... [] 

860 ... ] 

861 ... ] 

862 ... ] 

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

864 >>> print(calendar.name) 

865 VCALENDAR 

866 >>> print(calendar.prodid) 

867 -//Example Inc.//Example Calendar//EN 

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

869 >>> print(event.summary) 

870 Planning meeting 

871 

872 """ 

873 if isinstance(jcal, str): 

874 jcal = json.loads(jcal) 

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

876 raise JCalParsingError( 

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

878 ) 

879 name, properties, subcomponents = jcal 

880 if not isinstance(name, str): 

881 raise JCalParsingError( 

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

883 ) 

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

885 # delegate to correct component class 

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

887 return component_cls.from_jcal(jcal) 

888 component = cls() 

889 if not isinstance(properties, list): 

890 raise JCalParsingError( 

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

892 ) 

893 for i, prop in enumerate(properties): 

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

895 prop_name = prop[0] 

896 prop_value = prop[2] 

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

898 prop_name, prop_value 

899 ) 

900 with JCalParsingError.reraise_with_path_added(1, i): 

901 v_prop = prop_cls.from_jcal(prop) 

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

903 # VALUE parameter 

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

905 del v_prop.VALUE 

906 component.add(prop_name, v_prop) 

907 if not isinstance(subcomponents, list): 

908 raise JCalParsingError( 

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

910 ) 

911 for i, subcomponent in enumerate(subcomponents): 

912 with JCalParsingError.reraise_with_path_added(2, i): 

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

914 return component 

915 

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

917 """Copy the component. 

918 

919 Parameters: 

920 recursive: 

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

922 and all its properties. 

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

924 

925 Returns: 

926 A copy of the component. 

927 

928 Examples: 

929 

930 Create a shallow copy of a component: 

931 

932 .. code-block:: pycon 

933 

934 >>> from icalendar import Event 

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

936 >>> event_copy = event.copy() 

937 >>> str(event_copy.description) 

938 'Event to be copied' 

939 

940 Shallow copies lose their subcomponents: 

941 

942 .. code-block:: pycon 

943 

944 >>> from icalendar import Calendar 

945 >>> calendar = Calendar.example() 

946 >>> len(calendar.subcomponents) 

947 3 

948 >>> calendar_copy = calendar.copy() 

949 >>> len(calendar_copy.subcomponents) 

950 0 

951 

952 A recursive copy also copies all the subcomponents: 

953 

954 .. code-block:: pycon 

955 

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

957 >>> len(full_calendar_copy.subcomponents) 

958 3 

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

960 True 

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

962 False 

963 

964 """ 

965 if recursive: 

966 return deepcopy(self) 

967 return super().copy() 

968 

969 def is_lazy(self) -> bool: 

970 """This component is fully parsed.""" 

971 return False 

972 

973 def parse(self) -> Self: 

974 """Return the fully parsed component. 

975 

976 For non-lazy components, this returns self. 

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

978 """ 

979 return self 

980 

981 

982__all__ = ["Component"]