Skip to content

The base classes

All iCalendar Component classes and iCalendar Property classes inherit from ICalBaseClass. Then ICalBaseClass is extended by both Component and Property.

ICalBaseClass

This is the base class of all custom classes representing an iCal component or iCal property in our library.

ical_library.base_classes.Component and :class:Property are the only ones inheriting this class directly, the rest of the classes are inheriting from :class:Component and :class:Property based on whether they represent an iCal component or iCal property.

Parameters:

Name Type Description Default
name str

the actual name of this property or component. E.g. VEVENT, RRULE, VCUSTOMCOMPONENT, CUSTOMPROPERTY.

required
parent Optional[Component]

The Component this item is encapsulated by in the iCalendar data file.

required
Source code in ical_library/base_classes/base_class.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class ICalBaseClass:
    """
    This is the base class of all custom classes representing an iCal component or iCal property in our library.

    [ical_library.base_classes.Component][] and :class:`Property` are the only ones inheriting this class directly, the
    rest of the classes are inheriting from :class:`Component` and :class:`Property` based on whether they represent an
    iCal component or iCal property.

    :param name: the actual name of this property or component. E.g. VEVENT, RRULE, VCUSTOMCOMPONENT, CUSTOMPROPERTY.
    :param parent: The Component this item is encapsulated by in the iCalendar data file.
    """

    def __init__(self, name: str, parent: Optional["Component"]):
        self._name = name
        if self._name is None:
            raise ValueError("Name of a Component or Property should not be None. Please specify it.")
        self._parent: Optional["Component"] = parent

    @property
    def parent(self) -> Optional["Component"]:
        """
        Return the parent :class:`Component` that contains this :class:`Component`.
        :return: Return the parent :class:`Component` instance or None in the case there is no parent (for VCalender's).
        """
        return self._parent

    @parent.setter
    def parent(self, value: "Component"):
        """
        Setter for the parent :class:`Component`. This allows us to set the parent at a later moment.
        :param value: The parent :class:`Component`.
        """
        self._parent = value

    @property
    def name(self) -> str:
        """
        Return the actual name of this property or component. E.g. VEVENT, RRULE, VCUSTOMCOMPONENT, CUSTOMPROPERTY.

        We inherit this class, for the general Property and Component but also for the specific VEvent component and
        the RRule property. Now what do we do with the `x-comp` or `iana-comp` components and `x-prop` and `iana-prop`
        properties? They also have an iCalendar name, e.g. VCUSTOMCOMPONENT. However, we can't specify them beforehand
        as we simply can't cover all cases. Therefore, we use `get_ical_name_of_class` to find and map all of our
        pre-defined Components and Properties but we still specify the name for all custom components. So the rule of
        thumb:
        Use `.name` on instantiated classes while we use `.get_ical_name_of_class()` for non-instantiated classes.
        """
        return self._name

    @classmethod
    def get_ical_name_of_class(cls) -> str:
        """
        Return the name of a pre-defined property or pre-defined component. E.g. VEVENT, RRULE, COMPONENT, PROPERTY.

        For a :class:`Property` this would be the value at the start of the line. Example: a property with the name of
        `ABC;def=ghi:jkl` would be `ABC`.
        For a :class:`Component` this would be the value at the start of the component after BEGIN. Example: a VEvent
        starts with `BEGIN:VEVENT`, hence this function would return `VEVENT`.
        """
        return cls.__name__.upper()

name: str property

Return the actual name of this property or component. E.g. VEVENT, RRULE, VCUSTOMCOMPONENT, CUSTOMPROPERTY.

We inherit this class, for the general Property and Component but also for the specific VEvent component and the RRule property. Now what do we do with the x-comp or iana-comp components and x-prop and iana-prop properties? They also have an iCalendar name, e.g. VCUSTOMCOMPONENT. However, we can't specify them beforehand as we simply can't cover all cases. Therefore, we use get_ical_name_of_class to find and map all of our pre-defined Components and Properties but we still specify the name for all custom components. So the rule of thumb: Use .name on instantiated classes while we use .get_ical_name_of_class() for non-instantiated classes.

parent: Optional[Component] property writable

Return the parent :class:Component that contains this :class:Component.

Returns:

Type Description

Return the parent :class:Component instance or None in the case there is no parent (for VCalender's).

get_ical_name_of_class() classmethod

Return the name of a pre-defined property or pre-defined component. E.g. VEVENT, RRULE, COMPONENT, PROPERTY.

For a :class:Property this would be the value at the start of the line. Example: a property with the name of ABC;def=ghi:jkl would be ABC. For a :class:Component this would be the value at the start of the component after BEGIN. Example: a VEvent starts with BEGIN:VEVENT, hence this function would return VEVENT.

Source code in ical_library/base_classes/base_class.py
56
57
58
59
60
61
62
63
64
65
66
@classmethod
def get_ical_name_of_class(cls) -> str:
    """
    Return the name of a pre-defined property or pre-defined component. E.g. VEVENT, RRULE, COMPONENT, PROPERTY.

    For a :class:`Property` this would be the value at the start of the line. Example: a property with the name of
    `ABC;def=ghi:jkl` would be `ABC`.
    For a :class:`Component` this would be the value at the start of the component after BEGIN. Example: a VEvent
    starts with `BEGIN:VEVENT`, hence this function would return `VEVENT`.
    """
    return cls.__name__.upper()

Component

Bases: ICalBaseClass

This is the base class for any component (according to the RFC 5545 specification) in iCal-library.

Inside all components (so also all classes inheriting this class, e.g. VEvent) there are four kind of variables:

  • variables that start with _. These are metadata of the class and not parsed as a property or component from the iCalendar data file.
  • variables that have a type of List[x] and a default value of List. These are child components/properties of the instance. These components/properties may or may not be required to be present in the iCal file.
  • variables that have a type of Optional[List[x]]. These are components/properties of the instance. They can be either optional or required and may occur multiple times in the iCal file.
  • variables that have a type of Optional[x] (and not Optional[List[x]]). These are properties of the instance. They can be either optional or required, but may only occur once in the iCal file.

Any Component that is predefined according to the RFC 5545 should inherit this class, e.g. VCalendar, VEVENT. Only x-components or iana-components should instantiate the Component class directly.

Parameters:

Name Type Description Default
name str

The actual name of this component instance. E.g. VEVENT, RRULE, VCUSTOMCOMPONENT.

required
parent Optional[Component]

The Component this item is encapsulated by in the iCalendar data file.

None
Source code in ical_library/base_classes/component.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
class Component(ICalBaseClass):
    """
    This is the base class for any component (according to the RFC 5545 specification) in iCal-library.

    Inside all components (so also all classes inheriting this class, e.g. VEvent) there are four kind of variables:

    - variables that start with `_`. These are metadata of the class and not parsed as a property or
     component from the iCalendar data file.
    - variables that have a type of `List[x]` and a default value of List. These are child components/properties of
    the instance. These components/properties may or may not be required to be present in the iCal file.
    - variables that have a type of `Optional[List[x]]`. These are components/properties of the instance.
    They can be either optional or required and may occur multiple times in the iCal file.
    - variables that have a type of `Optional[x]` (and not `Optional[List[x]]`). These are properties of the instance.
    They can be either optional or required, but may only occur once in the iCal file.

    Any Component that is predefined according to the RFC 5545 should inherit this class, e.g. VCalendar, VEVENT.
    Only x-components or iana-components should instantiate the Component class directly.

    :param name: The actual name of this component instance. E.g. `VEVENT`, `RRULE`, `VCUSTOMCOMPONENT`.
    :param parent: The Component this item is encapsulated by in the iCalendar data file.
    """

    def __init__(self, name: str, parent: Optional["Component"] = None):
        name = name if self.__class__ == Component else self.__class__.get_ical_name_of_class()
        super().__init__(name=name, parent=parent or ComponentContext.get_current_component())
        if parent is None and self.parent is not None:
            self.parent.__add_child_without_setting_the_parent(self)
        if self.parent is None and self.name != "VCALENDAR":
            raise ValueError("We should always set a Parent on __init__ of Components.")
        self._parse_line_start: Optional[int] = 0
        self._parse_line_end: Optional[int] = 0
        self._extra_child_components: Dict[str, List["Component"]] = defaultdict(list)
        self._extra_properties: Dict[str, List[Property]] = defaultdict(list)

    def __repr__(self) -> str:
        """Overwrite the repr to create a better representation for the item."""
        properties_as_string = ", ".join([f"{name}={value}" for name, value in self.properties.items()])
        return f"{self.__class__.__name__}({properties_as_string})"

    def __eq__(self: "Component", other: "Component") -> bool:
        """Return whether the current instance and the other instance are the same."""
        if type(self) != type(other):
            return False
        return self.properties == other.properties and self.children == other.children

    def __enter__(self):
        """Enter the context manager. Check ComponentContext for more info."""
        ComponentContext.push_context_managed_component(self)
        return self

    def __exit__(self, _type, _value, _tb):
        """Exit the context manager. Check ComponentContext for more info."""
        ComponentContext.pop_context_managed_component()

    def _set_self_as_parent_for_ical_component(self, prop_or_comp: ICalBaseClass) -> None:
        """Verifies the parent is not already set to a different component, if not sets the parent."""
        if prop_or_comp.parent is None:
            prop_or_comp.parent = self
        elif prop_or_comp.parent != self:
            raise ValueError(
                "Trying to overwrite a parent. Please do not re-use property instance across different components."
            )

    def as_parent(self, value: T) -> T:
        """We set self as Parent for Properties and Components but also Properties and Components in lists."""
        if isinstance(value, ICalBaseClass):
            self._set_self_as_parent_for_ical_component(value)
        elif isinstance(value, list):  # checking for list over Iterable is ~8,5x faster.
            for item in value:
                if isinstance(item, ICalBaseClass):
                    self._set_self_as_parent_for_ical_component(item)
        return value

    @property
    def extra_child_components(self) -> Dict[str, List["Component"]]:
        """Return all children components that are considered as `x-comp` or `iana-comp` components."""
        return self._extra_child_components

    @property
    def extra_properties(self) -> Dict[str, List[Property]]:
        """Return all properties that are considered as `x-prop` or `iana-prop` properties."""
        return self._extra_properties

    @property
    def tree_root(self) -> "VCalendar":
        """Return the tree root which should always be a VCalendar object."""
        from ical_library.ical_components import VCalendar

        instance = self
        while instance.parent is not None:
            instance = instance.parent
        if not isinstance(instance, VCalendar):
            raise CalendarParentRelationError(
                f"TreeRoot {instance=} is of type {type(instance)=} instead of VCalendar."
            )
        return instance

    @property
    def children(self) -> List["Component"]:
        """Return all children components."""
        extras = (child for list_of_children in self._extra_child_components.values() for child in list_of_children)
        children = [
            item_in_list
            for ical_name, (var_name, var_type, is_list) in self._get_child_component_mapping().items()
            for item_in_list in getattr(self, var_name)
        ]
        children.extend(extras)
        return children

    def __add_child_without_setting_the_parent(self, child: "Component") -> None:
        """
        Just add a child component and do not also set the parent.

        If the child is an undefined `x-comp` or `iana-comp` component, we add it to _extra_child_components.
        If the child is defined, we add it to one of the other variables according to
        :function:`self._get_child_component_mapping()`.
        """
        child_component_mapping = self._get_child_component_mapping()
        if child.name in child_component_mapping:
            var_name, var_type, is_list = child_component_mapping[child.name]
            getattr(self, var_name).append(child)
            return
        self._extra_child_components[child.name].append(child)

    def add_child(self, child: "Component") -> None:
        """
        Add a children component and set its parent.

        If the child is an undefined `x-comp` or `iana-comp` component, we add it to _extra_child_components.
        If the child is defined, we add it to one of the other variables according to
        :function:`self._get_child_component_mapping()`.
        """
        self.as_parent(child)
        self.__add_child_without_setting_the_parent(child)

    @property
    def original_ical_text(self) -> str:
        """Return the original iCAL text for your property from the RAW string list as if it is a property."""
        return self.tree_root.get_original_ical_text(self._parse_line_start, self._parse_line_end)

    @classmethod
    @lru_cache()
    def get_property_ical_names(cls) -> Set[str]:
        """
        Get all the variables for this component class that reference a :class:`Property` in the typing information.
        """
        return {var_name for var_name, var_type, is_list in cls._get_property_mapping().values()}

    @staticmethod
    def _extract_ical_class_from_args(var_name: str, a_type: Union[Type[List], type(Union)]) -> Type:
        """
        Given *a_type*, which is either a List or an Optional, return the subtype that is not None.

        Note: When we execute get_args(some_type), we consider the result to be the subtypes.
        :param var_name: The variable name of the type we are dissecting.
        :param a_type: The type we want to get the subtype of.
        :return: The subtype that is not equal to the NoneType.
        :raise: TypeError when there is no subtype that does not contain a type that is not equal to NoneType.
        """
        sub_types: List[Type] = [st for st in get_args(a_type) if not issubclass(get_origin(st) or st, type(None))]
        if len(sub_types) != 1:
            raise TypeError(f"Incorrect number of sub_types to follow here for {var_name=}, {a_type=}, {sub_types=}.")
        return sub_types[0]

    @staticmethod
    def _extract_type_information(
        var_name: str, a_type: Type, is_in_list: bool
    ) -> Optional[Tuple[str, Tuple[str, Optional[Type[ICalBaseClass]], bool]]]:
        """
        Extract typing information for an instance variable of the component.

        The type of the variable should either be (wrapping) a :class:`Property` or a :class:`Component`.
        :param var_name: The variable name of the type we are dissecting.
        :param a_type: The type we want to extract a child class of :class:`ICalBaseClass` from.
        :param is_in_list: Whether the child class of :class:`ICalBaseClass` is contained in a List type.
        :return: None if there is no child class of :class:`ICalBaseClass` we can detect. Otherwise, we return
        a tuple containing the iCal name (e.g. VEVENT) and another tuple that contains the variable name, the child
        class of :class:`ICalBaseClass` and a boolean whether that child class was wrapped in a List.
        :raise: TypeError if there is no child class of :class:`ICalBaseClass` to detect.
        """
        if get_origin(a_type) is None:
            if issubclass(a_type, ICalBaseClass):
                return a_type.get_ical_name_of_class(), (var_name, a_type, is_in_list)
            return None
        elif get_origin(a_type) == Union:  # This also covers the Optional case.
            sub_class = Component._extract_ical_class_from_args(var_name, a_type)
            return Component._extract_type_information(var_name, sub_class, is_in_list)
        elif issubclass(get_origin(a_type), List):
            sub_class = Component._extract_ical_class_from_args(var_name, a_type)
            return Component._extract_type_information(var_name, sub_class, True)
        elif get_origin(a_type) == ClassVar:
            return None
        else:
            raise TypeError(f"Unknown type '{a_type}' came by in Component.extract_custom_type.")

    @classmethod
    def _get_init_method_for_var_mapping(cls) -> Callable:
        """
        We generate _get_var_mapping based on `cls.__init__`. This var mapping is later used to list all properties,
        all components but also all the types of the items. This is a function so that it can be overwritten for
        the recurring components.
        """
        return cls.__init__

    @classmethod
    @lru_cache()
    def _get_var_mapping(cls) -> Mapping[str, Tuple[str, Type[ICalBaseClass], bool]]:
        """
        Get a mapping of all variables of this class that do not start with `_`.
        :return: A class mapping that maps the iCal name (e.g. VEVENT) to another tuple that contains
        the variable name, the child class of :class:`ICalBaseClass` and a boolean whether that child class was wrapped
        in a List.
        """
        var_mapping: Dict[str, Tuple[str, Type[ICalBaseClass], bool]] = {}
        a_field: inspect.Parameter
        for a_field in inspect.signature(cls._get_init_method_for_var_mapping()).parameters.values():
            if a_field.name.startswith("_") or a_field.name in ["self", "parent", "name"]:
                continue
            result = Component._extract_type_information(a_field.name, a_field.annotation, False)
            if result is None:
                continue
            ical_name, var_type_info = result
            if issubclass(var_type_info[1], ICalBaseClass):
                var_mapping[ical_name] = var_type_info
        return var_mapping

    @classmethod
    @lru_cache()
    def _get_property_mapping(cls) -> Mapping[str, Tuple[str, Type[Property], bool]]:
        """
        Return the same mapping as :function:`cls._get_var_mapping()` but only return variables related to
        :class:`Property` classes. Example: `{"RRULE": tuple("rrule", Type[RRule], False), ...}`
        """
        return {
            ical_name: var_tuple
            for ical_name, var_tuple in cls._get_var_mapping().items()
            if issubclass(var_tuple[1], Property)
        }

    @classmethod
    @lru_cache()
    def _get_child_component_mapping(cls) -> Mapping[str, Tuple[str, Type["Component"], bool]]:
        """
        Return the same mapping as :function:`cls._get_var_mapping()` but only return variables related to
        :class:`Component` classes.
        """
        return {
            ical_name: var_tuple
            for ical_name, var_tuple in cls._get_var_mapping().items()
            if issubclass(var_tuple[1], Component)
        }

    @property
    def properties(self) -> Dict[str, Union[Property, List[Property]]]:
        """Return all iCalendar properties of this component instance."""
        standard_properties = {
            var_name: getattr(self, var_name)
            for var_name, var_type, is_list in self._get_property_mapping().values()
            if getattr(self, var_name) is not None
        }
        return {**standard_properties, **self._extra_properties}

    def print_tree_structure(self, indent: int = 0) -> None:
        """Print the tree structure of all components starting with this instance."""
        print(f"{'  ' * indent} - {self}")
        for child in self.children:
            child.print_tree_structure(indent=indent + 1)

    def set_property(
        self,
        property_instance: Property,
        property_map_info: Optional[Tuple[str, Type[Property], bool]] = None,
        property_map_was_checked: bool = False,
    ) -> None:
        """
        Setting a property for a Component instance.

        If the `property_map_info` is equal to None, we either have not yet looked up all the properties or it is an
        x-prop/iana-prop. This can be decided based on property_map_was_checked. This avoids extra (expensive) lookups
        in our self._get_property_mapping.

        :param property_instance: The Property we wish to set.
        :param property_map_info: A tuple containing the variable name for this Component instance, the type of the
        Property and whether the variable can occur multiple times for the same property. If it equals None, this either
        means it is an x-prop/iana-prop or that the property_map was not checked yet.
        :param property_map_was_checked: Whether the `property_map_info` was passed or not. If True, and
        `property_map_info` is `None`, we know that it is an iana property. If False, we still need to consult
        `_get_property_mapping`.
        """
        if property_map_info is None and property_map_was_checked is False:
            property_map_info = self._get_property_mapping().get(property_instance.name)
        var_name, var_type, is_list = property_map_info or [None, None, None]
        if var_name is not None and is_list is not None:
            if is_list is True:
                if getattr(self, var_name) is None:
                    setattr(self, var_name, [property_instance])
                else:
                    current_value: List[Property] = getattr(self, var_name)
                    current_value.append(property_instance)
            else:
                setattr(self, var_name, property_instance)
        else:
            self._extra_properties[property_instance.name.lower().replace("-", "_")].append(property_instance)

    def parse_property(self, line: str) -> Property:
        """
        Parse a raw line containing a :class:`Property` definition, instantiate the corresponding Property and set the
        variable.

        Based on the first part of the line (before the ; and :), we know using *self._get_property_mapping()* which
        property type we should instantiate. Then, depending on whether the typing info of the property denoted it in
        a List or not, it adds it to a list/instantiates the list, compared to simply setting it as the variable of
        the :class:`Component` instance.

        Credits for the excellent regex parsing string go to @Jan Goyvaerts: https://stackoverflow.com/a/2482067/2277445

        :param line: The entire line that contains the property string (meaning multi-lines properties are already
        converted to a single line here).
        :return: The created Property instance based on the *line* that we set for the component.
        """
        property_mapping = self._get_property_mapping()
        result = re.search("^([^\r\n;:]+)(;[^\r\n:]+)?:(.*)$", line)
        if result is None:
            raise ValueError(f"{result=} should never be None! {line=} is invalid.")
        name, property_parameters, value = result.group(1), result.group(2), result.group(3)
        property_parameters = property_parameters.lstrip(";") if property_parameters else None
        if name in property_mapping.keys():
            property_map_info = property_mapping[name]
            var_name, var_type, is_list = property_map_info
            property_instance = var_type(name=name, property_parameters=property_parameters, value=value, parent=self)
            self.set_property(property_instance, property_map_info, property_map_was_checked=True)
        else:
            property_instance = Property(name=name, property_parameters=property_parameters, value=value, parent=self)
            self.set_property(property_instance, None, property_map_was_checked=True)
        return property_instance

    def _instantiate_component(self, ical_component_identifier: str) -> "Component":
        component_mapping = self._get_child_component_mapping()
        if ical_component_identifier in component_mapping:
            var_name, var_type, is_list = component_mapping[ical_component_identifier]
            return var_type(parent=self)  # type: ignore
        else:
            return Component(name=ical_component_identifier, parent=self)

    def parse_component(self, lines: List[str], line_number: int) -> int:
        """
        Parse the raw lines representing this component (which was just instantiated).

        Based on the first line that starts with `BEGIN:`, we know using *self._get_child_component_mapping()* which
        specific component type we should instantiate. We then add it to the current component instance as a child.
        Then we parse line by line, if we find another `BEGIN:`, we create another component instance and proceed
        to calling :function:`self.parse_component` for parsing all the lines related to that component. If we find
        a property line (any line that doesn't start with `BEGIN:`), we call :function:`self.parse_property` which
        then automatically adds it to the current instance.

        :param lines: A list of all the lines in the iCalendar file.
        :param line_number: The line number at which this component starts.
        :return: The line number at which this component ends.
        """
        self._parse_line_start = line_number - 1
        while not (current_line := lines[line_number]).startswith("END:"):
            line_number += 1
            if current_line.startswith("BEGIN:"):
                component_name = current_line[len("BEGIN:") :]
                instance = self._instantiate_component(component_name)
                self.add_child(instance)
                line_number = instance.parse_component(lines=lines, line_number=line_number)
                continue

            full_line_without_line_breaks = current_line
            while (next_line := lines[line_number]).startswith(" "):
                line_number += 1
                # [1:] so we skip the space indicating a line break.
                full_line_without_line_breaks += next_line[1:]
            self.parse_property(full_line_without_line_breaks)

        if current_line != f"END:{self.name}":
            raise ValueError(
                f"Expected {current_line=} to be equal to END:{self.name}. It seems {self} was never closed."
            )
        self._parse_line_end = line_number + 1
        return line_number + 1

children: List[Component] property

Return all children components.

extra_child_components: Dict[str, List[Component]] property

Return all children components that are considered as x-comp or iana-comp components.

extra_properties: Dict[str, List[Property]] property

Return all properties that are considered as x-prop or iana-prop properties.

original_ical_text: str property

Return the original iCAL text for your property from the RAW string list as if it is a property.

properties: Dict[str, Union[Property, List[Property]]] property

Return all iCalendar properties of this component instance.

tree_root: VCalendar property

Return the tree root which should always be a VCalendar object.

__add_child_without_setting_the_parent(child)

Just add a child component and do not also set the parent.

If the child is an undefined x-comp or iana-comp component, we add it to _extra_child_components. If the child is defined, we add it to one of the other variables according to :function:self._get_child_component_mapping().

Source code in ical_library/base_classes/component.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def __add_child_without_setting_the_parent(self, child: "Component") -> None:
    """
    Just add a child component and do not also set the parent.

    If the child is an undefined `x-comp` or `iana-comp` component, we add it to _extra_child_components.
    If the child is defined, we add it to one of the other variables according to
    :function:`self._get_child_component_mapping()`.
    """
    child_component_mapping = self._get_child_component_mapping()
    if child.name in child_component_mapping:
        var_name, var_type, is_list = child_component_mapping[child.name]
        getattr(self, var_name).append(child)
        return
    self._extra_child_components[child.name].append(child)

__enter__()

Enter the context manager. Check ComponentContext for more info.

Source code in ical_library/base_classes/component.py
78
79
80
81
def __enter__(self):
    """Enter the context manager. Check ComponentContext for more info."""
    ComponentContext.push_context_managed_component(self)
    return self

__exit__(_type, _value, _tb)

Exit the context manager. Check ComponentContext for more info.

Source code in ical_library/base_classes/component.py
83
84
85
def __exit__(self, _type, _value, _tb):
    """Exit the context manager. Check ComponentContext for more info."""
    ComponentContext.pop_context_managed_component()

_extract_ical_class_from_args(var_name, a_type) staticmethod

Given a_type, which is either a List or an Optional, return the subtype that is not None.

Note: When we execute get_args(some_type), we consider the result to be the subtypes.

Parameters:

Name Type Description Default
var_name str

The variable name of the type we are dissecting.

required
a_type Union[Type[List], type(Union)]

The type we want to get the subtype of.

required

Returns:

Type Description
Type

The subtype that is not equal to the NoneType.

Source code in ical_library/base_classes/component.py
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
@staticmethod
def _extract_ical_class_from_args(var_name: str, a_type: Union[Type[List], type(Union)]) -> Type:
    """
    Given *a_type*, which is either a List or an Optional, return the subtype that is not None.

    Note: When we execute get_args(some_type), we consider the result to be the subtypes.
    :param var_name: The variable name of the type we are dissecting.
    :param a_type: The type we want to get the subtype of.
    :return: The subtype that is not equal to the NoneType.
    :raise: TypeError when there is no subtype that does not contain a type that is not equal to NoneType.
    """
    sub_types: List[Type] = [st for st in get_args(a_type) if not issubclass(get_origin(st) or st, type(None))]
    if len(sub_types) != 1:
        raise TypeError(f"Incorrect number of sub_types to follow here for {var_name=}, {a_type=}, {sub_types=}.")
    return sub_types[0]

_extract_type_information(var_name, a_type, is_in_list) staticmethod

Extract typing information for an instance variable of the component.

The type of the variable should either be (wrapping) a :class:Property or a :class:Component.

Parameters:

Name Type Description Default
var_name str

The variable name of the type we are dissecting.

required
a_type Type

The type we want to extract a child class of :class:ICalBaseClass from.

required
is_in_list bool

Whether the child class of :class:ICalBaseClass is contained in a List type.

required

Returns:

Type Description
Optional[Tuple[str, Tuple[str, Optional[Type[ICalBaseClass]], bool]]]

None if there is no child class of :class:ICalBaseClass we can detect. Otherwise, we return a tuple containing the iCal name (e.g. VEVENT) and another tuple that contains the variable name, the child class of :class:ICalBaseClass and a boolean whether that child class was wrapped in a List.

Source code in ical_library/base_classes/component.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
@staticmethod
def _extract_type_information(
    var_name: str, a_type: Type, is_in_list: bool
) -> Optional[Tuple[str, Tuple[str, Optional[Type[ICalBaseClass]], bool]]]:
    """
    Extract typing information for an instance variable of the component.

    The type of the variable should either be (wrapping) a :class:`Property` or a :class:`Component`.
    :param var_name: The variable name of the type we are dissecting.
    :param a_type: The type we want to extract a child class of :class:`ICalBaseClass` from.
    :param is_in_list: Whether the child class of :class:`ICalBaseClass` is contained in a List type.
    :return: None if there is no child class of :class:`ICalBaseClass` we can detect. Otherwise, we return
    a tuple containing the iCal name (e.g. VEVENT) and another tuple that contains the variable name, the child
    class of :class:`ICalBaseClass` and a boolean whether that child class was wrapped in a List.
    :raise: TypeError if there is no child class of :class:`ICalBaseClass` to detect.
    """
    if get_origin(a_type) is None:
        if issubclass(a_type, ICalBaseClass):
            return a_type.get_ical_name_of_class(), (var_name, a_type, is_in_list)
        return None
    elif get_origin(a_type) == Union:  # This also covers the Optional case.
        sub_class = Component._extract_ical_class_from_args(var_name, a_type)
        return Component._extract_type_information(var_name, sub_class, is_in_list)
    elif issubclass(get_origin(a_type), List):
        sub_class = Component._extract_ical_class_from_args(var_name, a_type)
        return Component._extract_type_information(var_name, sub_class, True)
    elif get_origin(a_type) == ClassVar:
        return None
    else:
        raise TypeError(f"Unknown type '{a_type}' came by in Component.extract_custom_type.")

_get_child_component_mapping() cached classmethod

Return the same mapping as :function:cls._get_var_mapping() but only return variables related to :class:Component classes.

Source code in ical_library/base_classes/component.py
272
273
274
275
276
277
278
279
280
281
282
283
@classmethod
@lru_cache()
def _get_child_component_mapping(cls) -> Mapping[str, Tuple[str, Type["Component"], bool]]:
    """
    Return the same mapping as :function:`cls._get_var_mapping()` but only return variables related to
    :class:`Component` classes.
    """
    return {
        ical_name: var_tuple
        for ical_name, var_tuple in cls._get_var_mapping().items()
        if issubclass(var_tuple[1], Component)
    }

_get_init_method_for_var_mapping() classmethod

We generate _get_var_mapping based on cls.__init__. This var mapping is later used to list all properties, all components but also all the types of the items. This is a function so that it can be overwritten for the recurring components.

Source code in ical_library/base_classes/component.py
228
229
230
231
232
233
234
235
@classmethod
def _get_init_method_for_var_mapping(cls) -> Callable:
    """
    We generate _get_var_mapping based on `cls.__init__`. This var mapping is later used to list all properties,
    all components but also all the types of the items. This is a function so that it can be overwritten for
    the recurring components.
    """
    return cls.__init__

_get_property_mapping() cached classmethod

Return the same mapping as :function:cls._get_var_mapping() but only return variables related to :class:Property classes. Example: {"RRULE": tuple("rrule", Type[RRule], False), ...}

Source code in ical_library/base_classes/component.py
259
260
261
262
263
264
265
266
267
268
269
270
@classmethod
@lru_cache()
def _get_property_mapping(cls) -> Mapping[str, Tuple[str, Type[Property], bool]]:
    """
    Return the same mapping as :function:`cls._get_var_mapping()` but only return variables related to
    :class:`Property` classes. Example: `{"RRULE": tuple("rrule", Type[RRule], False), ...}`
    """
    return {
        ical_name: var_tuple
        for ical_name, var_tuple in cls._get_var_mapping().items()
        if issubclass(var_tuple[1], Property)
    }

_get_var_mapping() cached classmethod

Get a mapping of all variables of this class that do not start with _.

Returns:

Type Description
Mapping[str, Tuple[str, Type[ICalBaseClass], bool]]

A class mapping that maps the iCal name (e.g. VEVENT) to another tuple that contains the variable name, the child class of :class:ICalBaseClass and a boolean whether that child class was wrapped in a List.

Source code in ical_library/base_classes/component.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
@classmethod
@lru_cache()
def _get_var_mapping(cls) -> Mapping[str, Tuple[str, Type[ICalBaseClass], bool]]:
    """
    Get a mapping of all variables of this class that do not start with `_`.
    :return: A class mapping that maps the iCal name (e.g. VEVENT) to another tuple that contains
    the variable name, the child class of :class:`ICalBaseClass` and a boolean whether that child class was wrapped
    in a List.
    """
    var_mapping: Dict[str, Tuple[str, Type[ICalBaseClass], bool]] = {}
    a_field: inspect.Parameter
    for a_field in inspect.signature(cls._get_init_method_for_var_mapping()).parameters.values():
        if a_field.name.startswith("_") or a_field.name in ["self", "parent", "name"]:
            continue
        result = Component._extract_type_information(a_field.name, a_field.annotation, False)
        if result is None:
            continue
        ical_name, var_type_info = result
        if issubclass(var_type_info[1], ICalBaseClass):
            var_mapping[ical_name] = var_type_info
    return var_mapping

_set_self_as_parent_for_ical_component(prop_or_comp)

Verifies the parent is not already set to a different component, if not sets the parent.

Source code in ical_library/base_classes/component.py
87
88
89
90
91
92
93
94
def _set_self_as_parent_for_ical_component(self, prop_or_comp: ICalBaseClass) -> None:
    """Verifies the parent is not already set to a different component, if not sets the parent."""
    if prop_or_comp.parent is None:
        prop_or_comp.parent = self
    elif prop_or_comp.parent != self:
        raise ValueError(
            "Trying to overwrite a parent. Please do not re-use property instance across different components."
        )

add_child(child)

Add a children component and set its parent.

If the child is an undefined x-comp or iana-comp component, we add it to _extra_child_components. If the child is defined, we add it to one of the other variables according to :function:self._get_child_component_mapping().

Source code in ical_library/base_classes/component.py
157
158
159
160
161
162
163
164
165
166
def add_child(self, child: "Component") -> None:
    """
    Add a children component and set its parent.

    If the child is an undefined `x-comp` or `iana-comp` component, we add it to _extra_child_components.
    If the child is defined, we add it to one of the other variables according to
    :function:`self._get_child_component_mapping()`.
    """
    self.as_parent(child)
    self.__add_child_without_setting_the_parent(child)

as_parent(value)

We set self as Parent for Properties and Components but also Properties and Components in lists.

Source code in ical_library/base_classes/component.py
 96
 97
 98
 99
100
101
102
103
104
def as_parent(self, value: T) -> T:
    """We set self as Parent for Properties and Components but also Properties and Components in lists."""
    if isinstance(value, ICalBaseClass):
        self._set_self_as_parent_for_ical_component(value)
    elif isinstance(value, list):  # checking for list over Iterable is ~8,5x faster.
        for item in value:
            if isinstance(item, ICalBaseClass):
                self._set_self_as_parent_for_ical_component(item)
    return value

get_property_ical_names() cached classmethod

Get all the variables for this component class that reference a :class:Property in the typing information.

Source code in ical_library/base_classes/component.py
173
174
175
176
177
178
179
@classmethod
@lru_cache()
def get_property_ical_names(cls) -> Set[str]:
    """
    Get all the variables for this component class that reference a :class:`Property` in the typing information.
    """
    return {var_name for var_name, var_type, is_list in cls._get_property_mapping().values()}

parse_component(lines, line_number)

Parse the raw lines representing this component (which was just instantiated).

Based on the first line that starts with BEGIN:, we know using self._get_child_component_mapping() which specific component type we should instantiate. We then add it to the current component instance as a child. Then we parse line by line, if we find another BEGIN:, we create another component instance and proceed to calling :function:self.parse_component for parsing all the lines related to that component. If we find a property line (any line that doesn't start with BEGIN:), we call :function:self.parse_property which then automatically adds it to the current instance.

Parameters:

Name Type Description Default
lines List[str]

A list of all the lines in the iCalendar file.

required
line_number int

The line number at which this component starts.

required

Returns:

Type Description
int

The line number at which this component ends.

Source code in ical_library/base_classes/component.py
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
def parse_component(self, lines: List[str], line_number: int) -> int:
    """
    Parse the raw lines representing this component (which was just instantiated).

    Based on the first line that starts with `BEGIN:`, we know using *self._get_child_component_mapping()* which
    specific component type we should instantiate. We then add it to the current component instance as a child.
    Then we parse line by line, if we find another `BEGIN:`, we create another component instance and proceed
    to calling :function:`self.parse_component` for parsing all the lines related to that component. If we find
    a property line (any line that doesn't start with `BEGIN:`), we call :function:`self.parse_property` which
    then automatically adds it to the current instance.

    :param lines: A list of all the lines in the iCalendar file.
    :param line_number: The line number at which this component starts.
    :return: The line number at which this component ends.
    """
    self._parse_line_start = line_number - 1
    while not (current_line := lines[line_number]).startswith("END:"):
        line_number += 1
        if current_line.startswith("BEGIN:"):
            component_name = current_line[len("BEGIN:") :]
            instance = self._instantiate_component(component_name)
            self.add_child(instance)
            line_number = instance.parse_component(lines=lines, line_number=line_number)
            continue

        full_line_without_line_breaks = current_line
        while (next_line := lines[line_number]).startswith(" "):
            line_number += 1
            # [1:] so we skip the space indicating a line break.
            full_line_without_line_breaks += next_line[1:]
        self.parse_property(full_line_without_line_breaks)

    if current_line != f"END:{self.name}":
        raise ValueError(
            f"Expected {current_line=} to be equal to END:{self.name}. It seems {self} was never closed."
        )
    self._parse_line_end = line_number + 1
    return line_number + 1

parse_property(line)

Parse a raw line containing a :class:Property definition, instantiate the corresponding Property and set the variable.

Based on the first part of the line (before the ; and :), we know using self._get_property_mapping() which property type we should instantiate. Then, depending on whether the typing info of the property denoted it in a List or not, it adds it to a list/instantiates the list, compared to simply setting it as the variable of the :class:Component instance.

Credits for the excellent regex parsing string go to @Jan Goyvaerts: https://stackoverflow.com/a/2482067/2277445

Parameters:

Name Type Description Default
line str

The entire line that contains the property string (meaning multi-lines properties are already converted to a single line here).

required

Returns:

Type Description
Property

The created Property instance based on the line that we set for the component.

Source code in ical_library/base_classes/component.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def parse_property(self, line: str) -> Property:
    """
    Parse a raw line containing a :class:`Property` definition, instantiate the corresponding Property and set the
    variable.

    Based on the first part of the line (before the ; and :), we know using *self._get_property_mapping()* which
    property type we should instantiate. Then, depending on whether the typing info of the property denoted it in
    a List or not, it adds it to a list/instantiates the list, compared to simply setting it as the variable of
    the :class:`Component` instance.

    Credits for the excellent regex parsing string go to @Jan Goyvaerts: https://stackoverflow.com/a/2482067/2277445

    :param line: The entire line that contains the property string (meaning multi-lines properties are already
    converted to a single line here).
    :return: The created Property instance based on the *line* that we set for the component.
    """
    property_mapping = self._get_property_mapping()
    result = re.search("^([^\r\n;:]+)(;[^\r\n:]+)?:(.*)$", line)
    if result is None:
        raise ValueError(f"{result=} should never be None! {line=} is invalid.")
    name, property_parameters, value = result.group(1), result.group(2), result.group(3)
    property_parameters = property_parameters.lstrip(";") if property_parameters else None
    if name in property_mapping.keys():
        property_map_info = property_mapping[name]
        var_name, var_type, is_list = property_map_info
        property_instance = var_type(name=name, property_parameters=property_parameters, value=value, parent=self)
        self.set_property(property_instance, property_map_info, property_map_was_checked=True)
    else:
        property_instance = Property(name=name, property_parameters=property_parameters, value=value, parent=self)
        self.set_property(property_instance, None, property_map_was_checked=True)
    return property_instance

print_tree_structure(indent=0)

Print the tree structure of all components starting with this instance.

Source code in ical_library/base_classes/component.py
295
296
297
298
299
def print_tree_structure(self, indent: int = 0) -> None:
    """Print the tree structure of all components starting with this instance."""
    print(f"{'  ' * indent} - {self}")
    for child in self.children:
        child.print_tree_structure(indent=indent + 1)

set_property(property_instance, property_map_info=None, property_map_was_checked=False)

Setting a property for a Component instance.

If the property_map_info is equal to None, we either have not yet looked up all the properties or it is an x-prop/iana-prop. This can be decided based on property_map_was_checked. This avoids extra (expensive) lookups in our self._get_property_mapping.

Parameters:

Name Type Description Default
property_instance Property

The Property we wish to set.

required
property_map_info Optional[Tuple[str, Type[Property], bool]]

A tuple containing the variable name for this Component instance, the type of the Property and whether the variable can occur multiple times for the same property. If it equals None, this either means it is an x-prop/iana-prop or that the property_map was not checked yet.

None
property_map_was_checked bool

Whether the property_map_info was passed or not. If True, and property_map_info is None, we know that it is an iana property. If False, we still need to consult _get_property_mapping.

False
Source code in ical_library/base_classes/component.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def set_property(
    self,
    property_instance: Property,
    property_map_info: Optional[Tuple[str, Type[Property], bool]] = None,
    property_map_was_checked: bool = False,
) -> None:
    """
    Setting a property for a Component instance.

    If the `property_map_info` is equal to None, we either have not yet looked up all the properties or it is an
    x-prop/iana-prop. This can be decided based on property_map_was_checked. This avoids extra (expensive) lookups
    in our self._get_property_mapping.

    :param property_instance: The Property we wish to set.
    :param property_map_info: A tuple containing the variable name for this Component instance, the type of the
    Property and whether the variable can occur multiple times for the same property. If it equals None, this either
    means it is an x-prop/iana-prop or that the property_map was not checked yet.
    :param property_map_was_checked: Whether the `property_map_info` was passed or not. If True, and
    `property_map_info` is `None`, we know that it is an iana property. If False, we still need to consult
    `_get_property_mapping`.
    """
    if property_map_info is None and property_map_was_checked is False:
        property_map_info = self._get_property_mapping().get(property_instance.name)
    var_name, var_type, is_list = property_map_info or [None, None, None]
    if var_name is not None and is_list is not None:
        if is_list is True:
            if getattr(self, var_name) is None:
                setattr(self, var_name, [property_instance])
            else:
                current_value: List[Property] = getattr(self, var_name)
                current_value.append(property_instance)
        else:
            setattr(self, var_name, property_instance)
    else:
        self._extra_properties[property_instance.name.lower().replace("-", "_")].append(property_instance)

ComponentContext

Component context is used to keep the current Component when Component is used as ContextManager.

You can use components as context: with Component() as my_component:.

If you do this the context stores the Component and whenever a new component or property is created, it will use such stored Component as the parent Component.

Source code in ical_library/help_modules/component_context.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class ComponentContext:
    """
    Component context is used to keep the current Component when Component is used as ContextManager.

    You can use components as context: `#!py3 with Component() as my_component:`.

    If you do this the context stores the Component and whenever a new component or property is created, it will use
    such stored Component as the parent Component.
    """

    _context_managed_component: Optional["Component"] = None
    _previous_context_managed_components: List["Component"] = []

    @classmethod
    def push_context_managed_component(cls, component: "Component"):
        """Set the current context managed component."""
        if cls._context_managed_component:
            cls._previous_context_managed_components.append(cls._context_managed_component)
        cls._context_managed_component = component

    @classmethod
    def pop_context_managed_component(cls) -> Optional["Component"]:
        """Pop the current context managed component."""
        old_component = cls._context_managed_component
        if cls._previous_context_managed_components:
            cls._context_managed_component = cls._previous_context_managed_components.pop()
        else:
            cls._context_managed_component = None
        return old_component

    @classmethod
    def get_current_component(cls) -> Optional["Component"]:
        """Get the current context managed component."""
        return cls._context_managed_component

get_current_component() classmethod

Get the current context managed component.

Source code in ical_library/help_modules/component_context.py
37
38
39
40
@classmethod
def get_current_component(cls) -> Optional["Component"]:
    """Get the current context managed component."""
    return cls._context_managed_component

pop_context_managed_component() classmethod

Pop the current context managed component.

Source code in ical_library/help_modules/component_context.py
27
28
29
30
31
32
33
34
35
@classmethod
def pop_context_managed_component(cls) -> Optional["Component"]:
    """Pop the current context managed component."""
    old_component = cls._context_managed_component
    if cls._previous_context_managed_components:
        cls._context_managed_component = cls._previous_context_managed_components.pop()
    else:
        cls._context_managed_component = None
    return old_component

push_context_managed_component(component) classmethod

Set the current context managed component.

Source code in ical_library/help_modules/component_context.py
20
21
22
23
24
25
@classmethod
def push_context_managed_component(cls, component: "Component"):
    """Set the current context managed component."""
    if cls._context_managed_component:
        cls._previous_context_managed_components.append(cls._context_managed_component)
    cls._context_managed_component = component