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 typing import TYPE_CHECKING, Any, ClassVar, Literal, overload
9
10from icalendar.attr import (
11 CONCEPTS_TYPE_SETTER,
12 LINKS_TYPE_SETTER,
13 RELATED_TO_TYPE_SETTER,
14 comments_property,
15 concepts_property,
16 links_property,
17 refids_property,
18 related_to_property,
19 single_utc_property,
20 uid_property,
21)
22from icalendar.cal.component_factory import ComponentFactory
23from icalendar.caselessdict import CaselessDict
24from icalendar.error import InvalidCalendar, JCalParsingError
25from icalendar.parser import (
26 Contentline,
27 Contentlines,
28 Parameters,
29 q_join,
30 q_split,
31)
32from icalendar.parser_tools import DEFAULT_ENCODING
33from icalendar.prop import VPROPERTY, TypesFactory, vDDDLists, vText
34from icalendar.timezone import tzp
35from icalendar.tools import is_date
36
37if TYPE_CHECKING:
38 from icalendar.compatibility import Self
39
40_marker = []
41
42
43class Component(CaselessDict):
44 """Base class for calendar components.
45
46 Component is the base object for calendar, Event and the other
47 components defined in :rfc:`5545`. Normally you will not use this class
48 directly, but rather one of the subclasses.
49 """
50
51 name: ClassVar[str | None] = None
52 """The name of the component.
53
54 This should be defined in each component class.
55
56 Example: ``VCALENDAR``.
57 """
58
59 required: ClassVar[tuple[()]] = ()
60 """These properties are required."""
61
62 singletons: ClassVar[tuple[()]] = ()
63 """These properties must appear only once."""
64
65 multiple: ClassVar[tuple[()]] = ()
66 """These properties may occur more than once."""
67
68 exclusive: ClassVar[tuple[()]] = ()
69 """These properties are mutually exclusive."""
70
71 inclusive: ClassVar[(tuple[str] | tuple[tuple[str, str]])] = ()
72 """These properties are inclusive.
73
74 In other words, if the first property in the tuple occurs, then the
75 second one must also occur.
76
77 Example:
78
79 .. code-block:: python
80
81 ('duration', 'repeat')
82 """
83
84 ignore_exceptions: ClassVar[bool] = False
85 """Whether or not to ignore exceptions when parsing.
86
87 If ``True``, and this component can't be parsed, then it will silently
88 ignore it, rather than let the exception propagate upwards.
89 """
90
91 types_factory: ClassVar[TypesFactory] = TypesFactory.instance()
92 _components_factory: ClassVar[ComponentFactory | None] = None
93
94 subcomponents: list[Component]
95 """All subcomponents of this component."""
96
97 @classmethod
98 def get_component_class(cls, name: str) -> type[Component]:
99 """Return a component with this name.
100
101 Parameters:
102 name: Name of the component, i.e. ``VCALENDAR``
103 """
104 if cls._components_factory is None:
105 cls._components_factory = ComponentFactory()
106 return cls._components_factory.get_component_class(name)
107
108 @classmethod
109 def register(cls, component_class: type[Component]) -> None:
110 """Register a custom component class.
111
112 Parameters:
113 component_class: Component subclass to register.
114 Must have a ``name`` attribute.
115
116 Raises:
117 ValueError: If ``component_class`` has no ``name`` attribute.
118 ValueError: If a component with this name is already registered.
119
120 Examples:
121 Create a custom icalendar component with the name ``X-EXAMPLE``:
122
123 .. code-block:: pycon
124
125 >>> from icalendar import Component
126 >>> class XExample(Component):
127 ... name = "X-EXAMPLE"
128 ... def custom_method(self):
129 ... return "custom"
130 >>> Component.register(XExample)
131 """
132 if not hasattr(component_class, "name") or component_class.name is None:
133 raise ValueError(f"{component_class} must have a 'name' attribute")
134
135 if cls._components_factory is None:
136 cls._components_factory = ComponentFactory()
137
138 # Check if already registered
139 existing = cls._components_factory.get(component_class.name)
140 if existing is not None and existing is not component_class:
141 raise ValueError(
142 f"Component '{component_class.name}' is already registered"
143 f" as {existing}"
144 )
145
146 cls._components_factory.add_component_class(component_class)
147
148 @staticmethod
149 def _infer_value_type(
150 value: date | datetime | timedelta | time | tuple | list,
151 ) -> str | None:
152 """Infer the ``VALUE`` parameter from a Python type.
153
154 Parameters:
155 value: Python native type, one of :py:class:`date`, :py:mod:`datetime`,
156 :py:class:`timedelta`, :py:mod:`time`, :py:class:`tuple`,
157 or :py:class:`list`.
158
159 Returns:
160 str or None: The ``VALUE`` parameter string, for example, "DATE",
161 "TIME", or other string, or ``None``
162 if no specific ``VALUE`` is needed.
163 """
164 if isinstance(value, list):
165 if not value:
166 return None
167 # Check if ALL items are date (but not datetime)
168 if all(is_date(item) for item in value):
169 return "DATE"
170 # Check if ALL items are time
171 if all(isinstance(item, time) for item in value):
172 return "TIME"
173 # Mixed types or other types - don't infer
174 return None
175 if is_date(value):
176 return "DATE"
177 if isinstance(value, time):
178 return "TIME"
179 # Don't infer PERIOD - it's too risky and vPeriod already handles it
180 return None
181
182 def __init__(self, *args, **kwargs):
183 """Set keys to upper for initial dict."""
184 super().__init__(*args, **kwargs)
185 # set parameters here for properties that use non-default values
186 self.subcomponents: list[Component] = [] # Components can be nested.
187 self.errors = [] # If we ignored exception(s) while
188 # parsing a property, contains error strings
189
190 def __bool__(self):
191 """Returns True, CaselessDict would return False if it had no items."""
192 return True
193
194 def __getitem__(self, key):
195 """Get property value from the component dictionary."""
196 return super().__getitem__(key)
197
198 def get(self, key, default=None):
199 """Get property value with default."""
200 try:
201 return self[key]
202 except KeyError:
203 return default
204
205 def is_empty(self):
206 """Returns True if Component has no items or subcomponents, else False."""
207 return bool(not list(self.values()) + self.subcomponents)
208
209 #############################
210 # handling of property values
211
212 @classmethod
213 def _encode(cls, name, value, parameters=None, encode=1):
214 """Encode values to icalendar property values.
215
216 :param name: Name of the property.
217 :type name: string
218
219 :param value: Value of the property. Either of a basic Python type of
220 any of the icalendar's own property types.
221 :type value: Python native type or icalendar property type.
222
223 :param parameters: Property parameter dictionary for the value. Only
224 available, if encode is set to True.
225 :type parameters: Dictionary
226
227 :param encode: True, if the value should be encoded to one of
228 icalendar's own property types (Fallback is "vText")
229 or False, if not.
230 :type encode: Boolean
231
232 :returns: icalendar property value
233 """
234 if not encode:
235 return value
236 if isinstance(value, cls.types_factory.all_types):
237 # Don't encode already encoded values.
238 obj = value
239 else:
240 # Extract VALUE parameter if present, or infer it from the Python type
241 value_param = None
242 if parameters and "VALUE" in parameters:
243 value_param = parameters["VALUE"]
244 elif not isinstance(value, cls.types_factory.all_types):
245 inferred = cls._infer_value_type(value)
246 if inferred:
247 value_param = inferred
248 # Auto-set the VALUE parameter
249 if parameters is None:
250 parameters = {}
251 if "VALUE" not in parameters:
252 parameters["VALUE"] = inferred
253
254 klass = cls.types_factory.for_property(name, value_param)
255 obj = klass(value)
256 if parameters:
257 if not hasattr(obj, "params"):
258 obj.params = Parameters()
259 for key, item in parameters.items():
260 if item is None:
261 if key in obj.params:
262 del obj.params[key]
263 else:
264 obj.params[key] = item
265 return obj
266
267 def add(
268 self,
269 name: str,
270 value,
271 parameters: dict[str, str] | Parameters = None,
272 encode: bool = True,
273 ):
274 """Add a property.
275
276 :param name: Name of the property.
277 :type name: string
278
279 :param value: Value of the property. Either of a basic Python type of
280 any of the icalendar's own property types.
281 :type value: Python native type or icalendar property type.
282
283 :param parameters: Property parameter dictionary for the value. Only
284 available, if encode is set to True.
285 :type parameters: Dictionary
286
287 :param encode: True, if the value should be encoded to one of
288 icalendar's own property types (Fallback is "vText")
289 or False, if not.
290 :type encode: Boolean
291
292 :returns: None
293 """
294 if isinstance(value, datetime) and name.lower() in (
295 "dtstamp",
296 "created",
297 "last-modified",
298 ):
299 # RFC expects UTC for those... force value conversion.
300 value = tzp.localize_utc(value)
301
302 # encode value
303 if (
304 encode
305 and isinstance(value, list)
306 and name.lower() not in ["rdate", "exdate", "categories"]
307 ):
308 # Individually convert each value to an ical type except rdate and
309 # exdate, where lists of dates might be passed to vDDDLists.
310 value = [self._encode(name, v, parameters, encode) for v in value]
311 else:
312 value = self._encode(name, value, parameters, encode)
313
314 # set value
315 if name in self:
316 # If property already exists, append it.
317 oldval = self[name]
318 if isinstance(oldval, list):
319 if isinstance(value, list):
320 value = oldval + value
321 else:
322 oldval.append(value)
323 value = oldval
324 else:
325 value = [oldval, value]
326 self[name] = value
327
328 def _decode(self, name: str, value: VPROPERTY):
329 """Internal for decoding property values."""
330
331 # TODO: Currently the decoded method calls the icalendar.prop instances
332 # from_ical. We probably want to decode properties into Python native
333 # types here. But when parsing from an ical string with from_ical, we
334 # want to encode the string into a real icalendar.prop property.
335 if hasattr(value, "ical_value"):
336 return value.ical_value
337 if isinstance(value, vDDDLists):
338 # TODO: Workaround unfinished decoding
339 return value
340 decoded = self.types_factory.from_ical(name, value)
341 # TODO: remove when proper decoded is implemented in every prop.* class
342 # Workaround to decode vText properly
343 if isinstance(decoded, vText):
344 decoded = decoded.encode(DEFAULT_ENCODING)
345 return decoded
346
347 def decoded(self, name: str, default: Any = _marker) -> Any:
348 """Returns decoded value of property.
349
350 A component maps keys to icalendar property value types.
351 This function returns values compatible to native Python types.
352 """
353 if name in self:
354 value = self[name]
355 if isinstance(value, list):
356 return [self._decode(name, v) for v in value]
357 return self._decode(name, value)
358 if default is _marker:
359 raise KeyError(name)
360 return default
361
362 ########################################################################
363 # Inline values. A few properties have multiple values inlined in in one
364 # property line. These methods are used for splitting and joining these.
365
366 def get_inline(self, name, decode=1):
367 """Returns a list of values (split on comma)."""
368 vals = [v.strip('" ') for v in q_split(self[name])]
369 if decode:
370 return [self._decode(name, val) for val in vals]
371 return vals
372
373 def set_inline(self, name, values, encode=1):
374 """Converts a list of values into comma separated string and sets value
375 to that.
376 """
377 if encode:
378 values = [self._encode(name, value, encode=1) for value in values]
379 self[name] = self.types_factory["inline"](q_join(values))
380
381 #########################
382 # Handling of components
383
384 def add_component(self, component: Component):
385 """Add a subcomponent to this component."""
386 self.subcomponents.append(component)
387
388 def _walk(self, name, select):
389 """Walk to given component."""
390 result = []
391 if (name is None or self.name == name) and select(self):
392 result.append(self)
393 for subcomponent in self.subcomponents:
394 result += subcomponent._walk(name, select)
395 return result
396
397 def walk(self, name=None, select=lambda _: True) -> list[Component]:
398 """Recursively traverses component and subcomponents. Returns sequence
399 of same. If name is passed, only components with name will be returned.
400
401 :param name: The name of the component or None such as ``VEVENT``.
402 :param select: A function that takes the component as first argument
403 and returns True/False.
404 :returns: A list of components that match.
405 :rtype: list[Component]
406 """
407 if name is not None:
408 name = name.upper()
409 return self._walk(name, select)
410
411 def with_uid(self, uid: str) -> list[Component]:
412 """Return a list of components with the given UID.
413
414 Parameters:
415 uid: The UID of the component.
416
417 Returns:
418 list[Component]: List of components with the given UID.
419 """
420 return [c for c in self.walk() if c.get("uid") == uid]
421
422 #####################
423 # Generation
424
425 def property_items(
426 self,
427 recursive=True,
428 sorted: bool = True,
429 ) -> list[tuple[str, object]]:
430 """Returns properties in this component and subcomponents as:
431 [(name, value), ...]
432 """
433 v_text = self.types_factory["text"]
434 properties = [("BEGIN", v_text(self.name).to_ical())]
435 property_names = self.sorted_keys() if sorted else self.keys()
436
437 for name in property_names:
438 values = self[name]
439 if isinstance(values, list):
440 # normally one property is one line
441 for value in values:
442 properties.append((name, value))
443 else:
444 properties.append((name, values))
445 if recursive:
446 # recursion is fun!
447 for subcomponent in self.subcomponents:
448 properties += subcomponent.property_items(sorted=sorted)
449 properties.append(("END", v_text(self.name).to_ical()))
450 return properties
451
452 @overload
453 @classmethod
454 def from_ical(
455 cls, st: str | bytes, multiple: Literal[False] = False
456 ) -> Component: ...
457
458 @overload
459 @classmethod
460 def from_ical(cls, st: str | bytes, multiple: Literal[True]) -> list[Component]: ...
461
462 @classmethod
463 def from_ical(
464 cls, st: str | bytes, multiple: bool = False
465 ) -> Component | list[Component]:
466 """Parse iCalendar data into component instances.
467
468 Handles standard and custom components (``X-*``, IANA-registered).
469
470 Parameters:
471 st: iCalendar data as bytes or string
472 multiple: If ``True``, returns list. If ``False``, returns single component.
473
474 Returns:
475 Component or list of components
476
477 See Also:
478 :doc:`/how-to/custom-components` for examples of parsing custom components
479 """
480 from icalendar.prop import vBroken
481
482 stack = [] # a stack of components
483 comps = []
484 for line in Contentlines.from_ical(st): # raw parsing
485 if not line:
486 continue
487
488 try:
489 name, params, vals = line.parts()
490 except ValueError as e:
491 # if unable to parse a line within a component
492 # that ignores exceptions, mark the component
493 # as broken and skip the line. otherwise raise.
494 component = stack[-1] if stack else None
495 if not component or not component.ignore_exceptions:
496 raise
497 component.errors.append((None, str(e)))
498 continue
499
500 uname = name.upper()
501 # check for start of component
502 if uname == "BEGIN":
503 # try and create one of the components defined in the spec,
504 # otherwise get a general Components for robustness.
505 c_name = vals.upper()
506 c_class = cls.get_component_class(c_name)
507 # If component factory cannot resolve ``c_name``, the generic
508 # ``Component`` class is used which does not have the name set.
509 # That's opposed to the usage of ``cls``, which represents a
510 # more concrete subclass with a name set (e.g. VCALENDAR).
511 component = c_class()
512 if not getattr(component, "name", ""): # undefined components
513 component.name = c_name
514 stack.append(component)
515 # check for end of event
516 elif uname == "END":
517 # we are done adding properties to this component
518 # so pop it from the stack and add it to the new top.
519 if not stack:
520 # The stack is currently empty, the input must be invalid
521 raise ValueError("END encountered without an accompanying BEGIN!")
522
523 component = stack.pop()
524 if not stack: # we are at the end
525 comps.append(component)
526 else:
527 stack[-1].add_component(component)
528 if vals == "VTIMEZONE" and "TZID" in component:
529 tzp.cache_timezone_component(component)
530 # we are adding properties to the current top of the stack
531 else:
532 # Extract VALUE parameter if present
533 value_param = params.get("VALUE") if params else None
534 factory = cls.types_factory.for_property(name, value_param)
535 component = stack[-1] if stack else None
536 if not component:
537 # only accept X-COMMENT at the end of the .ics file
538 # ignore these components in parsing
539 if uname == "X-COMMENT":
540 break
541 raise ValueError(
542 f'Property "{name}" does not have a parent component.'
543 )
544 datetime_names = (
545 "DTSTART",
546 "DTEND",
547 "RECURRENCE-ID",
548 "DUE",
549 "RDATE",
550 "EXDATE",
551 )
552
553 # Determine TZID for datetime properties
554 tzid = params.get("TZID") if params and name in datetime_names else None
555
556 # Handle special cases for value list preparation
557 if name.upper() == "CATEGORIES":
558 # Special handling for CATEGORIES - need raw value
559 # before unescaping to properly split on unescaped commas
560 from icalendar.parser import (
561 split_on_unescaped_comma,
562 )
563
564 line_str = str(line)
565 # Use rfind to get the last colon (value separator)
566 # to handle parameters with colons like ALTREP="http://..."
567 colon_idx = line_str.rfind(":")
568 if colon_idx > 0:
569 raw_value = line_str[colon_idx + 1 :]
570 # Parse categories immediately (not lazily) for both
571 # strict and tolerant components.
572 # CATEGORIES needs special comma handling
573 try:
574 category_list = split_on_unescaped_comma(raw_value)
575 vals_inst = factory(category_list)
576 vals_inst.params = params
577 component.add(name, vals_inst, encode=0)
578 except ValueError as e:
579 if not component.ignore_exceptions:
580 raise
581 component.errors.append((uname, str(e)))
582 continue
583 # Fallback to normal processing if we can't find colon
584 vals_list = [vals]
585 elif name == "FREEBUSY":
586 # Handle FREEBUSY comma-separated values
587 vals_list = vals.split(",")
588 # Workaround broken ICS files with empty RDATE
589 # (not EXDATE - let it parse and fail)
590 elif name == "RDATE" and vals == "":
591 vals_list = []
592 else:
593 vals_list = [vals]
594
595 # Parse all properties eagerly
596 for val in vals_list:
597 try:
598 if tzid:
599 parsed_val = factory.from_ical(val, tzid)
600 else:
601 parsed_val = factory.from_ical(val)
602 vals_inst = factory(parsed_val)
603 vals_inst.params = params
604 component.add(name, vals_inst, encode=0)
605 except Exception as e:
606 if not component.ignore_exceptions:
607 raise
608 # Error-tolerant mode: create vBroken
609 expected_type = getattr(factory, "__name__", "unknown")
610 broken_prop = vBroken.from_parse_error(
611 raw_value=val,
612 params=params,
613 property_name=name,
614 expected_type=expected_type,
615 error=e,
616 )
617 component.errors.append((name, str(e)))
618 component.add(name, broken_prop, encode=0)
619
620 if multiple:
621 return comps
622 if len(comps) > 1:
623 raise ValueError(
624 cls._format_error(
625 "Found multiple components where only one is allowed", st
626 )
627 )
628 if len(comps) < 1:
629 raise ValueError(
630 cls._format_error(
631 "Found no components where exactly one is required", st
632 )
633 )
634 return comps[0]
635
636 @staticmethod
637 def _format_error(error_description, bad_input, elipsis="[...]"):
638 # there's three character more in the error, ie. ' ' x2 and a ':'
639 max_error_length = 100 - 3
640 if len(error_description) + len(bad_input) + len(elipsis) > max_error_length:
641 truncate_to = max_error_length - len(error_description) - len(elipsis)
642 return f"{error_description}: {bad_input[:truncate_to]} {elipsis}"
643 return f"{error_description}: {bad_input}"
644
645 def content_line(self, name, value, sorted: bool = True):
646 """Returns property as content line."""
647 params = getattr(value, "params", Parameters())
648 return Contentline.from_parts(name, params, value, sorted=sorted)
649
650 def content_lines(self, sorted: bool = True):
651 """Converts the Component and subcomponents into content lines."""
652 contentlines = Contentlines()
653 for name, value in self.property_items(sorted=sorted):
654 cl = self.content_line(name, value, sorted=sorted)
655 contentlines.append(cl)
656 contentlines.append("") # remember the empty string in the end
657 return contentlines
658
659 def to_ical(self, sorted: bool = True):
660 """
661 :param sorted: Whether parameters and properties should be
662 lexicographically sorted.
663 """
664
665 content_lines = self.content_lines(sorted=sorted)
666 return content_lines.to_ical()
667
668 def __repr__(self):
669 """String representation of class with all of it's subcomponents."""
670 subs = ", ".join(str(it) for it in self.subcomponents)
671 return (
672 f"{self.name or type(self).__name__}"
673 f"({dict(self)}{', ' + subs if subs else ''})"
674 )
675
676 def __eq__(self, other):
677 if len(self.subcomponents) != len(other.subcomponents):
678 return False
679
680 properties_equal = super().__eq__(other)
681 if not properties_equal:
682 return False
683
684 # The subcomponents might not be in the same order,
685 # neither there's a natural key we can sort the subcomponents by nor
686 # are the subcomponent types hashable, so we cant put them in a set to
687 # check for set equivalence. We have to iterate over the subcomponents
688 # and look for each of them in the list.
689 for subcomponent in self.subcomponents:
690 if subcomponent not in other.subcomponents:
691 return False
692
693 return True
694
695 DTSTAMP = stamp = single_utc_property(
696 "DTSTAMP",
697 """RFC 5545:
698
699 Conformance: This property MUST be included in the "VEVENT",
700 "VTODO", "VJOURNAL", or "VFREEBUSY" calendar components.
701
702 Description: In the case of an iCalendar object that specifies a
703 "METHOD" property, this property specifies the date and time that
704 the instance of the iCalendar object was created. In the case of
705 an iCalendar object that doesn't specify a "METHOD" property, this
706 property specifies the date and time that the information
707 associated with the calendar component was last revised in the
708 calendar store.
709
710 The value MUST be specified in the UTC time format.
711
712 In the case of an iCalendar object that doesn't specify a "METHOD"
713 property, this property is equivalent to the "LAST-MODIFIED"
714 property.
715 """,
716 )
717 LAST_MODIFIED = single_utc_property(
718 "LAST-MODIFIED",
719 """RFC 5545:
720
721 Purpose: This property specifies the date and time that the
722 information associated with the calendar component was last
723 revised in the calendar store.
724
725 Note: This is analogous to the modification date and time for a
726 file in the file system.
727
728 Conformance: This property can be specified in the "VEVENT",
729 "VTODO", "VJOURNAL", or "VTIMEZONE" calendar components.
730 """,
731 )
732
733 @property
734 def last_modified(self) -> datetime:
735 """Datetime when the information associated with the component was last revised.
736
737 Since :attr:`LAST_MODIFIED` is an optional property,
738 this returns :attr:`DTSTAMP` if :attr:`LAST_MODIFIED` is not set.
739 """
740 return self.LAST_MODIFIED or self.DTSTAMP
741
742 @last_modified.setter
743 def last_modified(self, value):
744 self.LAST_MODIFIED = value
745
746 @last_modified.deleter
747 def last_modified(self):
748 del self.LAST_MODIFIED
749
750 @property
751 def created(self) -> datetime:
752 """Datetime when the information associated with the component was created.
753
754 Since :attr:`CREATED` is an optional property,
755 this returns :attr:`DTSTAMP` if :attr:`CREATED` is not set.
756 """
757 return self.CREATED or self.DTSTAMP
758
759 @created.setter
760 def created(self, value):
761 self.CREATED = value
762
763 @created.deleter
764 def created(self):
765 del self.CREATED
766
767 def is_thunderbird(self) -> bool:
768 """Whether this component has attributes that indicate that Mozilla Thunderbird created it."""
769 return any(attr.startswith("X-MOZ-") for attr in self.keys())
770
771 @staticmethod
772 def _utc_now():
773 """Return now as UTC value."""
774 return datetime.now(timezone.utc)
775
776 uid = uid_property
777 comments = comments_property
778 links = links_property
779 related_to = related_to_property
780 concepts = concepts_property
781 refids = refids_property
782
783 CREATED = single_utc_property(
784 "CREATED",
785 """
786 CREATED specifies the date and time that the calendar
787 information was created by the calendar user agent in the calendar
788 store.
789
790 Conformance:
791 The property can be specified once in "VEVENT",
792 "VTODO", or "VJOURNAL" calendar components. The value MUST be
793 specified as a date with UTC time.
794
795 """,
796 )
797
798 _validate_new = True
799
800 @staticmethod
801 def _validate_start_and_end(start, end):
802 """This validates start and end.
803
804 Raises:
805 ~error.InvalidCalendar: If the information is not valid
806 """
807 if start is None or end is None:
808 return
809 if start > end:
810 raise InvalidCalendar("end must be after start")
811
812 @classmethod
813 def new(
814 cls,
815 created: date | None = None,
816 comments: list[str] | str | None = None,
817 concepts: CONCEPTS_TYPE_SETTER = None,
818 last_modified: date | None = None,
819 links: LINKS_TYPE_SETTER = None,
820 refids: list[str] | str | None = None,
821 related_to: RELATED_TO_TYPE_SETTER = None,
822 stamp: date | None = None,
823 ) -> Component:
824 """Create a new component.
825
826 Parameters:
827 comments: The :attr:`comments` of the component.
828 concepts: The :attr:`concepts` of the component.
829 created: The :attr:`created` of the component.
830 last_modified: The :attr:`last_modified` of the component.
831 links: The :attr:`links` of the component.
832 related_to: The :attr:`related_to` of the component.
833 stamp: The :attr:`DTSTAMP` of the component.
834
835 Raises:
836 ~error.InvalidCalendar: If the content is not valid
837 according to :rfc:`5545`.
838
839 .. warning:: As time progresses, we will be stricter with the
840 validation.
841 """
842 component = cls()
843 component.DTSTAMP = stamp
844 component.created = created
845 component.last_modified = last_modified
846 component.comments = comments
847 component.links = links
848 component.related_to = related_to
849 component.concepts = concepts
850 component.refids = refids
851 return component
852
853 def to_jcal(self) -> list:
854 """Convert this component to a jCal object.
855
856 Returns:
857 jCal object
858
859 See also :attr:`to_json`.
860
861 In this example, we create a simple VEVENT component and convert it to jCal:
862
863 .. code-block:: pycon
864
865 >>> from icalendar import Event
866 >>> from datetime import date
867 >>> from pprint import pprint
868 >>> event = Event.new(summary="My Event", start=date(2025, 11, 22))
869 >>> pprint(event.to_jcal())
870 ['vevent',
871 [['dtstamp', {}, 'date-time', '2025-05-17T08:06:12Z'],
872 ['summary', {}, 'text', 'My Event'],
873 ['uid', {}, 'text', 'd755cef5-2311-46ed-a0e1-6733c9e15c63'],
874 ['dtstart', {}, 'date', '2025-11-22']],
875 []]
876 """
877 properties = []
878 for key, value in self.items():
879 for item in value if isinstance(value, list) else [value]:
880 properties.append(item.to_jcal(key.lower()))
881 return [
882 self.name.lower(),
883 properties,
884 [subcomponent.to_jcal() for subcomponent in self.subcomponents],
885 ]
886
887 def to_json(self) -> str:
888 """Return this component as a jCal JSON string.
889
890 Returns:
891 JSON string
892
893 See also :attr:`to_jcal`.
894 """
895 return json.dumps(self.to_jcal())
896
897 @classmethod
898 def from_jcal(cls, jcal: str | list) -> Component:
899 """Create a component from a jCal list.
900
901 Parameters:
902 jcal: jCal list or JSON string according to :rfc:`7265`.
903
904 Raises:
905 ~error.JCalParsingError: If the jCal provided is invalid.
906 ~json.JSONDecodeError: If the provided string is not valid JSON.
907
908 This reverses :func:`to_json` and :func:`to_jcal`.
909
910 The following code parses an example from :rfc:`7265`:
911
912 .. code-block:: pycon
913
914 >>> from icalendar import Component
915 >>> jcal = ["vcalendar",
916 ... [
917 ... ["calscale", {}, "text", "GREGORIAN"],
918 ... ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"],
919 ... ["version", {}, "text", "2.0"]
920 ... ],
921 ... [
922 ... ["vevent",
923 ... [
924 ... ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"],
925 ... ["dtstart", {}, "date", "2008-10-06"],
926 ... ["summary", {}, "text", "Planning meeting"],
927 ... ["uid", {}, "text", "4088E990AD89CB3DBB484909"]
928 ... ],
929 ... []
930 ... ]
931 ... ]
932 ... ]
933 >>> calendar = Component.from_jcal(jcal)
934 >>> print(calendar.name)
935 VCALENDAR
936 >>> print(calendar.prodid)
937 -//Example Inc.//Example Calendar//EN
938 >>> event = calendar.events[0]
939 >>> print(event.summary)
940 Planning meeting
941
942 """
943 if isinstance(jcal, str):
944 jcal = json.loads(jcal)
945 if not isinstance(jcal, list) or len(jcal) != 3:
946 raise JCalParsingError(
947 "A component must be a list with 3 items.", cls, value=jcal
948 )
949 name, properties, subcomponents = jcal
950 if not isinstance(name, str):
951 raise JCalParsingError(
952 "The name must be a string.", cls, path=[0], value=name
953 )
954 if name.upper() != cls.name:
955 # delegate to correct component class
956 component_cls = cls.get_component_class(name.upper())
957 return component_cls.from_jcal(jcal)
958 component = cls()
959 if not isinstance(properties, list):
960 raise JCalParsingError(
961 "The properties must be a list.", cls, path=1, value=properties
962 )
963 for i, prop in enumerate(properties):
964 JCalParsingError.validate_property(prop, cls, path=[1, i])
965 prop_name = prop[0]
966 prop_value = prop[2]
967 prop_cls: type[VPROPERTY] = cls.types_factory.for_property(
968 prop_name, prop_value
969 )
970 with JCalParsingError.reraise_with_path_added(1, i):
971 v_prop = prop_cls.from_jcal(prop)
972 # if we use the default value for that property, we can delete the
973 # VALUE parameter
974 if prop_cls == cls.types_factory.for_property(prop_name):
975 del v_prop.VALUE
976 component.add(prop_name, v_prop)
977 if not isinstance(subcomponents, list):
978 raise JCalParsingError(
979 "The subcomponents must be a list.", cls, 2, value=subcomponents
980 )
981 for i, subcomponent in enumerate(subcomponents):
982 with JCalParsingError.reraise_with_path_added(2, i):
983 component.subcomponents.append(cls.from_jcal(subcomponent))
984 return component
985
986 def copy(self, recursive: bool = False) -> Self:
987 """Copy the component.
988
989 Parameters:
990 recursive:
991 If ``True``, this creates copies of the component, its subcomponents,
992 and all its properties.
993 If ``False``, this only creates a shallow copy of the component.
994
995 Returns:
996 A copy of the component.
997
998 Examples:
999
1000 Create a shallow copy of a component:
1001
1002 .. code-block:: pycon
1003
1004 >>> from icalendar import Event
1005 >>> event = Event.new(description="Event to be copied")
1006 >>> event_copy = event.copy()
1007 >>> str(event_copy.description)
1008 'Event to be copied'
1009
1010 Shallow copies lose their subcomponents:
1011
1012 .. code-block:: pycon
1013
1014 >>> from icalendar import Calendar
1015 >>> calendar = Calendar.example()
1016 >>> len(calendar.subcomponents)
1017 3
1018 >>> calendar_copy = calendar.copy()
1019 >>> len(calendar_copy.subcomponents)
1020 0
1021
1022 A recursive copy also copies all the subcomponents:
1023
1024 .. code-block:: pycon
1025
1026 >>> full_calendar_copy = calendar.copy(recursive=True)
1027 >>> len(full_calendar_copy.subcomponents)
1028 3
1029 >>> full_calendar_copy.events[0] == calendar.events[0]
1030 True
1031 >>> full_calendar_copy.events[0] is calendar.events[0]
1032 False
1033
1034 """
1035 if recursive:
1036 return deepcopy(self)
1037 return super().copy()
1038
1039
1040__all__ = ["Component"]