From 65537b4cb5918d5b82608d14dfa07a76dd515abb Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 26 Jun 2024 23:59:49 +0100 Subject: [PATCH 01/11] Test examples from RFC 7529 --- src/icalendar/cal.py | 16 ++++++++---- src/icalendar/tests/calendars/rfc_7529.ics | 29 ++++++++++++++++++++++ src/icalendar/tests/test_rfc_7529.py | 25 +++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 src/icalendar/tests/calendars/rfc_7529.ics create mode 100644 src/icalendar/tests/test_rfc_7529.py diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index a4f2a644..3c250596 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -275,23 +275,29 @@ def add_component(self, component): """ self.subcomponents.append(component) - def _walk(self, name): + def _walk(self, name, select): """Walk to given component. """ result = [] - if name is None or self.name == name: + if (name is None or self.name == name) and select(self): result.append(self) for subcomponent in self.subcomponents: - result += subcomponent._walk(name) + result += subcomponent._walk(name, select) return result - def walk(self, name=None): + def walk(self, name=None, select=lambda c: True): """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) + return self._walk(name, select) ##################### # Generation diff --git a/src/icalendar/tests/calendars/rfc_7529.ics b/src/icalendar/tests/calendars/rfc_7529.ics new file mode 100644 index 00000000..1ba3e1f1 --- /dev/null +++ b/src/icalendar/tests/calendars/rfc_7529.ics @@ -0,0 +1,29 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID://RESEARCH IN MOTION//BIS 3.0 +METHOD:REQUEST +BEGIN:VEVENT +UID:4.3.1 +DTSTART;VALUE=DATE:20130210 +RRULE:RSCALE=CHINESE;FREQ=YEARLY +SUMMARY:Chinese New Year +END:VEVENT +BEGIN:VEVENT +UID:4.3.2 +DTSTART;VALUE=DATE:20130906 +RRULE:RSCALE=ETHIOPIC;FREQ=MONTHLY;BYMONTH=13 +SUMMARY:First day of 13th month +END:VEVENT +BEGIN:VEVENT +UID:4.3.3 +DTSTART;VALUE=DATE:20140208 +RRULE:RSCALE=HEBREW;FREQ=YEARLY;BYMONTH=5L;BYMONTHDAY=8;SKIP=FORWARD +SUMMARY:Anniversary +END:VEVENT +BEGIN:VEVENT +UID:4.3.4 +DTSTART;VALUE=DATE:20120229 +RRULE:RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=FORWARD +SUMMARY:Anniversary +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/test_rfc_7529.py b/src/icalendar/tests/test_rfc_7529.py new file mode 100644 index 00000000..5c21a24a --- /dev/null +++ b/src/icalendar/tests/test_rfc_7529.py @@ -0,0 +1,25 @@ +"""This tests the compatibility with RFC 7529. + +See +- https://github.com/collective/icalendar/issues/653 +- https://www.rfc-editor.org/rfc/rfc7529.html +""" +import pytest + + +@pytest.mark.parametrize( + "uid,scale", + [ + ("4.3.1", "CHINESE"), + ("4.3.2", "ETHIOPIC"), + ("4.3.3", "HEBREW"), + ("4.3.4", "GREGORIAN"), + ] +) +def test_rscale(calendars, uid, scale): + """Check that the RSCALE is parsed correctly.""" + event = calendars.rfc_7529.walk(select=lambda c: c.get("UID") == uid)[0] + print(event.errors) + rrule = event["RRULE"] + print(rrule) + assert rrule["RSCALE"] == [scale] From 58d10464e59e39474211e322b9400e9824ab5771 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Thu, 27 Jun 2024 00:04:37 +0100 Subject: [PATCH 02/11] log changes --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 349fa43e..e9f54bdf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -60,6 +60,8 @@ New features: - Test compatibility with Python 3.12 - Add function ``icalendar.use_pytz()``. +- Allows selecting components with ``walk(select=func)`` where ``func`` takes a + component and returns ``True``/``False``. Bug fixes: From 003602daed1c888d8169532964ecb8868811d97c Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Fri, 28 Jun 2024 15:55:00 +0100 Subject: [PATCH 03/11] Add vMonth to represent the leap month parameter for RFC 7529 --- src/icalendar/parser_tools.py | 9 ++- src/icalendar/prop.py | 106 +++++++++++++++++++++++---- src/icalendar/tests/test_rfc_7529.py | 39 ++++++++++ 3 files changed, 134 insertions(+), 20 deletions(-) diff --git a/src/icalendar/parser_tools.py b/src/icalendar/parser_tools.py index 45c0c238..045c56d1 100644 --- a/src/icalendar/parser_tools.py +++ b/src/icalendar/parser_tools.py @@ -1,10 +1,11 @@ -from typing import Any +from typing import Any, Union SEQUENCE_TYPES = (list, tuple) DEFAULT_ENCODING = 'utf-8' +ICAL_TYPE = Union[str, bytes] -def from_unicode(value: Any, encoding='utf-8') -> bytes: +def from_unicode(value: ICAL_TYPE, encoding='utf-8') -> bytes: """ Converts a value to bytes, even if it already is bytes :param value: The value to convert @@ -21,7 +22,7 @@ def from_unicode(value: Any, encoding='utf-8') -> bytes: return value -def to_unicode(value, encoding='utf-8'): +def to_unicode(value:ICAL_TYPE, encoding='utf-8') -> str: """Converts a value to unicode, even if it is already a unicode string. """ if isinstance(value, str): @@ -34,7 +35,7 @@ def to_unicode(value, encoding='utf-8'): return value -def data_encode(data, encoding=DEFAULT_ENCODING): +def data_encode(data:Union[ICAL_TYPE, dict, list], encoding=DEFAULT_ENCODING) -> bytes: """Encode all datastructures to the given encoding. Currently unicode strings, dicts and lists are supported. """ diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index d61ef547..ca3171f5 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -44,10 +44,9 @@ from icalendar.parser import Parameters from icalendar.parser import escape_char from icalendar.parser import unescape_char -from icalendar.parser_tools import DEFAULT_ENCODING -from icalendar.parser_tools import SEQUENCE_TYPES -from icalendar.parser_tools import to_unicode -from icalendar.parser_tools import from_unicode +from icalendar.parser_tools import ( + DEFAULT_ENCODING, SEQUENCE_TYPES, to_unicode, from_unicode, ICAL_TYPE +) import base64 import binascii @@ -55,7 +54,7 @@ import re import time as _time -from typing import Optional +from typing import Optional, Union DURATION_REGEX = re.compile(r'([-+]?)P(?:(\d+)W)?(?:(\d+)D)?' @@ -176,11 +175,11 @@ def __new__(cls, *args, **kwargs): self.params = Parameters() return self - def to_ical(self): + def to_ical(self) -> bytes: return str(self).encode('utf-8') @classmethod - def from_ical(cls, ical): + def from_ical(cls, ical:ICAL_TYPE): try: return cls(ical) except Exception: @@ -610,6 +609,76 @@ def from_ical(cls, ical): raise ValueError(f'Expected frequency, got: {ical}') +class vMonth(int): + """The number of the month for recurrence. + + In :rfc:`5545`, this is just an int. + In :rfc:`7529`, this can be followed by `L` to indicate a leap month. + + >>> vMonth(1) # first month January + vMonth('1') + >>> vMonth("5L") # leap month in Hebrew calendar + vMonth('5L') + >>> vMonth(1).leap + False + >>> vMonth("5L").leap + True + + Definition from RFC:: + + type-bymonth = element bymonth { + xsd:positiveInteger | + xsd:string + } + """ + def __new__(cls, month:Union[str, int]): + if isinstance(month, vMonth): + return cls(month.to_ical().decode()) + if isinstance(month, str): + if month.isdigit(): + month_index = int(month) + leap = False + else: + if not month[-1] == "L" and month[:-1].isdigit(): + raise ValueError(f"Invalid month: {month!r}") + month_index = int(month[:-1]) + leap = True + else: + leap = False + month_index = int(month) + self = super().__new__(cls, month_index) + self.leap = leap + self.params = Parameters() + return self + + def to_ical(self) -> bytes: + """The ical representation.""" + return str(self).encode('utf-8') + + @classmethod + def from_ical(cls, ical: str): + return cls(ical) + + def leap(): + doc = "Whether this is a leap month." + def fget(self) -> bool: + return self._leap + def fset(self, value:bool) -> None: + self._leap = value + return locals() + leap = property(**leap()) + + + def __repr__(self) -> str: + """repr(self)""" + return f"{self.__class__.__name__}({str(self)!r})" + + def __str__(self) -> str: + """str(self)""" + return f"{int(self)}{'L' if self.leap else ''}" + + + class vRecur(CaselessDict): """Recurrence definition. """ @@ -619,7 +688,7 @@ class vRecur(CaselessDict): # Mac iCal ignores RRULEs where FREQ is not the first rule part. # Sorts parts according to the order listed in RFC 5545, section 3.3.10. - canonical_order = ("FREQ", "UNTIL", "COUNT", "INTERVAL", + canonical_order = ("RSCALE", "FREQ", "UNTIL", "COUNT", "INTERVAL", "BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY", "BYWEEKDAY", "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH", "BYSETPOS", "WKST") @@ -633,7 +702,7 @@ class vRecur(CaselessDict): 'BYWEEKNO': vInt, 'BYMONTHDAY': vInt, 'BYYEARDAY': vInt, - 'BYMONTH': vInt, + 'BYMONTH': vMonth, 'UNTIL': vDDDTypes, 'BYSETPOS': vInt, 'WKST': vWeekday, @@ -643,6 +712,9 @@ class vRecur(CaselessDict): }) def __init__(self, *args, **kwargs): + for k, v in kwargs.items(): + if not isinstance(v, SEQUENCE_TYPES): + kwargs[k] = [v] super().__init__(*args, **kwargs) self.params = Parameters() @@ -667,7 +739,7 @@ def parse_type(cls, key, values): return [parser.from_ical(v) for v in values.split(',')] @classmethod - def from_ical(cls, ical): + def from_ical(cls, ical: str): if isinstance(ical, cls): return ical try: @@ -680,8 +752,10 @@ def from_ical(cls, ical): # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU; continue recur[key] = cls.parse_type(key, vals) - return dict(recur) - except Exception: + return cls(recur) + except ValueError: + raise + except: raise ValueError(f'Error in recurrence rule: {ical}') @@ -696,14 +770,14 @@ def __new__(cls, value, encoding=DEFAULT_ENCODING): self.params = Parameters() return self - def __repr__(self): - return f"vText('{self.to_ical()!r}')" + def __repr__(self) -> str: + return f"vText({self.to_ical()!r})" - def to_ical(self): + def to_ical(self) -> bytes: return escape_char(self).encode(self.encoding) @classmethod - def from_ical(cls, ical): + def from_ical(cls, ical:ICAL_TYPE): ical_unesc = unescape_char(ical) return cls(ical_unesc) diff --git a/src/icalendar/tests/test_rfc_7529.py b/src/icalendar/tests/test_rfc_7529.py index 5c21a24a..1691a5a8 100644 --- a/src/icalendar/tests/test_rfc_7529.py +++ b/src/icalendar/tests/test_rfc_7529.py @@ -5,6 +5,7 @@ - https://www.rfc-editor.org/rfc/rfc7529.html """ import pytest +from icalendar.prop import vRecur, vMonth @pytest.mark.parametrize( @@ -23,3 +24,41 @@ def test_rscale(calendars, uid, scale): rrule = event["RRULE"] print(rrule) assert rrule["RSCALE"] == [scale] + + +@pytest.mark.parametrize( + "uid,skip", + [ + ("4.3.2", None), + ("4.3.3", ["FORWARD"]), + ] +) +def test_rscale(calendars, uid, skip): + """Check that the RSCALE is parsed correctly.""" + event = calendars.rfc_7529.walk(select=lambda c: c.get("UID") == uid)[0] + recur = event["RRULE"] + assert recur.get("SKIP") == skip + + +def test_leap_month(calendars): + """Check that we can parse the leap month.""" + event = calendars.rfc_7529.walk(select=lambda c: c.get("UID") == "4.3.3")[0] + recur = event["RRULE"] + assert recur["BYMONTH"][0].leap is True + + +@pytest.mark.parametrize( + "ty, recur, ics", + [ + (vRecur, vRecur(rscale="CHINESE", freq="YEARLY"), b"RSCALE=CHINESE;FREQ=YEARLY"), + (vRecur, vRecur(bymonth=vMonth(10)), b"BYMONTH=10"), + (vRecur, vRecur(bymonth=vMonth("5L")), b"BYMONTH=5L"), + (vMonth, vMonth(10), b"10"), + (vMonth, vMonth("5L"), b"5L"), + ] +) +def test_conversion(ty, recur, ics): + """Test string conversion.""" + assert recur.to_ical() == ics + assert ty.from_ical(ics.decode()) == recur + assert ty.from_ical(ics.decode()).to_ical() == ics From 844c7e921c12a56a30e33eb65343645c0f950ad8 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Fri, 28 Jun 2024 16:18:01 +0100 Subject: [PATCH 04/11] Create vSkip class --- src/icalendar/prop.py | 62 +++++++++++++++++----------- src/icalendar/tests/test_rfc_7529.py | 9 +++- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index ca3171f5..d82df19f 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -55,6 +55,7 @@ import time as _time from typing import Optional, Union +from enum import Enum, auto DURATION_REGEX = re.compile(r'([-+]?)P(?:(\d+)W)?(?:(\d+)D)?' @@ -125,6 +126,29 @@ def from_ical(cls, ical): raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}") +class vText(str): + """Simple text. + """ + + def __new__(cls, value, encoding=DEFAULT_ENCODING): + value = to_unicode(value, encoding=encoding) + self = super().__new__(cls, value) + self.encoding = encoding + self.params = Parameters() + return self + + def __repr__(self) -> str: + return f"vText({self.to_ical()!r})" + + def to_ical(self) -> bytes: + return escape_char(self).encode(self.encoding) + + @classmethod + def from_ical(cls, ical:ICAL_TYPE): + ical_unesc = unescape_char(ical) + return cls(ical_unesc) + + class vCalAddress(str): """This just returns an unquoted string. """ @@ -678,6 +702,18 @@ def __str__(self) -> str: return f"{int(self)}{'L' if self.leap else ''}" +class vSkip(vText, Enum): + """Skip values for RRULE. + + These are defined in :rfc:`7529`. + + OMIT is the default value. + """ + + OMIT = "OMIT" + FORWARD = "FORWARD" + BACKWARD = "BACKWARD" + class vRecur(CaselessDict): """Recurrence definition. @@ -691,7 +727,7 @@ class vRecur(CaselessDict): canonical_order = ("RSCALE", "FREQ", "UNTIL", "COUNT", "INTERVAL", "BYSECOND", "BYMINUTE", "BYHOUR", "BYDAY", "BYWEEKDAY", "BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH", - "BYSETPOS", "WKST") + "BYSETPOS", "WKST", "SKIP") types = CaselessDict({ 'COUNT': vInt, @@ -709,6 +745,7 @@ class vRecur(CaselessDict): 'BYDAY': vWeekday, 'FREQ': vFrequency, 'BYWEEKDAY': vWeekday, + 'SKIP': vSkip, }) def __init__(self, *args, **kwargs): @@ -759,29 +796,6 @@ def from_ical(cls, ical: str): raise ValueError(f'Error in recurrence rule: {ical}') -class vText(str): - """Simple text. - """ - - def __new__(cls, value, encoding=DEFAULT_ENCODING): - value = to_unicode(value, encoding=encoding) - self = super().__new__(cls, value) - self.encoding = encoding - self.params = Parameters() - return self - - def __repr__(self) -> str: - return f"vText({self.to_ical()!r})" - - def to_ical(self) -> bytes: - return escape_char(self).encode(self.encoding) - - @classmethod - def from_ical(cls, ical:ICAL_TYPE): - ical_unesc = unescape_char(ical) - return cls(ical_unesc) - - class vTime(TimeBase): """Render and generates iCalendar time format. """ diff --git a/src/icalendar/tests/test_rfc_7529.py b/src/icalendar/tests/test_rfc_7529.py index 1691a5a8..15265358 100644 --- a/src/icalendar/tests/test_rfc_7529.py +++ b/src/icalendar/tests/test_rfc_7529.py @@ -5,7 +5,7 @@ - https://www.rfc-editor.org/rfc/rfc7529.html """ import pytest -from icalendar.prop import vRecur, vMonth +from icalendar.prop import vRecur, vMonth, vSkip @pytest.mark.parametrize( @@ -55,6 +55,13 @@ def test_leap_month(calendars): (vRecur, vRecur(bymonth=vMonth("5L")), b"BYMONTH=5L"), (vMonth, vMonth(10), b"10"), (vMonth, vMonth("5L"), b"5L"), + (vSkip, vSkip.OMIT, b"OMIT"), + (vSkip, vSkip.BACKWARD, b"BACKWARD"), + (vSkip, vSkip.FORWARD, b"FORWARD"), + (vSkip, vSkip("OMIT"), b"OMIT"), + (vSkip, vSkip("BACKWARD"), b"BACKWARD"), + (vSkip, vSkip("FORWARD"), b"FORWARD"), + (vRecur, vRecur(rscale="GREGORIAN", freq="YEARLY", skip='FORWARD'), b"RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=FORWARD"), ] ) def test_conversion(ty, recur, ics): From dc3eaaebf3d5453bb3f6968d6dbc70ee6ecef8e7 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Fri, 28 Jun 2024 16:18:45 +0100 Subject: [PATCH 05/11] add one testcase --- src/icalendar/tests/test_rfc_7529.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/icalendar/tests/test_rfc_7529.py b/src/icalendar/tests/test_rfc_7529.py index 15265358..dd869e2a 100644 --- a/src/icalendar/tests/test_rfc_7529.py +++ b/src/icalendar/tests/test_rfc_7529.py @@ -62,6 +62,7 @@ def test_leap_month(calendars): (vSkip, vSkip("BACKWARD"), b"BACKWARD"), (vSkip, vSkip("FORWARD"), b"FORWARD"), (vRecur, vRecur(rscale="GREGORIAN", freq="YEARLY", skip='FORWARD'), b"RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=FORWARD"), + (vRecur, vRecur(rscale="GREGORIAN", freq="YEARLY", skip=vSkip.FORWARD), b"RSCALE=GREGORIAN;FREQ=YEARLY;SKIP=FORWARD"), ] ) def test_conversion(ty, recur, ics): From 0c3b6d0a47171068044967ab8f8813b41473e111 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Fri, 28 Jun 2024 16:27:19 +0100 Subject: [PATCH 06/11] allow copy of vSkip --- src/icalendar/prop.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index d82df19f..e2d6ef60 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -714,6 +714,10 @@ class vSkip(vText, Enum): FORWARD = "FORWARD" BACKWARD = "BACKWARD" + def __reduce_ex__(self, _p): + """For pickling.""" + return self.__class__, (self._name_,) + class vRecur(CaselessDict): """Recurrence definition. From c03caeb85b1d1e746b92ed92d1573a11b8c9a6b4 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 29 Jun 2024 17:25:14 +0100 Subject: [PATCH 07/11] Document RFCs see also https://github.com/collective/icalendar/issues/589 --- docs/usage.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 35ea03b5..e73fe742 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -7,6 +7,23 @@ standard in RFC 5545. It should be fully compliant, but it is possible to generate and parse invalid files if you really want to. +Compatibility +------------- + +This package is compatible with the following standards: + +- :rfc:`5545` +- :rfc:`7529` + +We do not claim compatibility to: + +- :rfc:`2445` - which is obsoleted by :rfc:`5545` +- :rfc:`6886` +- :rfc:`7953` +- :rfc:`7986` +- :rfc:`9073` +- :rfc:`9074` +- :rfc:`9253` File structure -------------- From 0ea39005d2e55ca289e62648a42366d701f7c3d3 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sat, 29 Jun 2024 17:28:05 +0100 Subject: [PATCH 08/11] log changes --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index e9f54bdf..cc44b82c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -62,6 +62,7 @@ New features: - Add function ``icalendar.use_pytz()``. - Allows selecting components with ``walk(select=func)`` where ``func`` takes a component and returns ``True``/``False``. +- Add compatibility to :rfc:`7529`, adding ``vMonth`` and ``vSkip`` Bug fixes: From 810679b180b99e6e5fea27f5f8fbdfc48dd13120 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sun, 30 Jun 2024 22:57:00 +0100 Subject: [PATCH 09/11] Update src/icalendar/parser_tools.py Co-authored-by: Steve Piercy --- src/icalendar/parser_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/parser_tools.py b/src/icalendar/parser_tools.py index 045c56d1..6779b7a5 100644 --- a/src/icalendar/parser_tools.py +++ b/src/icalendar/parser_tools.py @@ -35,7 +35,7 @@ def to_unicode(value:ICAL_TYPE, encoding='utf-8') -> str: return value -def data_encode(data:Union[ICAL_TYPE, dict, list], encoding=DEFAULT_ENCODING) -> bytes: +def data_encode(data: Union[ICAL_TYPE, dict, list], encoding=DEFAULT_ENCODING) -> bytes: """Encode all datastructures to the given encoding. Currently unicode strings, dicts and lists are supported. """ From df6fd88d3dd821755fd5e1e17f59ac614ac4efa7 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sun, 30 Jun 2024 23:02:52 +0100 Subject: [PATCH 10/11] Update src/icalendar/parser_tools.py Co-authored-by: Steve Piercy --- src/icalendar/parser_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/icalendar/parser_tools.py b/src/icalendar/parser_tools.py index 6779b7a5..47ad8092 100644 --- a/src/icalendar/parser_tools.py +++ b/src/icalendar/parser_tools.py @@ -22,7 +22,7 @@ def from_unicode(value: ICAL_TYPE, encoding='utf-8') -> bytes: return value -def to_unicode(value:ICAL_TYPE, encoding='utf-8') -> str: +def to_unicode(value: ICAL_TYPE, encoding='utf-8') -> str: """Converts a value to unicode, even if it is already a unicode string. """ if isinstance(value, str): From 88d72e8c176ad5760a7db01cc88ddfb07f54efd2 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sun, 30 Jun 2024 23:03:39 +0100 Subject: [PATCH 11/11] Update CHANGES.rst Co-authored-by: Steve Piercy --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index cc44b82c..42d99603 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -61,7 +61,7 @@ New features: - Test compatibility with Python 3.12 - Add function ``icalendar.use_pytz()``. - Allows selecting components with ``walk(select=func)`` where ``func`` takes a - component and returns ``True``/``False``. + component and returns ``True`` or ``False``. - Add compatibility to :rfc:`7529`, adding ``vMonth`` and ``vSkip`` Bug fixes: