From 800e990d700969b3bfcc38826c604f6541f3f381 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Fri, 18 Apr 2025 18:16:37 +0100 Subject: [PATCH] Add VCALENDAR properties NAME, DESCRIPTION and COLOR as attributes --- src/icalendar/attr.py | 40 ++++++++++ src/icalendar/cal.py | 106 +++++++++++++++++++++++++ src/icalendar/tests/test_rfc_7986.py | 114 +++++++++++++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 src/icalendar/attr.py create mode 100644 src/icalendar/tests/test_rfc_7986.py diff --git a/src/icalendar/attr.py b/src/icalendar/attr.py new file mode 100644 index 0000000..0d2b53b --- /dev/null +++ b/src/icalendar/attr.py @@ -0,0 +1,40 @@ +"""Attributes of Components and properties.""" +from __future__ import annotations + +from typing import Optional + + +def multi_language_text_property(main_prop:str, compatibility_prop:str, doc:str) -> property: + """This creates a text property. + + This property can be defined several times with different LANGAUGE parameters. + + Args: + + main_prop: The property to set and get, e.g. NAME + compatibility_prop: An old property used before, e.g. X-WR-CALNAME + doc: The documentation string + """ + def fget(self) -> Optional[str]: + """Get the property""" + result = self.get(main_prop, self.get(compatibility_prop)) + if isinstance(result, list): + for item in result: + if "LANGUAGE" not in item.params: + return item + return result + + def fset(self, value:str): + """Set the property.""" + fdel(self) + self.add(main_prop, value) + + def fdel(self): + """Delete the property.""" + self.pop(main_prop, None) + self.pop(compatibility_prop, None) + + return property(fget, fset, fdel, doc) + + +__all__ = ["multi_language_text_property"] diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 9309b7b..6e5190c 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, List, NamedTuple, Optional, Tuple, Union import dateutil.rrule import dateutil.tz +from icalendar.attr import multi_language_text_property from icalendar.caselessdict import CaselessDict from icalendar.parser import Contentline, Contentlines, Parameters, q_join, q_split from icalendar.parser_tools import DEFAULT_ENCODING @@ -1898,6 +1899,10 @@ class Calendar(Component): "PRODID", "CALSCALE", "METHOD", + "NAME", + "X-WR-CALNAME", + "DESCRIPTION", + "X-WR-CALDESC", ) required = ( "PRODID", @@ -2055,6 +2060,107 @@ class Calendar(Component): continue self.add_component(timezone) + calendar_name = multi_language_text_property( + "NAME", "X-WR-CALNAME", + """This property specifies the name of the calendar + + This takes care of :rfc:`7986` ``NAME`` and ``X-WR-CALNAME``. + + Property Parameters: + + IANA, non-standard, alternate text + representation, and language property parameters can be specified + on this property. + + Conformance: + + This property can be specified multiple times in an + iCalendar object. However, each property MUST represent the name + of the calendar in a different language. + + Description: + + This property is used to specify a name of the + iCalendar object that can be used by calendar user agents when + presenting the calendar data to a user. Whilst a calendar only + has a single name, multiple language variants can be specified by + including this property multiple times with different "LANGUAGE" + parameter values on each. + + >>> from icalendar import Calendar + >>> calendar = Calendar() + >>> calendar.calendar_name = "My Calendar" + >>> print(calendar.to_ical()) + BEGIN:VCALENDAR + NAME:My Calendar + END:VCALENDAR + """) + + description = calendar_description = multi_language_text_property( + "DESCRIPTION", "X-WR-CALDESC", + """This property specifies the description of the calendar. + + This takes care of :rfc:`7986` ``DESCRIPTION`` and ``X-WR-CALDESC``. + + Conformance: + + This property can be specified multiple times in an + iCalendar object. However, each property MUST represent the + description of the calendar in a different language. + + Description: + + This property is used to specify a lengthy textual + description of the iCalendar object that can be used by calendar + user agents when describing the nature of the calendar data to a + user. Whilst a calendar only has a single description, multiple + language variants can be specified by including this property + multiple times with different "LANGUAGE" parameter values on each. + + >>> from icalendar import Calendar + >>> calendar = Calendar() + >>> calendar.description = "This is a calendar" + >>> print(calendar.to_ical()) + BEGIN:VCALENDAR + DESCRIPTION:This is a calendar + END:VCALENDAR + """) + + color = calendar_color = multi_language_text_property( + "COLOR", "X-APPLE-CALENDAR-COLOR", + """This property specifies a color used for displaying the calendar. + + This takes care of :rfc:`7986` ``COLOR`` and ``X-APPLE-CALENDAR-COLOR``. + + Property Parameters: + + IANA and non-standard property parameters can + be specified on this property. + + Conformance: + + This property can be specified once in an iCalendar + object or in "VEVENT", "VTODO", or "VJOURNAL" calendar components. + + Description: + + This property specifies a color that clients MAY use + when presenting the relevant data to a user. Typically, this + would appear as the "background" color of events or tasks. The + value is a case-insensitive color name taken from the CSS3 set of + names, defined in Section 4.3 of [W3C.REC-css3-color-20110607]. + + Example: ``"turquoise"``, ``"#ffffff"`` + + >>> from icalendar import Calendar + >>> calendar = Calendar() + >>> calendar.color = "black" + >>> print(calendar.to_ical()) + BEGIN:VCALENDAR + COLOR:black + END:VCALENDAR + """ + ) # These are read only singleton, so one instance is enough for the module types_factory = TypesFactory() diff --git a/src/icalendar/tests/test_rfc_7986.py b/src/icalendar/tests/test_rfc_7986.py new file mode 100644 index 0000000..1d07dc9 --- /dev/null +++ b/src/icalendar/tests/test_rfc_7986.py @@ -0,0 +1,114 @@ +"""This tests additional attributes from RFC 7986. + +Some attributes are also available as X-... attributes. +They are also considered. +""" + +import pytest + +from icalendar import Calendar +from icalendar.prop import vText + + +@pytest.fixture() +def calendar() -> Calendar: + """Empty calendar""" + return Calendar() + + +param_name = pytest.mark.parametrize("name", ["Company Vacation Days", "Calendar Name"]) +param_prop = pytest.mark.parametrize("prop", ["NAME", "X-WR-CALNAME"]) + + +@param_prop +@param_name +def test_get_calendar_name(prop, name, calendar): + """Get the name of the calendar.""" + calendar.add(prop, name) + assert calendar.calendar_name == name + + +@param_name +def test_set_calendar_name(name, calendar): + """Setting the name overrides the old attributes.""" + calendar.calendar_name = name + assert calendar.calendar_name == name + assert calendar["NAME"] == name + + +@param_name +@param_prop +def test_replace_name(name, prop, calendar): + """Setting the name overrides the old attributes.""" + calendar[prop] = "Other Name" + calendar.calendar_name = name + assert calendar.calendar_name == name + + +@param_name +@param_prop +def test_del_name(name, calendar, prop): + """Delete the name.""" + calendar.add(prop, name) + del calendar.calendar_name + assert calendar.calendar_name is None + + +def test_default_name(calendar): + """We have no name by default.""" + assert calendar.calendar_name is None + + +@param_name +def test_setting_the_name_deletes_the_non_standard_attribute(calendar, name): + """The default_attr is deleted when setting the name.""" + calendar["X-WR-CALNAME"] = name + assert "X-WR-CALNAME" in calendar + calendar.calendar_name = "other name" + assert "X-WR-CALNAME" not in calendar + + +@param_name +@pytest.mark.parametrize("order", [1, 2]) +def test_multiple_names_use_the_one_without_a_language(calendar, name, order): + """Add several names and use the one without a language param.""" + if order == 1: + calendar.add("NAME", name) + calendar.add("NAME", vText("Kalendername", params={"LANGUAGE":"de"})) + if order == 2: + calendar.add("NAME", name) + assert calendar.calendar_name == name + + +@param_name +def test_name_is_preferred(calendar, name): + """NAME is more important that X-WR-CALNAME""" + calendar.add("NAME", name) + calendar.add("X-WR-CALNAME", "asd") + assert calendar.calendar_name == name + + + +# For description, we would use the same tests as name but we also use the +# same code, so it is alright. + +param_color = pytest.mark.parametrize("desc", ["DESCRIPTION", "X-WR-CALDESC"]) + +@param_color +@param_name +def test_description(calendar, desc, name): + """Get the value""" + calendar.add(desc, name) + assert calendar.calendar_description == name + +# For color, we would use the same tests as name but we also use the +# same code, so it is alright. + +param_color = pytest.mark.parametrize("color", ["COLOR", "X-APPLE-CALENDAR-COLOR"]) + +@param_color +@param_name +def test_color(calendar, color, name): + """Get the value""" + calendar.add(color, name) + assert calendar.calendar_color == name