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 ) -> None:
289 """Add a property to this component.
290
291 If the property already exists, the new value is appended so the
292 property carries a list of values rather than replacing the previous
293 one. When ``name`` is ``DTSTAMP``, ``CREATED``, or ``LAST-MODIFIED``
294 and ``value`` is a ``datetime``, the value is converted to UTC as the
295 RFC requires.
296
297 Parameters:
298 name: Name of the property.
299 value:
300 Value of the property. Either a basic Python type or any of
301 icalendar's own property types.
302 parameters:
303 Property parameter dictionary for the value. Only consulted
304 when ``encode`` is ``True``.
305 encode:
306 ``True`` if the value should be encoded to one of icalendar's
307 own property types (fallback is ``vText``); ``False`` to
308 store the value as-is.
309
310 Returns:
311 ``None``
312
313 Example:
314
315 >>> from icalendar import Event
316 >>> event = Event()
317 >>> event.add("summary", "Team sync")
318 >>> event["summary"]
319 vText(b'Team sync')
320
321 """
322 if isinstance(value, datetime) and name.lower() in (
323 "dtstamp",
324 "created",
325 "last-modified",
326 ):
327 # RFC expects UTC for those... force value conversion.
328 value = tzp.localize_utc(value)
329
330 # encode value
331 if (
332 encode
333 and isinstance(value, list)
334 and name.lower() not in ["rdate", "exdate", "categories"]
335 ):
336 # Individually convert each value to an ical type except rdate and
337 # exdate, where lists of dates might be passed to vDDDLists.
338 value = [self._encode(name, v, parameters, encode) for v in value]
339 else:
340 value = self._encode(name, value, parameters, encode)
341
342 # set value
343 if name in self:
344 # If property already exists, append it.
345 oldval = self[name]
346 if isinstance(oldval, list):
347 if isinstance(value, list):
348 value = oldval + value
349 else:
350 oldval.append(value)
351 value = oldval
352 else:
353 value = [oldval, value]
354 self[name] = value
355
356 def _decode(self, name: str, value: VPROPERTY):
357 """Internal for decoding property values."""
358
359 # TODO: Currently the decoded method calls the icalendar.prop instances
360 # from_ical. We probably want to decode properties into Python native
361 # types here. But when parsing from an ical string with from_ical, we
362 # want to encode the string into a real icalendar.prop property.
363 if hasattr(value, "ical_value"):
364 return value.ical_value
365 if isinstance(value, vDDDLists):
366 # TODO: Workaround unfinished decoding
367 return value
368 decoded = self.types_factory.from_ical(name, value)
369 # TODO: remove when proper decoded is implemented in every prop.* class
370 # Workaround to decode vText properly
371 if isinstance(decoded, vText):
372 decoded = decoded.encode(DEFAULT_ENCODING)
373 return decoded
374
375 def decoded(self, name: str, default: Any = _marker) -> Any:
376 """Returns decoded value of property.
377
378 A component maps keys to icalendar property value types.
379 This function returns values compatible to native Python types.
380 """
381 if name in self:
382 value = self[name]
383 if isinstance(value, list):
384 return [self._decode(name, v) for v in value]
385 return self._decode(name, value)
386 if default is _marker:
387 raise KeyError(name)
388 return default
389
390 ########################################################################
391 # Inline values. A few properties have multiple values inlined in in one
392 # property line. These methods are used for splitting and joining these.
393
394 def get_inline(self, name, decode=1):
395 """Returns a list of values (split on comma)."""
396 vals = [v.strip('" ') for v in q_split(self[name])]
397 if decode:
398 return [self._decode(name, val) for val in vals]
399 return vals
400
401 def set_inline(self, name, values, encode=1):
402 """Converts a list of values into comma separated string and sets value
403 to that.
404 """
405 if encode:
406 values = [self._encode(name, value, encode=1) for value in values]
407 self[name] = self.types_factory["inline"](q_join(values))
408
409 #########################
410 # Handling of components
411
412 def add_component(self, component: Component) -> None:
413 """Add a subcomponent to this component."""
414 self.subcomponents.append(component)
415
416 def _walk(
417 self, name: str | None, select: callable[[Component], bool]
418 ) -> list[Component]:
419 """Walk to given component."""
420 result = []
421 stack = [self]
422 while stack:
423 component = stack.pop()
424 if (name is None or component.name == name) and select(component):
425 result.append(component)
426 stack.extend(reversed(component.subcomponents))
427 return result
428
429 def walk(
430 self,
431 name: str | None = None,
432 select: callable[[Component], bool] = lambda _: True,
433 ) -> list[Component]:
434 """Recursively traverses component and subcomponents. Returns sequence
435 of same. If name is passed, only components with name will be returned.
436
437 :param name: The name of the component or None such as ``VEVENT``.
438 :param select: A function that takes the component as first argument
439 and returns True/False.
440 :returns: A list of components that match.
441 :rtype: list[Component]
442 """
443 if name is not None:
444 name = name.upper()
445 return self._walk(name, select)
446
447 def with_uid(self, uid: str) -> list[Component]:
448 """Return a list of components with the given UID.
449
450 Parameters:
451 uid: The UID of the component.
452
453 Returns:
454 list[Component]: List of components with the given UID.
455 """
456 return self.walk(select=lambda c: c.uid == uid)
457
458 #####################
459 # Generation
460
461 def property_items(
462 self,
463 recursive: bool = True,
464 sorted: bool = True,
465 ) -> list[tuple[str, object]]:
466 """Returns properties in this component and subcomponents as:
467 [(name, value), ...]
468 """
469 # Iterative implementation to avoid RecursionError
470 result = []
471 v_text = self.types_factory["text"]
472 # Stack stores (component, state)
473 # state: True means we are processing the END of the component
474 # state: False means we are processing the BEGIN and properties of the component
475 stack = [(self, False)]
476 while stack:
477 comp, is_end = stack.pop()
478 if is_end:
479 result.append(("END", v_text(comp.name).to_ical()))
480 else:
481 result.append(("BEGIN", v_text(comp.name).to_ical()))
482 property_names = comp.sorted_keys() if sorted else comp.keys()
483
484 for name in property_names:
485 values = comp[name]
486 if isinstance(values, list):
487 # normally one property is one line
488 for value in values:
489 result.append((name, value))
490 else:
491 result.append((name, values))
492
493 # Push the END marker for this component
494 stack.append((comp, True))
495 # Push subcomponents if recursion is enabled
496 if recursive:
497 # Push in reverse order to maintain original order in result
498 for subcomponent in reversed(comp.subcomponents):
499 stack.append((subcomponent, False))
500
501 return result
502
503 @overload
504 @classmethod
505 def from_ical(
506 cls, st: str | bytes, multiple: Literal[False] = False
507 ) -> Component: ...
508
509 @overload
510 @classmethod
511 def from_ical(cls, st: str | bytes, multiple: Literal[True]) -> list[Component]: ...
512
513 @classmethod
514 def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser:
515 """Get the iCal parser for the given input string."""
516 return ComponentIcalParser(st, cls._get_component_factory(), cls.types_factory)
517
518 @classmethod
519 def from_ical(
520 cls, st: str | bytes | Path, multiple: bool = False
521 ) -> Component | list[Component]:
522 """Parse iCalendar data into component instances.
523
524 Handles standard and custom components (``X-*``, IANA-registered).
525
526 Parameters:
527 st: iCalendar data as bytes or string, or a path to an iCalendar file as
528 :class:`pathlib.Path` or string.
529 multiple: If ``True``, returns list. If ``False``, returns single component.
530
531 Returns:
532 Component or list of components
533
534 See Also:
535 :doc:`/how-to/custom-components` for examples of parsing custom components
536 """
537 if isinstance(st, Path):
538 st = st.read_bytes()
539 elif isinstance(st, str) and "\n" not in st and "\r" not in st:
540 path = Path(st)
541 try:
542 is_file = path.is_file()
543 except OSError:
544 is_file = False
545 if is_file:
546 st = path.read_bytes()
547 parser = cls._get_ical_parser(st)
548 components = parser.parse()
549 if multiple:
550 return components
551 if len(components) > 1:
552 raise ValueError(
553 cls._format_error(
554 "Found multiple components where only one is allowed", st
555 )
556 )
557 if len(components) < 1:
558 raise ValueError(
559 cls._format_error(
560 "Found no components where exactly one is required", st
561 )
562 )
563 return components[0]
564
565 @staticmethod
566 def _format_error(error_description, bad_input, elipsis="[...]"):
567 # there's three character more in the error, ie. ' ' x2 and a ':'
568 max_error_length = 100 - 3
569 if len(error_description) + len(bad_input) + len(elipsis) > max_error_length:
570 truncate_to = max_error_length - len(error_description) - len(elipsis)
571 return f"{error_description}: {bad_input[:truncate_to]} {elipsis}"
572 return f"{error_description}: {bad_input}"
573
574 def content_line(self, name, value, sorted: bool = True):
575 """Returns property as content line."""
576 params = getattr(value, "params", Parameters())
577 return Contentline.from_parts(name, params, value, sorted=sorted)
578
579 def content_lines(self, sorted: bool = True):
580 """Converts the Component and subcomponents into content lines."""
581 contentlines = Contentlines()
582 for name, value in self.property_items(sorted=sorted):
583 cl = self.content_line(name, value, sorted=sorted)
584 contentlines.append(cl)
585 contentlines.append("") # remember the empty string in the end
586 return contentlines
587
588 def to_ical(self, sorted: bool = True):
589 """
590 :param sorted: Whether parameters and properties should be
591 lexicographically sorted.
592 """
593
594 content_lines = self.content_lines(sorted=sorted)
595 return content_lines.to_ical()
596
597 def __repr__(self):
598 """String representation of class with all of its subcomponents.
599
600 Implemented iteratively rather than recursively so that calendars
601 with deeply nested subcomponents do not raise ``RecursionError``.
602 A pathological ``.ics`` payload of only ~13 KB can otherwise nest
603 ``BEGIN:VEVENT`` ~500 levels and crash any caller that performs
604 ``repr()``/``str()``/``f"{cal}"`` on the parsed calendar
605 (e.g. logging, error reporting, debug pages).
606 """
607 # Stack-based traversal. Each frame is one of:
608 # ("open", component) -> emit "Name({props}" and schedule children
609 # ("close",) -> emit ")"
610 # ("comma",) -> emit ", "
611 out: list[str] = []
612 stack: list[tuple] = [("open", self)]
613 while stack:
614 frame = stack.pop()
615 kind = frame[0]
616 if kind == "comma":
617 out.append(", ")
618 elif kind == "close":
619 out.append(")")
620 else: # "open"
621 node = frame[1]
622 if isinstance(node, Component):
623 out.append(f"{node.name or type(node).__name__}({dict(node)}")
624 subs = node.subcomponents
625 if subs:
626 # Defer ")" then push children in reverse so that
627 # popping yields original order, with ", " separators
628 # (the first popped comma serves as the separator
629 # between the component's dict and its first child).
630 stack.append(("close",))
631 for sub in reversed(subs):
632 stack.append(("open", sub))
633 stack.append(("comma",))
634 else:
635 out.append(")")
636 else:
637 # Should not normally occur (subcomponents are Components),
638 # but be safe and fall back to non-recursive str().
639 out.append(str(node))
640 return "".join(out)
641
642 def __eq__(self, other):
643 if len(self.subcomponents) != len(other.subcomponents):
644 return False
645
646 properties_equal = super().__eq__(other)
647 if not properties_equal:
648 return False
649
650 # The subcomponents might not be in the same order,
651 # neither there's a natural key we can sort the subcomponents by nor
652 # are the subcomponent types hashable, so we cant put them in a set to
653 # check for set equivalence. We have to iterate over the subcomponents
654 # and look for each of them in the list.
655 for subcomponent in self.subcomponents:
656 if subcomponent not in other.subcomponents:
657 return False
658
659 # We now know the other component's subcomponents are not a strict subset
660 # of this component's. However, we still need to check the other way around.
661 for subcomponent in other.subcomponents:
662 if subcomponent not in self.subcomponents:
663 return False
664
665 return True
666
667 DTSTAMP = stamp = single_utc_property(
668 "DTSTAMP",
669 """RFC 5545:
670
671 Conformance: This property MUST be included in the "VEVENT",
672 "VTODO", "VJOURNAL", or "VFREEBUSY" calendar components.
673
674 Description: In the case of an iCalendar object that specifies a
675 "METHOD" property, this property specifies the date and time that
676 the instance of the iCalendar object was created. In the case of
677 an iCalendar object that doesn't specify a "METHOD" property, this
678 property specifies the date and time that the information
679 associated with the calendar component was last revised in the
680 calendar store.
681
682 The value MUST be specified in the UTC time format.
683
684 In the case of an iCalendar object that doesn't specify a "METHOD"
685 property, this property is equivalent to the "LAST-MODIFIED"
686 property.
687 """,
688 )
689 LAST_MODIFIED = single_utc_property(
690 "LAST-MODIFIED",
691 """RFC 5545:
692
693 Purpose: This property specifies the date and time that the
694 information associated with the calendar component was last
695 revised in the calendar store.
696
697 Note: This is analogous to the modification date and time for a
698 file in the file system.
699
700 Conformance: This property can be specified in the "VEVENT",
701 "VTODO", "VJOURNAL", or "VTIMEZONE" calendar components.
702 """,
703 )
704
705 @property
706 def last_modified(self) -> datetime:
707 """Datetime when the information associated with the component was last revised.
708
709 Since :attr:`LAST_MODIFIED` is an optional property,
710 this returns :attr:`DTSTAMP` if :attr:`LAST_MODIFIED` is not set.
711 """
712 return self.LAST_MODIFIED or self.DTSTAMP
713
714 @last_modified.setter
715 def last_modified(self, value):
716 self.LAST_MODIFIED = value
717
718 @last_modified.deleter
719 def last_modified(self):
720 del self.LAST_MODIFIED
721
722 @property
723 def created(self) -> datetime:
724 """Datetime when the information associated with the component was created.
725
726 Since :attr:`CREATED` is an optional property,
727 this returns :attr:`DTSTAMP` if :attr:`CREATED` is not set.
728 """
729 return self.CREATED or self.DTSTAMP
730
731 @created.setter
732 def created(self, value):
733 self.CREATED = value
734
735 @created.deleter
736 def created(self):
737 del self.CREATED
738
739 def is_thunderbird(self) -> bool:
740 """Whether this component has attributes that indicate that Mozilla Thunderbird created it."""
741 return any(attr.startswith("X-MOZ-") for attr in self.keys())
742
743 @staticmethod
744 def _utc_now():
745 """Return now as UTC value."""
746 return datetime.now(timezone.utc)
747
748 uid = uid_property
749 comments = comments_property
750 links = links_property
751 related_to = related_to_property
752 concepts = concepts_property
753 refids = refids_property
754
755 CREATED = single_utc_property(
756 "CREATED",
757 """
758 CREATED specifies the date and time that the calendar
759 information was created by the calendar user agent in the calendar
760 store.
761
762 Conformance:
763 The property can be specified once in "VEVENT",
764 "VTODO", or "VJOURNAL" calendar components. The value MUST be
765 specified as a date with UTC time.
766
767 """,
768 )
769
770 _validate_new = True
771
772 @staticmethod
773 def _validate_start_and_end(start, end):
774 """This validates start and end.
775
776 Raises:
777 ~error.InvalidCalendar: If the information is not valid
778 """
779 if start is None or end is None:
780 return
781 if start > end:
782 raise InvalidCalendar("end must be after start")
783
784 @classmethod
785 def new(
786 cls,
787 created: date | None = None,
788 comments: list[str] | str | None = None,
789 concepts: CONCEPTS_TYPE_SETTER = None,
790 last_modified: date | None = None,
791 links: LINKS_TYPE_SETTER = None,
792 refids: list[str] | str | None = None,
793 related_to: RELATED_TO_TYPE_SETTER = None,
794 stamp: date | None = None,
795 subcomponents: Iterable[Component] | None = None,
796 ) -> Component:
797 """Create a new component.
798
799 Parameters:
800 comments: The :attr:`comments` of the component.
801 concepts: The :attr:`concepts` of the component.
802 created: The :attr:`created` of the component.
803 last_modified: The :attr:`last_modified` of the component.
804 links: The :attr:`links` of the component.
805 related_to: The :attr:`related_to` of the component.
806 stamp: The :attr:`DTSTAMP` of the component.
807 subcomponents: The subcomponents of the component.
808
809 Raises:
810 ~error.InvalidCalendar: If the content is not valid
811 according to :rfc:`5545`.
812
813 .. warning:: As time progresses, we will be stricter with the
814 validation.
815 """
816 component = cls()
817 component.DTSTAMP = stamp
818 component.created = created
819 component.last_modified = last_modified
820 component.comments = comments
821 component.links = links
822 component.related_to = related_to
823 component.concepts = concepts
824 component.refids = refids
825 if subcomponents is not None:
826 component.subcomponents = (
827 subcomponents
828 if isinstance(subcomponents, list)
829 else list(subcomponents)
830 )
831 return component
832
833 def to_jcal(self) -> list:
834 """Convert this component to a jCal object.
835
836 Returns:
837 jCal object
838
839 See also :attr:`to_json`.
840
841 In this example, we create a simple VEVENT component and convert it to jCal:
842
843 .. code-block:: pycon
844
845 >>> from icalendar import Event
846 >>> from datetime import date
847 >>> from pprint import pprint
848 >>> event = Event.new(summary="My Event", start=date(2025, 11, 22))
849 >>> pprint(event.to_jcal())
850 ['vevent',
851 [['dtstamp', {}, 'date-time', '2025-05-17T08:06:12Z'],
852 ['summary', {}, 'text', 'My Event'],
853 ['uid', {}, 'text', 'd755cef5-2311-46ed-a0e1-6733c9e15c63'],
854 ['dtstart', {}, 'date', '2025-11-22']],
855 []]
856 """
857 properties = []
858 for key, value in self.items():
859 for item in value if isinstance(value, list) else [value]:
860 properties.append(item.to_jcal(key.lower()))
861 return [
862 self.name.lower(),
863 properties,
864 [subcomponent.to_jcal() for subcomponent in self.subcomponents],
865 ]
866
867 def to_json(self) -> str:
868 """Return this component as a jCal JSON string.
869
870 Returns:
871 JSON string
872
873 See also :attr:`to_jcal`.
874 """
875 return json.dumps(self.to_jcal())
876
877 @classmethod
878 def from_jcal(cls, jcal: str | list) -> Component:
879 """Create a component from a jCal list.
880
881 Parameters:
882 jcal: jCal list or JSON string according to :rfc:`7265`.
883
884 Raises:
885 ~error.JCalParsingError: If the jCal provided is invalid.
886 ~json.JSONDecodeError: If the provided string is not valid JSON.
887
888 This reverses :func:`to_json` and :func:`to_jcal`.
889
890 The following code parses an example from :rfc:`7265`:
891
892 .. code-block:: pycon
893
894 >>> from icalendar import Component
895 >>> jcal = ["vcalendar",
896 ... [
897 ... ["calscale", {}, "text", "GREGORIAN"],
898 ... ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"],
899 ... ["version", {}, "text", "2.0"]
900 ... ],
901 ... [
902 ... ["vevent",
903 ... [
904 ... ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"],
905 ... ["dtstart", {}, "date", "2008-10-06"],
906 ... ["summary", {}, "text", "Planning meeting"],
907 ... ["uid", {}, "text", "4088E990AD89CB3DBB484909"]
908 ... ],
909 ... []
910 ... ]
911 ... ]
912 ... ]
913 >>> calendar = Component.from_jcal(jcal)
914 >>> print(calendar.name)
915 VCALENDAR
916 >>> print(calendar.prodid)
917 -//Example Inc.//Example Calendar//EN
918 >>> event = calendar.events[0]
919 >>> print(event.summary)
920 Planning meeting
921
922 """
923 if isinstance(jcal, str):
924 jcal = json.loads(jcal)
925 if not isinstance(jcal, list) or len(jcal) != 3:
926 raise JCalParsingError(
927 "A component must be a list with 3 items.", cls, value=jcal
928 )
929 name, properties, subcomponents = jcal
930 if not isinstance(name, str):
931 raise JCalParsingError(
932 "The name must be a string.", cls, path=[0], value=name
933 )
934 if name.upper() != cls.name:
935 # delegate to correct component class
936 component_cls = cls.get_component_class(name.upper())
937 return component_cls.from_jcal(jcal)
938 component = cls()
939 if not isinstance(properties, list):
940 raise JCalParsingError(
941 "The properties must be a list.", cls, path=1, value=properties
942 )
943 for i, prop in enumerate(properties):
944 JCalParsingError.validate_property(prop, cls, path=[1, i])
945 prop_name = prop[0]
946 prop_value = prop[2]
947 prop_cls: type[VPROPERTY] = cls.types_factory.for_property(
948 prop_name, prop_value
949 )
950 with JCalParsingError.reraise_with_path_added(1, i):
951 v_prop = prop_cls.from_jcal(prop)
952 # if we use the default value for that property, we can delete the
953 # VALUE parameter
954 if prop_cls == cls.types_factory.for_property(prop_name):
955 del v_prop.VALUE
956 component.add(prop_name, v_prop)
957 if not isinstance(subcomponents, list):
958 raise JCalParsingError(
959 "The subcomponents must be a list.", cls, 2, value=subcomponents
960 )
961 for i, subcomponent in enumerate(subcomponents):
962 with JCalParsingError.reraise_with_path_added(2, i):
963 component.subcomponents.append(cls.from_jcal(subcomponent))
964 return component
965
966 def copy(self, recursive: bool = False) -> Self:
967 """Copy the component.
968
969 Parameters:
970 recursive:
971 If ``True``, this creates copies of the component, its subcomponents,
972 and all its properties.
973 If ``False``, this only creates a shallow copy of the component.
974
975 Returns:
976 A copy of the component.
977
978 Examples:
979
980 Create a shallow copy of a component:
981
982 .. code-block:: pycon
983
984 >>> from icalendar import Event
985 >>> event = Event.new(description="Event to be copied")
986 >>> event_copy = event.copy()
987 >>> str(event_copy.description)
988 'Event to be copied'
989
990 Shallow copies lose their subcomponents:
991
992 .. code-block:: pycon
993
994 >>> from icalendar import Calendar
995 >>> calendar = Calendar.example()
996 >>> len(calendar.subcomponents)
997 3
998 >>> calendar_copy = calendar.copy()
999 >>> len(calendar_copy.subcomponents)
1000 0
1001
1002 A recursive copy also copies all the subcomponents:
1003
1004 .. code-block:: pycon
1005
1006 >>> full_calendar_copy = calendar.copy(recursive=True)
1007 >>> len(full_calendar_copy.subcomponents)
1008 3
1009 >>> full_calendar_copy.events[0] == calendar.events[0]
1010 True
1011 >>> full_calendar_copy.events[0] is calendar.events[0]
1012 False
1013
1014 """
1015 if recursive:
1016 return deepcopy(self)
1017 return super().copy()
1018
1019 def is_lazy(self) -> bool:
1020 """This component is fully parsed."""
1021 return False
1022
1023 def parse(self) -> Self:
1024 """Return the fully parsed component.
1025
1026 For non-lazy components, this returns self.
1027 For lazy components, this parses the component and returns the result.
1028 """
1029 return self
1030
1031
1032__all__ = ["Component"]