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"]