"""The base for :rfc:`5545` components."""
from __future__ import annotations
import json
from copy import deepcopy
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 = []
[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):
"""Returns True, CaselessDict would return False if it had no items."""
return True
def __getitem__(self, key):
"""Get property value from the component dictionary."""
return super().__getitem__(key)
[docs]
def get(self, key, default=None):
"""Get property value with default."""
try:
return self[key]
except KeyError:
return default
[docs]
def is_empty(self):
"""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,
):
"""Add a property.
: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: None
"""
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:
path = Path(st)
try:
is_file = path.is_file()
except OSError:
is_file = False
if is_file:
st = path.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):
"""String representation of class with all of it's subcomponents."""
subs = ", ".join(str(it) for it in self.subcomponents)
return (
f"{self.name or type(self).__name__}"
f"({dict(self)}{', ' + subs if subs else ''})"
)
def __eq__(self, other):
if len(self.subcomponents) != len(other.subcomponents):
return False
properties_equal = super().__eq__(other)
if not properties_equal:
return False
# The subcomponents might not be in the same order,
# neither there's a natural key we can sort the subcomponents by nor
# are the subcomponent types hashable, so we cant put them in a set to
# check for set equivalence. We have to iterate over the subcomponents
# and look for each of them in the list.
for subcomponent in self.subcomponents:
if subcomponent not in other.subcomponents:
return False
# We now know the other component's subcomponents are not a strict subset
# of this component's. However, we still need to check the other way around.
for subcomponent in other.subcomponents:
if subcomponent not in self.subcomponents:
return False
return True
DTSTAMP = stamp = single_utc_property(
"DTSTAMP",
"""The UTC datetime stamp recording when this component instance was created or last revised.
Defined in :rfc:`5545#section-3.8.7.2` and required in
``VEVENT``, ``VTODO``, ``VJOURNAL``, and ``VFREEBUSY`` components.
When the calendar object carries a ``METHOD`` property (e.g., for
scheduling), this value is the creation time of *this particular revision*.
Without a ``METHOD`` property it is equivalent to :attr:`LAST_MODIFIED`.
The value is always in UTC. 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 # doctest: +ELLIPSIS
datetime.datetime(2024, 6, 1, 12, 0, tzinfo=...)
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`.
Analogous to a file's modification timestamp. This property is optional;
when absent, :attr:`last_modified` falls back to :attr:`DTSTAMP`.
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 # doctest: +ELLIPSIS
datetime.datetime(2024, 6, 1, 9, 0, tzinfo=...)
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():
"""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`.
Records when the calendar user agent originally stored the component.
This property is optional; when absent, :attr:`created` falls back to
:attr:`DTSTAMP`.
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 # doctest: +ELLIPSIS
datetime.datetime(2024, 1, 1, 8, 0, tzinfo=...)
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']],
[]]
"""
properties = []
for key, value in self.items():
for item in value if isinstance(value, list) else [value]:
properties.append(item.to_jcal(key.lower()))
return [
self.name.lower(),
properties,
[subcomponent.to_jcal() for subcomponent in self.subcomponents],
]
[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)
if not isinstance(jcal, list) or len(jcal) != 3:
raise JCalParsingError(
"A component must be a list with 3 items.", cls, value=jcal
)
name, properties, subcomponents = jcal
if not isinstance(name, str):
raise JCalParsingError(
"The name must be a string.", cls, path=[0], value=name
)
if name.upper() != cls.name:
# delegate to correct component class
component_cls = cls.get_component_class(name.upper())
return component_cls.from_jcal(jcal)
component = cls()
if not isinstance(properties, list):
raise JCalParsingError(
"The properties must be a list.", cls, path=1, value=properties
)
for i, prop in enumerate(properties):
JCalParsingError.validate_property(prop, cls, path=[1, i])
prop_name = prop[0]
prop_value = prop[2]
prop_cls: type[VPROPERTY] = cls.types_factory.for_property(
prop_name, prop_value
)
with JCalParsingError.reraise_with_path_added(1, i):
v_prop = prop_cls.from_jcal(prop)
# if we use the default value for that property, we can delete the
# VALUE parameter
if prop_cls == cls.types_factory.for_property(prop_name):
del v_prop.VALUE
component.add(prop_name, v_prop)
if not isinstance(subcomponents, list):
raise JCalParsingError(
"The subcomponents must be a list.", cls, 2, value=subcomponents
)
for i, subcomponent in enumerate(subcomponents):
with JCalParsingError.reraise_with_path_added(2, i):
component.subcomponents.append(cls.from_jcal(subcomponent))
return component
[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
__all__ = ["Component"]