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