kopia lustrzana https://github.com/collective/icalendar
commit
33f554fb2f
|
|
@ -17,6 +17,7 @@ New features:
|
|||
|
||||
- Add attributes to the calendar for properties ``NAME``, ``DESCRIPTION``, and ``COLOR``. See `Issue 655 <https://github.com/collective/icalendar/issues/655>`_.
|
||||
- Add ``sequence`` attribute to ``Event``, ``Todo``, and ``Journal`` components. See `Issue 802 <https://github.com/collective/icalendar/issues/802>`_.
|
||||
- Add compatibility to :rfc:`6868`. See `Issue 652 <https://github.com/collective/icalendar/issues/652>`_.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ Compatibility
|
|||
|
||||
This package is compatible with the following standards:
|
||||
|
||||
- :rfc:`2445` - obsoleted by :rfc:`5545`
|
||||
- :rfc:`5545` - Internet Calendaring and Scheduling Core Object Specification (iCalendar)
|
||||
- :rfc:`6868` - Parameter Value Encoding in iCalendar and vCard
|
||||
- :rfc:`7529` - Non-Gregorian Recurrence Rules in the Internet Calendaring and Scheduling Core Object Specification (iCalendar)
|
||||
- :rfc:`9074` - "VALARM" Extensions for iCalendar
|
||||
|
||||
We do not claim compatibility to the following RFCs. They might work though.
|
||||
|
||||
- :rfc:`2445` - obsoleted by :rfc:`5545`
|
||||
- :rfc:`6868` - Parameter Value Encoding in iCalendar and vCard
|
||||
- :rfc:`7953` - Calendar Availability
|
||||
- :rfc:`7986` - New Properties for iCalendar
|
||||
- :rfc:`9073` - Event Publishing Extensions to iCalendar
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ It is stupid in the sense that it treats the content purely as strings. No type
|
|||
conversion is attempted.
|
||||
"""
|
||||
|
||||
import os
|
||||
from icalendar.caselessdict import CaselessDict
|
||||
from icalendar.parser_tools import DEFAULT_ENCODING
|
||||
from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE
|
||||
from icalendar.parser_tools import SEQUENCE_TYPES
|
||||
from icalendar.parser_tools import to_unicode
|
||||
|
||||
|
|
@ -94,10 +95,9 @@ def param_value(value):
|
|||
"""Returns a parameter value."""
|
||||
if isinstance(value, SEQUENCE_TYPES):
|
||||
return q_join(value)
|
||||
elif isinstance(value, str):
|
||||
return dquote(value)
|
||||
else:
|
||||
return dquote(value.to_ical().decode(DEFAULT_ENCODING))
|
||||
if isinstance(value, str):
|
||||
return dquote(rfc_6868_escape(value))
|
||||
return dquote(rfc_6868_escape(value.to_ical().decode(DEFAULT_ENCODING)))
|
||||
|
||||
|
||||
# Could be improved
|
||||
|
|
@ -232,13 +232,13 @@ class Parameters(CaselessDict):
|
|||
if v.startswith('"') and v.endswith('"'):
|
||||
v = v.strip('"')
|
||||
validate_param_value(v, quoted=True)
|
||||
vals.append(v)
|
||||
vals.append(rfc_6868_unescape(v))
|
||||
else:
|
||||
validate_param_value(v, quoted=False)
|
||||
if strict:
|
||||
vals.append(v.upper())
|
||||
vals.append(rfc_6868_unescape(v.upper()))
|
||||
else:
|
||||
vals.append(v)
|
||||
vals.append(rfc_6868_unescape(v))
|
||||
if not vals:
|
||||
result[key] = val
|
||||
else:
|
||||
|
|
@ -270,6 +270,43 @@ def unescape_string(val):
|
|||
)
|
||||
|
||||
|
||||
RFC_6868_UNESCAPE_REGEX = re.compile(r"\^\^|\^n|\^'")
|
||||
|
||||
|
||||
def rfc_6868_unescape(param_value: str) -> str:
|
||||
"""Take care of :rfc:`6868` unescaping.
|
||||
|
||||
- ^^ -> ^
|
||||
- ^n -> system specific newline
|
||||
- ^' -> "
|
||||
- ^ with others stay intact
|
||||
"""
|
||||
replacements = {
|
||||
"^^": "^",
|
||||
"^n": os.linesep,
|
||||
"^'": '"',
|
||||
}
|
||||
return RFC_6868_UNESCAPE_REGEX.sub(lambda m: replacements.get(m.group(0), m.group(0)), param_value)
|
||||
|
||||
|
||||
RFC_6868_ESCAPE_REGEX = re.compile(r'\^|\r\n|\r|\n|"')
|
||||
|
||||
def rfc_6868_escape(param_value: str) -> str:
|
||||
"""Take care of :rfc:`6868` escaping.
|
||||
|
||||
- ^ -> ^^
|
||||
- " -> ^'
|
||||
- newline -> ^n
|
||||
"""
|
||||
replacements = {
|
||||
"^": "^^",
|
||||
"\n": "^n",
|
||||
"\r": "^n",
|
||||
"\r\n": "^n",
|
||||
'"': "^'",
|
||||
}
|
||||
return RFC_6868_ESCAPE_REGEX.sub(lambda m: replacements.get(m.group(0), m.group(0)), param_value)
|
||||
|
||||
def unescape_list_or_string(val):
|
||||
if isinstance(val, list):
|
||||
return [unescape_string(s) for s in val]
|
||||
|
|
@ -296,7 +333,7 @@ class Contentline(str):
|
|||
return self
|
||||
|
||||
@classmethod
|
||||
def from_parts(cls, name, params, values, sorted=True):
|
||||
def from_parts(cls, name:ICAL_TYPE, params: Parameters, values, sorted=True):
|
||||
"""Turn a parts into a content line."""
|
||||
assert isinstance(params, Parameters)
|
||||
if hasattr(values, "to_ical"):
|
||||
|
|
@ -351,7 +388,7 @@ class Contentline(str):
|
|||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"Content line could not be parsed into parts: '{self}': {exc}"
|
||||
)
|
||||
) from exc
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical, strict=False):
|
||||
|
|
@ -408,6 +445,8 @@ __all__ = [
|
|||
"param_value",
|
||||
"q_join",
|
||||
"q_split",
|
||||
"rfc_6868_escape",
|
||||
"rfc_6868_unescape",
|
||||
"uFOLD",
|
||||
"unescape_char",
|
||||
"unescape_list_or_string",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
BEGIN:VCALENDAR
|
||||
X-PARAM;NEWLINE=^n;ALL=^^^'^n;UNKNOWN=^a^ ^asd:asd
|
||||
BEGIN:VEVENT
|
||||
ATTENDEE;CN=George Herman ^'Babe^' Ruth:mailto:babe@example.com
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
|
@ -89,8 +89,9 @@ def test_parameter_keys_are_uppercase():
|
|||
# a single quote in parameter value - double quote the value
|
||||
("Aramis d'Alameda", '"Aramis d\'Alameda"'),
|
||||
("Арамис д'Аламеда", '"Арамис д\'Аламеда"'),
|
||||
# double quote is replaced with single quote
|
||||
('Aramis d"Alameda', '"Aramis d\'Alameda"'),
|
||||
# Before, double quote is replaced with single quote
|
||||
# Since RFC 6868, we replace this with ^'
|
||||
('Aramis d"Alameda', '"Aramis d^\'Alameda"'),
|
||||
],
|
||||
)
|
||||
def test_quoting(cn_param, cn_quoted):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
"""This implemensts RFC 6868.
|
||||
|
||||
There are only some changes to parameters needed.
|
||||
"""
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from icalendar import Calendar
|
||||
from icalendar.parser import dquote, rfc_6868_escape, rfc_6868_unescape
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from icalendar import vCalAddress, vText
|
||||
|
||||
|
||||
param_duplicate = pytest.mark.parametrize(
|
||||
"duplicate",
|
||||
[ lambda x:x, lambda cal: Calendar.from_ical(cal.to_ical()) ]
|
||||
)
|
||||
|
||||
@param_duplicate
|
||||
def test_rfc_6868_example(calendars, duplicate):
|
||||
"""Check the example from the RFC."""
|
||||
cal : Calendar = duplicate(calendars.rfc_6868)
|
||||
attendee : vCalAddress = cal.events[0]["attendee"]
|
||||
assert attendee.name == 'George Herman "Babe" Ruth'
|
||||
|
||||
|
||||
@param_duplicate
|
||||
def test_all_parameters(calendars, duplicate):
|
||||
"""Check that all examples get decoded correctly."""
|
||||
cal : Calendar = duplicate(calendars.rfc_6868)
|
||||
param = cal["X-PARAM"].params["ALL"]
|
||||
assert param == '^"\n'
|
||||
|
||||
|
||||
@param_duplicate
|
||||
def test_unknown_character(calendars, duplicate):
|
||||
"""if a ^ (U+005E) character is followed by any character other than
|
||||
the ones above, parsers MUST leave both the ^ and the following
|
||||
character in place"""
|
||||
cal : Calendar = duplicate(calendars.rfc_6868)
|
||||
param = cal["X-PARAM"].params["UNKNOWN"]
|
||||
assert param == "^a^ ^asd"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "expected"),
|
||||
[
|
||||
("^a", "^a"),
|
||||
("^^", "^"),
|
||||
# ("^n", ), # see other test
|
||||
("^'", '"'),
|
||||
("^^a^b", "^a^b")
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"modify", [lambda x: x, lambda x: x*10, lambda x: f"asd{x}aaA^AA"]
|
||||
)
|
||||
def test_unescape(text, expected, modify):
|
||||
"""Check unescaping."""
|
||||
result = rfc_6868_unescape(modify(text))
|
||||
assert result == modify(expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"newline", ["\n", "\r\n", "\r", os.linesep])
|
||||
def test_unescape_newline(newline, monkeypatch):
|
||||
"""Unescape the newline."""
|
||||
monkeypatch.setattr(os, "linesep", newline)
|
||||
assert rfc_6868_unescape("^n") == newline
|
||||
|
||||
|
||||
param_values = pytest.mark.parametrize("text, expected", [
|
||||
("", ""),
|
||||
("^", "^^"),
|
||||
("^n", "^^n"),
|
||||
("This text\n has\r several\r\n lines", "This text^n has^n several^n lines"),
|
||||
('Call me "Harry".', "Call me ^'Harry^'."),
|
||||
]
|
||||
)
|
||||
|
||||
@param_values
|
||||
def test_escape_rfc_6868(text, expected):
|
||||
"""Check that we can escape the content with special characters."""
|
||||
escaped = rfc_6868_escape(text)
|
||||
assert escaped == expected, f"{escaped!r} == {expected!r}"
|
||||
assert rfc_6868_escape(rfc_6868_unescape(escaped)) == expected
|
||||
|
||||
|
||||
@param_values
|
||||
def test_escaping_parameters(text, expected):
|
||||
cal = Calendar()
|
||||
cal.add("X-Param", "asd")
|
||||
param : vText = cal["X-PARAM"]
|
||||
param.params["RFC6868"] = text
|
||||
ical = cal.to_ical().decode()
|
||||
print(ical)
|
||||
assert "X-PARAM;RFC6868=" + dquote(expected) in ical
|
||||
|
||||
|
||||
def test_encode_example_again(calendars):
|
||||
"""The example file should yield its content again."""
|
||||
cal : Calendar = calendars.rfc_6868
|
||||
again = Calendar.from_ical(cal.to_ical())
|
||||
assert cal == again
|
||||
assert cal.to_ical() == again.to_ical()
|
||||
Ładowanie…
Reference in New Issue