"""The base for :rfc:`5545` components."""
from __future__ import annotations
import json
from copy import deepcopy
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta, timezone
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload
from icalendar.attr import (
CONCEPTS_TYPE_SETTER,
LINKS_TYPE_SETTER,
RELATED_TO_TYPE_SETTER,
comments_property,
concepts_property,
links_property,
refids_property,
related_to_property,
single_utc_property,
uid_property,
)
from icalendar.cal.component_factory import ComponentFactory
from icalendar.caselessdict import CaselessDict
from icalendar.error import InvalidCalendar, JCalParsingError
from icalendar.parser import (
Contentline,
Contentlines,
Parameters,
q_join,
q_split,
)
from icalendar.parser.ical.component import ComponentIcalParser
from icalendar.parser_tools import DEFAULT_ENCODING
from icalendar.prop import VPROPERTY, TypesFactory, vDDDLists, vText
from icalendar.timezone import tzp
from icalendar.tools import is_date
if TYPE_CHECKING:
from collections.abc import Iterable
from icalendar.compatibility import Self
_marker = []
@dataclass
class _ComponentEqFrame:
"""A pending component-equality comparison on the iterative stack.
See ``Component.__eq__`` for how the fields are used.
"""
#: the two components being compared
a: Component
b: Component
#: ``b``'s subcomponents not yet matched against one of ``a``'s. ``None``
#: until ``a`` and ``b``'s own properties have been compared and found equal.
unmatched: list | None = None
#: index of the ``a`` subcomponent we are currently trying to match
a_index: int = 0
#: index of the unmatched ``b`` subcomponent we are currently testing
candidate_index: int = 0
[docs]
class Component(CaselessDict):
"""Base class for calendar components.
Component is the base object for calendar, Event and the other
components defined in :rfc:`5545`. Normally you will not use this class
directly, but rather one of the subclasses.
"""
name: ClassVar[str | None] = None
"""The name of the component.
This is defined in each component class.
Example:
.. code-block:: pycon
>>> from icalendar import Calendar
>>> cal = Calendar.new()
>>> cal.name
'VCALENDAR'
"""
required: ClassVar[tuple[()]] = ()
"""These properties are required."""
singletons: ClassVar[tuple[()]] = ()
"""These properties must appear only once."""
multiple: ClassVar[tuple[()]] = ()
"""These properties may occur more than once."""
exclusive: ClassVar[tuple[()]] = ()
"""These properties are mutually exclusive."""
inclusive: ClassVar[(tuple[str] | tuple[tuple[str, str]])] = ()
"""These properties are inclusive.
In other words, if the first property in the tuple occurs, then the
second one must also occur.
Example:
.. code-block:: python
('duration', 'repeat')
"""
ignore_exceptions: ClassVar[bool] = False
"""Whether or not to ignore exceptions when parsing.
If ``True``, and this component can't be parsed, then it will silently
ignore it, rather than let the exception propagate upwards.
"""
types_factory: ClassVar[TypesFactory] = TypesFactory.instance()
_components_factory: ClassVar[ComponentFactory | None] = None
subcomponents: list[Component]
"""All subcomponents of this component."""
@classmethod
def _get_component_factory(cls) -> ComponentFactory:
"""Get the component factory."""
if cls._components_factory is None:
cls._components_factory = ComponentFactory()
return cls._components_factory
[docs]
@classmethod
def get_component_class(cls, name: str) -> type[Component]:
"""Return a component with this name.
Parameters:
name: Name of the component, i.e. ``VCALENDAR``
"""
return cls._get_component_factory().get_component_class(name)
[docs]
@classmethod
def register(cls, component_class: type[Component]) -> None:
"""Register a custom component class.
Parameters:
component_class: Component subclass to register.
Must have a ``name`` attribute.
Raises:
ValueError: If ``component_class`` has no ``name`` attribute.
ValueError: If a component with this name is already registered.
Examples:
Create a custom icalendar component with the name ``X-EXAMPLE``:
.. code-block:: pycon
>>> from icalendar import Component
>>> class XExample(Component):
... name = "X-EXAMPLE"
... def custom_method(self):
... return "custom"
>>> Component.register(XExample)
"""
if not hasattr(component_class, "name") or component_class.name is None:
raise ValueError(f"{component_class} must have a 'name' attribute")
# Check if already registered
component_factory = cls._get_component_factory()
existing = component_factory.get(component_class.name)
if existing is not None and existing is not component_class:
raise ValueError(
f"Component '{component_class.name}' is already registered"
f" as {existing}"
)
component_factory.add_component_class(component_class)
@staticmethod
def _infer_value_type(
value: date | datetime | timedelta | time | tuple | list,
) -> str | None:
"""Infer the ``VALUE`` parameter from a Python type.
Parameters:
value: Python native type, one of :class:`datetime.date`, :class:`datetime.datetime`,
:class:`datetime.timedelta`, :class:`datetime.time`, :class:`tuple`,
or :class:`list`.
Returns:
str or None: The ``VALUE`` parameter string, for example, "DATE",
"TIME", or other string, or ``None``
if no specific ``VALUE`` is needed.
"""
if isinstance(value, list):
if not value:
return None
# Check if ALL items are date (but not datetime)
if all(is_date(item) for item in value):
return "DATE"
# Check if ALL items are time
if all(isinstance(item, time) for item in value):
return "TIME"
# Mixed types or other types - don't infer
return None
if is_date(value):
return "DATE"
if isinstance(value, time):
return "TIME"
# Don't infer PERIOD - it's too risky and vPeriod already handles it
return None
def __init__(self, *args, **kwargs):
"""Set keys to upper for initial dict."""
super().__init__(*args, **kwargs)
# set parameters here for properties that use non-default values
self.subcomponents: list[Component] = [] # Components can be nested.
self.errors = [] # If we ignored exception(s) while
# parsing a property, contains error strings
def __bool__(self) -> bool:
"""Returns True, CaselessDict would return False if it had no items."""
return True
def __getitem__(self, key) -> VPROPERTY:
"""Get property value from the component dictionary."""
return super().__getitem__(key)
[docs]
def get(self, key, default=None) -> Any:
"""Get property value with default."""
try:
return self[key]
except KeyError:
return default
[docs]
def is_empty(self) -> bool:
"""Returns True if Component has no items or subcomponents, else False."""
return bool(not list(self.values()) + self.subcomponents)
#############################
# handling of property values
@classmethod
def _encode(cls, name, value, parameters=None, encode=1):
"""Encode values to icalendar property values.
:param name: Name of the property.
:type name: string
:param value: Value of the property. Either of a basic Python type of
any of the icalendar's own property types.
:type value: Python native type or icalendar property type.
:param parameters: Property parameter dictionary for the value. Only
available, if encode is set to True.
:type parameters: Dictionary
:param encode: True, if the value should be encoded to one of
icalendar's own property types (Fallback is "vText")
or False, if not.
:type encode: Boolean
:returns: icalendar property value
"""
if not encode:
return value
if isinstance(value, cls.types_factory.all_types):
# Don't encode already encoded values.
obj = value
else:
# Extract VALUE parameter if present, or infer it from the Python type
value_param = None
if parameters and "VALUE" in parameters:
value_param = parameters["VALUE"]
elif not isinstance(value, cls.types_factory.all_types):
inferred = cls._infer_value_type(value)
if inferred:
value_param = inferred
# Auto-set the VALUE parameter
if parameters is None:
parameters = {}
if "VALUE" not in parameters:
parameters["VALUE"] = inferred
klass = cls.types_factory.for_property(name, value_param)
obj = klass(value)
if parameters:
if not hasattr(obj, "params"):
obj.params = Parameters()
for key, item in parameters.items():
if item is None:
if key in obj.params:
del obj.params[key]
else:
obj.params[key] = item
return obj
[docs]
def add(
self,
name: str,
value,
parameters: dict[str, str] | Parameters = None,
encode: bool = True,
) -> None:
"""Add a property to this component.
If the property already exists, the new value is appended so the
property carries a list of values rather than replacing the previous
one. When ``name`` is ``DTSTAMP``, ``CREATED``, or ``LAST-MODIFIED``
and ``value`` is a ``datetime``, the value is converted to UTC as the
RFC requires.
Parameters:
name: Name of the property.
value:
Value of the property. Either a basic Python type or any of
icalendar's own property types.
parameters:
Property parameter dictionary for the value. Only consulted
when ``encode`` is ``True``.
encode:
``True`` if the value should be encoded to one of icalendar's
own property types (fallback is ``vText``); ``False`` to
store the value as-is.
Returns:
``None``
Example:
>>> from icalendar import Event
>>> event = Event()
>>> event.add("summary", "Team sync")
>>> event["summary"]
vText(b'Team sync')
"""
if isinstance(value, datetime) and name.lower() in (
"dtstamp",
"created",
"last-modified",
):
# RFC expects UTC for those... force value conversion.
value = tzp.localize_utc(value)
# encode value
if (
encode
and isinstance(value, list)
and name.lower() not in ["rdate", "exdate", "categories"]
):
# Individually convert each value to an ical type except rdate and
# exdate, where lists of dates might be passed to vDDDLists.
value = [self._encode(name, v, parameters, encode) for v in value]
else:
value = self._encode(name, value, parameters, encode)
# set value
if name in self:
# If property already exists, append it.
oldval = self[name]
if isinstance(oldval, list):
if isinstance(value, list):
value = oldval + value
else:
oldval.append(value)
value = oldval
else:
value = [oldval, value]
self[name] = value
def _decode(self, name: str, value: VPROPERTY):
"""Internal for decoding property values."""
# TODO: Currently the decoded method calls the icalendar.prop instances
# from_ical. We probably want to decode properties into Python native
# types here. But when parsing from an ical string with from_ical, we
# want to encode the string into a real icalendar.prop property.
if hasattr(value, "ical_value"):
return value.ical_value
if isinstance(value, vDDDLists):
# TODO: Workaround unfinished decoding
return value
decoded = self.types_factory.from_ical(name, value)
# TODO: remove when proper decoded is implemented in every prop.* class
# Workaround to decode vText properly
if isinstance(decoded, vText):
decoded = decoded.encode(DEFAULT_ENCODING)
return decoded
[docs]
def decoded(self, name: str, default: Any = _marker) -> Any:
"""Returns decoded value of property.
A component maps keys to icalendar property value types.
This function returns values compatible to native Python types.
"""
if name in self:
value = self[name]
if isinstance(value, list):
return [self._decode(name, v) for v in value]
return self._decode(name, value)
if default is _marker:
raise KeyError(name)
return default
########################################################################
# Inline values. A few properties have multiple values inlined in in one
# property line. These methods are used for splitting and joining these.
[docs]
def get_inline(self, name, decode=1):
"""Returns a list of values (split on comma)."""
vals = [v.strip('" ') for v in q_split(self[name])]
if decode:
return [self._decode(name, val) for val in vals]
return vals
[docs]
def set_inline(self, name, values, encode=1):
"""Converts a list of values into comma separated string and sets value
to that.
"""
if encode:
values = [self._encode(name, value, encode=1) for value in values]
self[name] = self.types_factory["inline"](q_join(values))
#########################
# Handling of components
[docs]
def add_component(self, component: Component) -> None:
"""Add a subcomponent to this component."""
self.subcomponents.append(component)
def _walk(
self, name: str | None, select: callable[[Component], bool]
) -> list[Component]:
"""Walk to given component."""
result = []
stack = [self]
while stack:
component = stack.pop()
if (name is None or component.name == name) and select(component):
result.append(component)
stack.extend(reversed(component.subcomponents))
return result
[docs]
def walk(
self,
name: str | None = None,
select: callable[[Component], bool] = lambda _: True,
) -> list[Component]:
"""Recursively traverses component and subcomponents. Returns sequence
of same. If name is passed, only components with name will be returned.
:param name: The name of the component or None such as ``VEVENT``.
:param select: A function that takes the component as first argument
and returns True/False.
:returns: A list of components that match.
:rtype: list[Component]
"""
if name is not None:
name = name.upper()
return self._walk(name, select)
[docs]
def with_uid(self, uid: str) -> list[Component]:
"""Return a list of components with the given UID.
Parameters:
uid: The UID of the component.
Returns:
list[Component]: List of components with the given UID.
"""
return self.walk(select=lambda c: c.uid == uid)
#####################
# Generation
[docs]
def property_items(
self,
recursive: bool = True,
sorted: bool = True,
) -> list[tuple[str, object]]:
"""Returns properties in this component and subcomponents as:
[(name, value), ...]
"""
# Iterative implementation to avoid RecursionError
result = []
v_text = self.types_factory["text"]
# Stack stores (component, state)
# state: True means we are processing the END of the component
# state: False means we are processing the BEGIN and properties of the component
stack = [(self, False)]
while stack:
comp, is_end = stack.pop()
if is_end:
result.append(("END", v_text(comp.name).to_ical()))
else:
result.append(("BEGIN", v_text(comp.name).to_ical()))
property_names = comp.sorted_keys() if sorted else comp.keys()
for name in property_names:
values = comp[name]
if isinstance(values, list):
# normally one property is one line
for value in values:
result.append((name, value))
else:
result.append((name, values))
# Push the END marker for this component
stack.append((comp, True))
# Push subcomponents if recursion is enabled
if recursive:
# Push in reverse order to maintain original order in result
for subcomponent in reversed(comp.subcomponents):
stack.append((subcomponent, False))
return result
@overload
@classmethod
def from_ical(
cls, st: str | bytes, multiple: Literal[False] = False
) -> Component: ...
@overload
@classmethod
def from_ical(cls, st: str | bytes, multiple: Literal[True]) -> list[Component]: ...
@classmethod
def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser:
"""Get the iCal parser for the given input string."""
return ComponentIcalParser(st, cls._get_component_factory(), cls.types_factory)
[docs]
@classmethod
def from_ical(
cls, st: str | bytes | Path, multiple: bool = False
) -> Component | list[Component]:
"""Parse iCalendar data into component instances.
Handles standard and custom components (``X-*``, IANA-registered).
Parameters:
st: iCalendar data as bytes or string, or a path to an iCalendar file as
:class:`pathlib.Path` or string.
multiple: If ``True``, returns list. If ``False``, returns single component.
Returns:
Component or list of components
See Also:
:doc:`/how-to/custom-components` for examples of parsing custom components
"""
if isinstance(st, Path):
st = st.read_bytes()
elif isinstance(st, str) and "\n" not in st and "\r" not in st:
# A string is only probed as a file path when it contains no line
# breaks. Valid iCalendar data is always folded with CRLF line
# endings (RFC 5545), so real calendar content never reaches this
# branch and is never read from disk. File paths, conversely, do
# not contain line breaks on the platforms we support.
try:
is_file = Path(st).is_file()
except (OSError, ValueError):
# The string is not usable as a path on this platform (e.g. it
# is too long, or contains characters the OS rejects such as an
# embedded null byte). Treat it as calendar data, not a file, so
# the parser raises a consistent ValueError across platforms.
is_file = False
if is_file:
st = Path(st).read_bytes()
parser = cls._get_ical_parser(st)
components = parser.parse()
if multiple:
return components
if len(components) > 1:
raise ValueError(
cls._format_error(
"Found multiple components where only one is allowed", st
)
)
if len(components) < 1:
raise ValueError(
cls._format_error(
"Found no components where exactly one is required", st
)
)
return components[0]
@staticmethod
def _format_error(error_description, bad_input, elipsis="[...]"):
# there's three character more in the error, ie. ' ' x2 and a ':'
max_error_length = 100 - 3
if len(error_description) + len(bad_input) + len(elipsis) > max_error_length:
truncate_to = max_error_length - len(error_description) - len(elipsis)
return f"{error_description}: {bad_input[:truncate_to]} {elipsis}"
return f"{error_description}: {bad_input}"
[docs]
def content_line(self, name, value, sorted: bool = True):
"""Returns property as content line."""
params = getattr(value, "params", Parameters())
return Contentline.from_parts(name, params, value, sorted=sorted)
[docs]
def content_lines(self, sorted: bool = True):
"""Converts the Component and subcomponents into content lines."""
contentlines = Contentlines()
for name, value in self.property_items(sorted=sorted):
cl = self.content_line(name, value, sorted=sorted)
contentlines.append(cl)
contentlines.append("") # remember the empty string in the end
return contentlines
[docs]
def to_ical(self, sorted: bool = True):
"""
:param sorted: Whether parameters and properties should be
lexicographically sorted.
"""
content_lines = self.content_lines(sorted=sorted)
return content_lines.to_ical()
def __repr__(self) -> str:
"""String representation of class with all of its subcomponents.
Implemented iteratively rather than recursively so that calendars
with deeply nested subcomponents do not raise ``RecursionError``.
A pathological ``.ics`` payload of only ~13 KB can otherwise nest
``BEGIN:VEVENT`` ~500 levels and crash any caller that performs
``repr()``/``str()``/``f"{cal}"`` on the parsed calendar
(e.g. logging, error reporting, debug pages).
"""
# Stack-based traversal. Each frame is one of:
# ("open", component) -> emit "Name({props}" and schedule children
# ("close",) -> emit ")"
# ("comma",) -> emit ", "
out: list[str] = []
stack: list[tuple] = [("open", self)]
while stack:
frame = stack.pop()
kind = frame[0]
if kind == "comma":
out.append(", ")
elif kind == "close":
out.append(")")
else: # "open"
node = frame[1]
if isinstance(node, Component):
out.append(f"{node.name or type(node).__name__}({dict(node)}")
subs = node.subcomponents
if subs:
# Defer ")" then push children in reverse so that
# popping yields original order, with ", " separators
# (the first popped comma serves as the separator
# between the component's dict and its first child).
stack.append(("close",))
for sub in reversed(subs):
stack.append(("open", sub))
stack.append(("comma",))
else:
out.append(")")
else:
# Should not normally occur (subcomponents are Components),
# but be safe and fall back to non-recursive str().
out.append(str(node))
return "".join(out)
def __eq__(self, other: Component) -> bool:
if not isinstance(other, Component):
return NotImplemented
# Two components are equal when their own properties are equal and their
# subcomponents are equal as a multiset: order does not matter, and each
# nested pair is compared the same way recursively. Subcomponents are
# neither sortable nor hashable, so we can't use a set; we have to match
# each one by searching. Done recursively that search is exponential for
# deeply nested components (GHSA-cv84-9p8j-fj68), so we walk an explicit
# stack instead of recursing.
#
# Each frame holds the pair being compared plus b's subcomponents
# still unmatched. child_result carries the outcome of the comparison
# that just finished back up to its parent frame: a successful child
# match removes that subcomponent from unmatched and advances to the
# next a subcomponent, while a failure tries the next candidate.
# Exhausting a's subcomponents means every one found a partner ->
# equal; running out of candidates for some subcomponent -> not equal.
# (Greedy matching is sufficient because equality is transitive, so equal
# candidates are interchangeable.)
stack = [_ComponentEqFrame(self, other)]
child_result = None
while stack:
frame = stack[-1]
if frame.unmatched is None:
if len(frame.a.subcomponents) != len(frame.b.subcomponents) or not (
CaselessDict.__eq__(frame.a, frame.b)
):
stack.pop()
child_result = False
continue
frame.unmatched = list(frame.b.subcomponents)
elif child_result is not None:
if child_result:
del frame.unmatched[frame.candidate_index]
frame.a_index += 1
frame.candidate_index = 0
else:
frame.candidate_index += 1
child_result = None
if frame.a_index >= len(frame.a.subcomponents):
stack.pop()
child_result = True
elif frame.candidate_index >= len(frame.unmatched):
stack.pop()
child_result = False
else:
stack.append(
_ComponentEqFrame(
frame.a.subcomponents[frame.a_index],
frame.unmatched[frame.candidate_index],
)
)
return child_result
DTSTAMP = stamp = single_utc_property(
"DTSTAMP",
"""The UTC datetime stamp recording when this component instance was created or last revised.
This property is defined in :rfc:`5545#section-3.8.7.2`. It's required
in ``VEVENT``, ``VTODO``, ``VJOURNAL``, and ``VFREEBUSY`` components.
When the calendar object carries a ``METHOD`` property, such as for
scheduling, this value is the creation time of *this particular revision*.
Without a ``METHOD`` property, it's equivalent to :attr:`LAST_MODIFIED`.
The value is always in UTC. It's also accessible as :attr:`stamp`.
Example:
.. code-block:: pycon
>>> from datetime import timezone, datetime
>>> from icalendar import Event
>>> event = Event()
>>> event.DTSTAMP = datetime(2024, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
>>> event.DTSTAMP
datetime.datetime(2024, 6, 1, 12, 0, tzinfo=ZoneInfo(key='UTC'))
See also:
:attr:`LAST_MODIFIED`, :attr:`CREATED`, :attr:`stamp`
""",
)
LAST_MODIFIED = single_utc_property(
"LAST-MODIFIED",
"""The UTC datetime when this component's information was last revised, per :rfc:`5545#section-3.8.7.3`.
It's analogous to a file's modification timestamp. This property is optional.
When it's absent, :attr:`last_modified` falls back to :attr:`DTSTAMP`.
This property is applicable to ``VEVENT``, ``VTODO``, ``VJOURNAL``, and ``VTIMEZONE``
components. The value is always in UTC.
Example:
.. code-block:: pycon
>>> from datetime import timezone, datetime
>>> from icalendar import Event
>>> event = Event()
>>> event.LAST_MODIFIED = datetime(2024, 6, 1, 9, 0, 0, tzinfo=timezone.utc)
>>> event.LAST_MODIFIED
datetime.datetime(2024, 6, 1, 9, 0, tzinfo=ZoneInfo(key='UTC'))
See also:
:attr:`last_modified`, :attr:`DTSTAMP`, :attr:`CREATED`
""",
)
@property
def last_modified(self) -> datetime:
"""Datetime when the information associated with the component was last revised.
Since :attr:`LAST_MODIFIED` is an optional property,
this returns :attr:`DTSTAMP` if :attr:`LAST_MODIFIED` is not set.
"""
return self.LAST_MODIFIED or self.DTSTAMP
@last_modified.setter
def last_modified(self, value):
self.LAST_MODIFIED = value
@last_modified.deleter
def last_modified(self):
del self.LAST_MODIFIED
@property
def created(self) -> datetime:
"""Datetime when the information associated with the component was created.
Since :attr:`CREATED` is an optional property,
this returns :attr:`DTSTAMP` if :attr:`CREATED` is not set.
"""
return self.CREATED or self.DTSTAMP
@created.setter
def created(self, value):
self.CREATED = value
@created.deleter
def created(self):
del self.CREATED
[docs]
def is_thunderbird(self) -> bool:
"""Whether this component has attributes that indicate that Mozilla Thunderbird created it."""
return any(attr.startswith("X-MOZ-") for attr in self.keys())
@staticmethod
def _utc_now() -> datetime:
"""Return now as UTC value."""
return datetime.now(timezone.utc)
uid = uid_property
comments = comments_property
links = links_property
related_to = related_to_property
concepts = concepts_property
refids = refids_property
CREATED = single_utc_property(
"CREATED",
"""The UTC datetime when this calendar component was first created, per :rfc:`5545#section-3.8.7.1`.
This property records when the calendar user agent originally stored the component.
This property is optional. When it's absent, :attr:`created` falls back to
:attr:`DTSTAMP`.
This property is applicable to ``VEVENT``, ``VTODO``, and ``VJOURNAL`` components.
The value is always in UTC.
Example:
.. code-block:: pycon
>>> from datetime import timezone, datetime
>>> from icalendar import Event
>>> event = Event()
>>> event.CREATED = datetime(2024, 1, 1, 8, 0, 0, tzinfo=timezone.utc)
>>> event.CREATED
datetime.datetime(2024, 1, 1, 8, 0, tzinfo=ZoneInfo(key='UTC'))
See also:
:attr:`created`, :attr:`DTSTAMP`, :attr:`LAST_MODIFIED`
""",
)
_validate_new = True
@staticmethod
def _validate_start_and_end(start, end):
"""This validates start and end.
Raises:
~error.InvalidCalendar: If the information is not valid
"""
if start is None or end is None:
return
if start > end:
raise InvalidCalendar("end must be after start")
[docs]
@classmethod
def new(
cls,
created: date | None = None,
comments: list[str] | str | None = None,
concepts: CONCEPTS_TYPE_SETTER = None,
last_modified: date | None = None,
links: LINKS_TYPE_SETTER = None,
refids: list[str] | str | None = None,
related_to: RELATED_TO_TYPE_SETTER = None,
stamp: date | None = None,
subcomponents: Iterable[Component] | None = None,
) -> Component:
"""Create a new component.
Parameters:
comments: The :attr:`comments` of the component.
concepts: The :attr:`concepts` of the component.
created: The :attr:`created` of the component.
last_modified: The :attr:`last_modified` of the component.
links: The :attr:`links` of the component.
related_to: The :attr:`related_to` of the component.
stamp: The :attr:`DTSTAMP` of the component.
subcomponents: The subcomponents of the component.
Raises:
~error.InvalidCalendar: If the content is not valid
according to :rfc:`5545`.
.. warning:: As time progresses, we will be stricter with the
validation.
"""
component = cls()
component.DTSTAMP = stamp
component.created = created
component.last_modified = last_modified
component.comments = comments
component.links = links
component.related_to = related_to
component.concepts = concepts
component.refids = refids
if subcomponents is not None:
component.subcomponents = (
subcomponents
if isinstance(subcomponents, list)
else list(subcomponents)
)
return component
[docs]
def to_jcal(self) -> list:
"""Convert this component to a jCal object.
Returns:
jCal object
See also :attr:`to_json`.
In this example, we create a simple VEVENT component and convert it to jCal:
.. code-block:: pycon
>>> from icalendar import Event
>>> from datetime import date
>>> from pprint import pprint
>>> event = Event.new(summary="My Event", start=date(2025, 11, 22))
>>> pprint(event.to_jcal())
['vevent',
[['dtstamp', {}, 'date-time', '2025-05-17T08:06:12Z'],
['summary', {}, 'text', 'My Event'],
['uid', {}, 'text', 'd755cef5-2311-46ed-a0e1-6733c9e15c63'],
['dtstart', {}, 'date', '2025-11-22']],
[]]
"""
# Iterative tree walk to avoid RecursionError on deeply nested
# components, mirroring the iterative iCal parser/serializer (GH #1370).
def make_node(comp: Component) -> list:
properties = [
item.to_jcal(key.lower())
for key, value in comp.items()
for item in (value if isinstance(value, list) else [value])
]
return [comp.name.lower(), properties, []]
root_node = make_node(self)
# stack of (component, jCal node) pairs still to expand
stack: list[tuple[Component, list]] = [(self, root_node)]
while stack:
comp, node = stack.pop()
children = node[2]
for subcomponent in comp.subcomponents:
child_node = make_node(subcomponent)
children.append(child_node)
stack.append((subcomponent, child_node))
return root_node
[docs]
def to_json(self) -> str:
"""Return this component as a jCal JSON string.
Returns:
JSON string
See also :attr:`to_jcal`.
"""
return json.dumps(self.to_jcal())
[docs]
@classmethod
def from_jcal(cls, jcal: str | list) -> Component:
"""Create a component from a jCal list.
Parameters:
jcal: jCal list or JSON string according to :rfc:`7265`.
Raises:
~error.JCalParsingError: If the jCal provided is invalid.
~json.JSONDecodeError: If the provided string is not valid JSON.
This reverses :func:`to_json` and :func:`to_jcal`.
The following code parses an example from :rfc:`7265`:
.. code-block:: pycon
>>> from icalendar import Component
>>> jcal = ["vcalendar",
... [
... ["calscale", {}, "text", "GREGORIAN"],
... ["prodid", {}, "text", "-//Example Inc.//Example Calendar//EN"],
... ["version", {}, "text", "2.0"]
... ],
... [
... ["vevent",
... [
... ["dtstamp", {}, "date-time", "2008-02-05T19:12:24Z"],
... ["dtstart", {}, "date", "2008-10-06"],
... ["summary", {}, "text", "Planning meeting"],
... ["uid", {}, "text", "4088E990AD89CB3DBB484909"]
... ],
... []
... ]
... ]
... ]
>>> calendar = Component.from_jcal(jcal)
>>> print(calendar.name)
VCALENDAR
>>> print(calendar.prodid)
-//Example Inc.//Example Calendar//EN
>>> event = calendar.events[0]
>>> print(event.summary)
Planning meeting
"""
if isinstance(jcal, str):
jcal = json.loads(jcal)
# Iterative tree build to avoid RecursionError on deeply nested jCal,
# mirroring the iterative iCal parser (GH #1370). ``_node_from_jcal``
# parses a single component (without its subcomponents); the stack walks
# the subcomponent tree, accumulating the jCal error path ([2, i] per
# nesting level) so error messages match the recursive implementation.
root, root_subcomponents = _node_from_jcal(jcal, cls)
stack: list[tuple[Component, list, list]] = [(root, root_subcomponents, [])]
while stack:
parent, subcomponents, prefix = stack.pop()
for i, subcomponent in enumerate(subcomponents):
child_prefix = [*prefix, 2, i]
# Prepend the full nesting path so errors match the recursive
# implementation. This also preserves the error value and
# traceback, like the nested context managers did before.
with JCalParsingError.reraise_with_path_added(*child_prefix):
child, child_subcomponents = _node_from_jcal(
subcomponent, type(parent)
)
parent.subcomponents.append(child)
stack.append((child, child_subcomponents, child_prefix))
return root
[docs]
def copy(self, recursive: bool = False) -> Self:
"""Copy the component.
Parameters:
recursive:
If ``True``, this creates copies of the component, its subcomponents,
and all its properties.
If ``False``, this only creates a shallow copy of the component.
Returns:
A copy of the component.
Examples:
Create a shallow copy of a component:
.. code-block:: pycon
>>> from icalendar import Event
>>> event = Event.new(description="Event to be copied")
>>> event_copy = event.copy()
>>> str(event_copy.description)
'Event to be copied'
Shallow copies lose their subcomponents:
.. code-block:: pycon
>>> from icalendar import Calendar
>>> calendar = Calendar.example()
>>> len(calendar.subcomponents)
3
>>> calendar_copy = calendar.copy()
>>> len(calendar_copy.subcomponents)
0
A recursive copy also copies all the subcomponents:
.. code-block:: pycon
>>> full_calendar_copy = calendar.copy(recursive=True)
>>> len(full_calendar_copy.subcomponents)
3
>>> full_calendar_copy.events[0] == calendar.events[0]
True
>>> full_calendar_copy.events[0] is calendar.events[0]
False
"""
if recursive:
return deepcopy(self)
return super().copy()
[docs]
def is_lazy(self) -> bool:
"""This component is fully parsed."""
return False
[docs]
def parse(self) -> Self:
"""Return the fully parsed component.
For non-lazy components, this returns self.
For lazy components, this parses the component and returns the result.
"""
return self
def _node_from_jcal(jcal, starting_cls: type[Component]) -> tuple[Component, list]:
"""Parse a single jCal component without recursing into subcomponents.
Module-level helper for :meth:`Component.from_jcal`: it has no ties to a
class or instance (the relevant class is passed in as ``starting_cls``), so
it is a plain function rather than a (static) method.
Parameters:
jcal: The jCal list for one component.
starting_cls: The class used as the parser for structural validation
before the component type is resolved from its name (the entry
class for the root, the parent's resolved class for a child).
Returns:
A ``(component, raw_subcomponents)`` tuple. The raw subcomponents are
returned for the caller to walk iteratively.
Raises:
~error.JCalParsingError: If this component node is invalid. The path
is relative to this node; callers prepend the nesting path.
"""
if not isinstance(jcal, list) or len(jcal) != 3:
raise JCalParsingError(
"A component must be a list with 3 items.", starting_cls, value=jcal
)
name, properties, subcomponents = jcal
if not isinstance(name, str):
raise JCalParsingError(
"The name must be a string.", starting_cls, path=[0], value=name
)
if name.upper() != starting_cls.name:
# delegate to correct component class
component_cls = starting_cls.get_component_class(name.upper())
else:
component_cls = starting_cls
component = component_cls()
if not isinstance(properties, list):
raise JCalParsingError(
"The properties must be a list.",
component_cls,
path=1,
value=properties,
)
for i, prop in enumerate(properties):
JCalParsingError.validate_property(prop, component_cls, path=[1, i])
prop_name = prop[0]
prop_value = prop[2]
prop_cls: type[VPROPERTY] = component_cls.types_factory.for_property(
prop_name, prop_value
)
with JCalParsingError.reraise_with_path_added(1, i):
v_prop = prop_cls.from_jcal(prop)
# jCal encodes the value type in the type field (``prop[2]``)
# instead of as a ``VALUE`` parameter (RFC 7265). Restore that
# parameter when the type differs from the property's default, so
# explicit value types such as ``RDATE;VALUE=PERIOD`` or
# ``TRIGGER;VALUE=DATE-TIME`` survive the round-trip (GH #1426).
# A type equal to the default needs no VALUE parameter, and the
# reserved ``unknown`` type must never become ``VALUE=UNKNOWN``
# (RFC 7265, section 5.2).
default_type = component_cls.types_factory.default_value_type(prop_name)
if isinstance(prop_value, str) and prop_value.lower() not in (
"unknown",
default_type,
):
v_prop.VALUE = prop_value.upper()
elif "VALUE" in v_prop.params:
del v_prop.VALUE
component.add(prop_name, v_prop)
if not isinstance(subcomponents, list):
raise JCalParsingError(
"The subcomponents must be a list.",
component_cls,
2,
value=subcomponents,
)
return component, subcomponents
__all__ = ["Component"]