Merge branch 'master' into doctest

pull/445/head
Nicco Kunzmann 2022-11-01 23:40:19 +00:00
commit 4d899a275b
26 zmienionych plików z 377 dodań i 277 usunięć

Wyświetl plik

@ -24,7 +24,7 @@ Output:
```
## Expected behavior**
## Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
## Environment

Wyświetl plik

@ -1,12 +1,16 @@
Changelog
=========
5.0.1 (unreleased)
5.0.2 (unreleased)
------------------
Minor changes:
- fixed setuptools deprecation warnings [mgorny]
- Refactored cal.py, tools.py and completed remaining minimal refactoring in parser.py. Ref: #481 [pronoym99]
- Calendar.from_ical no longer throws long errors
Ref: #473
Fixes: #472
[jacadzaca]
Breaking changes:
@ -18,7 +22,24 @@ New features:
Bug fixes:
- ...
- broken properties are not added to the parent component
Ref: #471
Fixes: #464
[jacadzaca]
5.0.1 (2022-10-22)
------------------
Minor changes:
- fixed setuptools deprecation warnings [mgorny]
Bug fixes:
- a well-known timezone timezone prefixed with a `/` is treated as if the slash wasn't present
Ref: #467
Fixes: #466
[jacadzaca]
5.0.0 (2022-10-17)
------------------

Wyświetl plik

@ -64,6 +64,7 @@ icalendar contributors
- Mauro Amico <mauro.amico@gmail.com>
- Alexander Pitkin <peleccom@gmail.com>
- Michał Górny <mgorny@gentoo.org>
- Pronoy <lukex9442@gmail.com>
Find out who contributed::

Wyświetl plik

@ -7,10 +7,9 @@ ignore =
[zest.releaser]
python-file-with-version = src/icalendar/__init__.py
create-wheel = yes
[bdist_wheel]
universal = 1
universal = 0
[metadata]
license_files = LICENSE.rst

Wyświetl plik

@ -1,4 +1,4 @@
__version__ = '5.0.0'
__version__ = '5.0.1'
from icalendar.cal import (
Calendar,

Wyświetl plik

@ -370,8 +370,7 @@ class Component(CaselessDict):
factory = types_factory.for_property(name)
component = stack[-1] if stack else None
if not component:
raise ValueError('Property "{prop}" does not have '
'a parent component.'.format(prop=name))
raise ValueError(f'Property "{name}" does not have a parent component.')
datetime_names = ('DTSTART', 'DTEND', 'RECURRENCE-ID', 'DUE',
'FREEBUSY', 'RDATE', 'EXDATE')
try:
@ -383,7 +382,6 @@ class Component(CaselessDict):
if not component.ignore_exceptions:
raise
component.errors.append((uname, str(e)))
component.add(name, None, encode=0)
else:
vals.params = params
component.add(name, vals, encode=0)
@ -391,14 +389,22 @@ class Component(CaselessDict):
if multiple:
return comps
if len(comps) > 1:
raise ValueError(f'Found multiple components where '
f'only one is allowed: {st!r}')
raise ValueError(cls._format_error(
'Found multiple components where only one is allowed', st))
if len(comps) < 1:
raise ValueError(f'Found no components where '
f'exactly one is required: '
f'{st!r}')
raise ValueError(cls._format_error(
'Found no components where exactly one is required', st))
return comps[0]
def _format_error(error_description, bad_input, elipsis='[...]'):
# there's three character more in the error, ie. ' ' x2 and a ':'
max_error_length = 100 - 3
if len(error_description) + len(bad_input) + len(elipsis) > max_error_length:
truncate_to = max_error_length - len(error_description) - len(elipsis)
return f'{error_description}: {bad_input[:truncate_to]} {elipsis}'
else:
return f'{error_description}: {bad_input}'
def content_line(self, name, value, sorted=True):
"""Returns property as content line.
"""
@ -427,12 +433,8 @@ class Component(CaselessDict):
def __repr__(self):
"""String representation of class with all of it's subcomponents.
"""
subs = ', '.join([str(it) for it in self.subcomponents])
return '{}({}{})'.format(
self.name or type(self).__name__,
dict(self),
', %s' % subs if subs else ''
)
subs = ', '.join(str(it) for it in self.subcomponents)
return f"{self.name or type(self).__name__}({dict(self)}{', ' + subs if subs else ''})"
#######################################
@ -605,12 +607,10 @@ class Timezone(Component):
tzname = component['TZNAME'].encode('ascii', 'replace')
tzname = self._make_unique_tzname(tzname, tznames)
except KeyError:
tzname = '{}_{}_{}_{}'.format(
zone,
component['DTSTART'].to_ical().decode('utf-8'),
component['TZOFFSETFROM'].to_ical(), # for whatever reason this is str/unicode
component['TZOFFSETTO'].to_ical(), # for whatever reason this is str/unicode
)
# for whatever reason this is str/unicode
tzname = f"{zone}_{component['DTSTART'].to_ical().decode('utf-8')}_" + \
f"{component['TZOFFSETFROM'].to_ical()}_" + \
f"{component['TZOFFSETTO'].to_ical()}"
tzname = self._make_unique_tzname(tzname, tznames)
dst[tzname], component_transitions = self._extract_offsets(

Wyświetl plik

@ -51,13 +51,14 @@ def tzid_from_dt(dt):
if hasattr(dt.tzinfo, 'zone'):
tzid = dt.tzinfo.zone # pytz implementation
elif hasattr(dt.tzinfo, 'key'):
tzid = dt.tzinfo.key # ZoneInfo implementation
tzid = dt.tzinfo.key # ZoneInfo implementation
elif hasattr(dt.tzinfo, 'tzname'):
# dateutil implementation, but this is broken
# See https://github.com/collective/icalendar/issues/333 for details
tzid = dt.tzinfo.tzname(dt)
return tzid
def foldline(line, limit=75, fold_sep='\r\n '):
"""Make a string folded as defined in RFC5545
Lines of text SHOULD NOT be longer than 75 octets, excluding the line
@ -142,7 +143,7 @@ def dquote(val):
# so replace it with a single-quote character
val = val.replace('"', "'")
if QUOTABLE.search(val):
return '"%s"' % val
return f'"{val}"'
return val
@ -158,8 +159,7 @@ def q_split(st, sep=',', maxsplit=-1):
length = len(st)
inquote = 0
splits = 0
for i in range(length):
ch = st[i]
for i, ch in enumerate(st):
if ch == '"':
inquote = not inquote
if not inquote and ch == sep:
@ -255,13 +255,13 @@ class Parameters(CaselessDict):
else:
result[key] = vals
except ValueError as exc:
raise ValueError('%r is not a valid parameter string: %s'
% (param, exc))
raise ValueError(
f'{param!r} is not a valid parameter string: {exc}')
return result
def escape_string(val):
# '%{:02X}'.format(i)
# f'{i:02X}'
return val.replace(r'\,', '%2C').replace(r'\:', '%3A')\
.replace(r'\;', '%3B').replace(r'\\', '%5C')
@ -288,7 +288,7 @@ class Contentline(str):
def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
assert '\n' not in value, ('Content line can not contain unescaped '
'new line characters.')
'new line characters.')
self = super().__new__(cls, value)
self.strict = strict
return self
@ -346,9 +346,7 @@ class Contentline(str):
return (name, params, values)
except ValueError as exc:
raise ValueError(
"Content line could not be parsed into parts: '%s': %s"
% (self, exc)
)
f"Content line could not be parsed into parts: '{self}': {exc}")
@classmethod
def from_ical(cls, ical, strict=False):
@ -370,6 +368,7 @@ class Contentlines(list):
Then this should be efficient. for Huge files, an iterator should probably
be used instead.
"""
def to_ical(self):
"""Simply join self.
"""

Wyświetl plik

@ -90,6 +90,7 @@ DSTDIFF = DSTOFFSET - STDOFFSET
class FixedOffset(tzinfo):
"""Fixed offset in minutes east from UTC.
"""
def __init__(self, offset, name):
self.__offset = timedelta(minutes=offset)
self.__name = name
@ -107,17 +108,12 @@ class FixedOffset(tzinfo):
class LocalTimezone(tzinfo):
"""Timezone of the machine where the code is running.
"""
def utcoffset(self, dt):
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
return DSTOFFSET if self._isdst(dt) else STDOFFSET
def dst(self, dt):
if self._isdst(dt):
return DSTDIFF
else:
return ZERO
return DSTDIFF if self._isdst(dt) else ZERO
def tzname(self, dt):
return _time.tzname[self._isdst(dt)]
@ -140,7 +136,7 @@ class vBinary:
self.params = Parameters(encoding='BASE64', value="BINARY")
def __repr__(self):
return "vBinary('%s')" % self.to_ical()
return f"vBinary('{self.to_ical()}')"
def to_ical(self):
return binascii.b2a_base64(self.obj.encode('utf-8'))[:-1]
@ -164,16 +160,14 @@ class vBoolean(int):
return self
def to_ical(self):
if self:
return b'TRUE'
return b'FALSE'
return b'TRUE' if self else b'FALSE'
@classmethod
def from_ical(cls, ical):
try:
return cls.BOOL_MAP[ical]
except Exception:
raise ValueError("Expected 'TRUE' or 'FALSE'. Got %s" % ical)
raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}")
class vCalAddress(str):
@ -186,7 +180,7 @@ class vCalAddress(str):
return self
def __repr__(self):
return "vCalAddress('%s')" % self.to_ical()
return f"vCalAddress('{self.to_ical()}')"
def to_ical(self):
return self.encode(DEFAULT_ENCODING)
@ -212,7 +206,7 @@ class vFloat(float):
try:
return cls(ical)
except Exception:
raise ValueError('Expected float value, got: %s' % ical)
raise ValueError(f'Expected float value, got: {ical}')
class vInt(int):
@ -231,12 +225,13 @@ class vInt(int):
try:
return cls(ical)
except Exception:
raise ValueError('Expected int, got: %s' % ical)
raise ValueError(f'Expected int, got: {ical}')
class vDDDLists:
"""A list of vDDDTypes values.
"""
def __init__(self, dt_list):
if not hasattr(dt_list, '__iter__'):
dt_list = [dt_list]
@ -265,6 +260,7 @@ class vDDDLists:
out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone))
return out
class vCategory:
def __init__(self, c_list):
@ -287,6 +283,7 @@ class vDDDTypes:
cannot be confused, and often values can be of either types.
So this is practical.
"""
def __init__(self, dt):
if not isinstance(dt, (datetime, date, timedelta, time, tuple)):
raise ValueError('You must use datetime, date, timedelta, '
@ -346,13 +343,14 @@ class vDDDTypes:
return vTime.from_ical(ical)
else:
raise ValueError(
"Expected datetime, date, or time, got: '%s'" % ical
f"Expected datetime, date, or time, got: '{ical}'"
)
class vDate:
"""Render and generates iCalendar date format.
"""
def __init__(self, dt):
if not isinstance(dt, date):
raise ValueError('Value MUST be a date instance')
@ -360,7 +358,7 @@ class vDate:
self.params = Parameters({'value': 'DATE'})
def to_ical(self):
s = "%04d%02d%02d" % (self.dt.year, self.dt.month, self.dt.day)
s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}"
return s.encode('utf-8')
@staticmethod
@ -373,7 +371,7 @@ class vDate:
)
return date(*timetuple)
except Exception:
raise ValueError('Wrong date format %s' % ical)
raise ValueError(f'Wrong date format {ical}')
class vDatetime:
@ -387,6 +385,7 @@ class vDatetime:
created. Be aware that there are certain limitations with timezone naive
DATE-TIME components in the icalendar standard.
"""
def __init__(self, dt):
self.dt = dt
self.params = Parameters()
@ -395,14 +394,7 @@ class vDatetime:
dt = self.dt
tzid = tzid_from_dt(dt)
s = "%04d%02d%02dT%02d%02d%02d" % (
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second
)
s = f"{dt.year:04}{dt.month:02}{dt.day:02}T{dt.hour:02}{dt.minute:02}{dt.second:02}"
if tzid == 'UTC':
s += "Z"
elif tzid:
@ -414,10 +406,11 @@ class vDatetime:
tzinfo = None
if timezone:
try:
tzinfo = pytz.timezone(timezone)
tzinfo = pytz.timezone(timezone.strip('/'))
except pytz.UnknownTimeZoneError:
if timezone in WINDOWS_TO_OLSON:
tzinfo = pytz.timezone(WINDOWS_TO_OLSON.get(timezone))
tzinfo = pytz.timezone(
WINDOWS_TO_OLSON.get(timezone.strip('/')))
else:
tzinfo = _timezone_cache.get(timezone, None)
@ -439,7 +432,7 @@ class vDatetime:
else:
raise ValueError(ical)
except Exception:
raise ValueError('Wrong datetime format: %s' % ical)
raise ValueError(f'Wrong datetime format: {ical}')
class vDuration:
@ -466,18 +459,18 @@ class vDuration:
minutes = td.seconds % 3600 // 60
seconds = td.seconds % 60
if hours:
timepart += "%dH" % hours
timepart += f"{hours}H"
if minutes or (hours and seconds):
timepart += "%dM" % minutes
timepart += f"{minutes}M"
if seconds:
timepart += "%dS" % seconds
timepart += f"{seconds}S"
if td.days == 0 and timepart:
return (str(sign).encode('utf-8') + b'P' +
str(timepart).encode('utf-8'))
return (str(sign).encode('utf-8') + b'P'
+ str(timepart).encode('utf-8'))
else:
return (str(sign).encode('utf-8') + b'P' +
str(abs(td.days)).encode('utf-8') +
b'D' + str(timepart).encode('utf-8'))
return (str(sign).encode('utf-8') + b'P'
+ str(abs(td.days)).encode('utf-8')
+ b'D' + str(timepart).encode('utf-8'))
@staticmethod
def from_ical(ical):
@ -495,19 +488,20 @@ class vDuration:
value = -value
return value
except Exception:
raise ValueError('Invalid iCalendar duration: %s' % ical)
raise ValueError(f'Invalid iCalendar duration: {ical}')
class vPeriod:
"""A precise period of time.
"""
def __init__(self, per):
start, end_or_duration = per
if not (isinstance(start, datetime) or isinstance(start, date)):
raise ValueError('Start value MUST be a datetime or date instance')
if not (isinstance(end_or_duration, datetime) or
isinstance(end_or_duration, date) or
isinstance(end_or_duration, timedelta)):
if not (isinstance(end_or_duration, datetime)
or isinstance(end_or_duration, date)
or isinstance(end_or_duration, timedelta)):
raise ValueError('end_or_duration MUST be a datetime, '
'date or timedelta instance')
by_duration = 0
@ -535,7 +529,8 @@ class vPeriod:
def __cmp__(self, other):
if not isinstance(other, vPeriod):
raise NotImplementedError('Cannot compare vPeriod with %r' % other)
raise NotImplementedError(
f'Cannot compare vPeriod with {other!r}')
return cmp((self.start, self.end), (other.start, other.end))
def overlaps(self, other):
@ -547,10 +542,10 @@ class vPeriod:
def to_ical(self):
if self.by_duration:
return (vDatetime(self.start).to_ical() + b'/' +
vDuration(self.duration).to_ical())
return (vDatetime(self.start).to_ical() + b'/' +
vDatetime(self.end).to_ical())
return (vDatetime(self.start).to_ical() + b'/'
+ vDuration(self.duration).to_ical())
return (vDatetime(self.start).to_ical() + b'/'
+ vDatetime(self.end).to_ical())
@staticmethod
def from_ical(ical):
@ -560,7 +555,7 @@ class vPeriod:
end_or_duration = vDDDTypes.from_ical(end_or_duration)
return (start, end_or_duration)
except Exception:
raise ValueError('Expected period format, got: %s' % ical)
raise ValueError(f'Expected period format, got: {ical}')
def __repr__(self):
if self.by_duration:
@ -582,13 +577,13 @@ class vWeekday(str):
self = super().__new__(cls, value)
match = WEEKDAY_RULE.match(self)
if match is None:
raise ValueError('Expected weekday abbrevation, got: %s' % self)
raise ValueError(f'Expected weekday abbrevation, got: {self}')
match = match.groupdict()
sign = match['signal']
weekday = match['weekday']
relative = match['relative']
if weekday not in vWeekday.week_days or sign not in '+-':
raise ValueError('Expected weekday abbrevation, got: %s' % self)
raise ValueError(f'Expected weekday abbrevation, got: {self}')
self.relative = relative and int(relative) or None
self.params = Parameters()
return self
@ -601,7 +596,7 @@ class vWeekday(str):
try:
return cls(ical.upper())
except Exception:
raise ValueError('Expected weekday abbrevation, got: %s' % ical)
raise ValueError(f'Expected weekday abbrevation, got: {ical}')
class vFrequency(str):
@ -622,7 +617,7 @@ class vFrequency(str):
value = to_unicode(value, encoding=encoding)
self = super().__new__(cls, value)
if self not in vFrequency.frequencies:
raise ValueError('Expected frequency, got: %s' % self)
raise ValueError(f'Expected frequency, got: {self}')
self.params = Parameters()
return self
@ -634,7 +629,7 @@ class vFrequency(str):
try:
return cls(ical.upper())
except Exception:
raise ValueError('Expected frequency, got: %s' % ical)
raise ValueError(f'Expected frequency, got: {ical}')
class vRecur(CaselessDict):
@ -708,7 +703,7 @@ class vRecur(CaselessDict):
recur[key] = cls.parse_type(key, vals)
return dict(recur)
except Exception:
raise ValueError('Error in recurrence rule: %s' % ical)
raise ValueError(f'Error in recurrence rule: {ical}')
class vText(str):
@ -723,7 +718,7 @@ class vText(str):
return self
def __repr__(self):
return "vText('%s')" % self.to_ical()
return f"vText('{self.to_ical()}')"
def to_ical(self):
return escape_char(self).encode(self.encoding)
@ -741,7 +736,7 @@ class vTime:
def __init__(self, *args):
if len(args) == 1:
if not isinstance(args[0], (time, datetime)):
raise ValueError('Expected a datetime.time, got: %s' % args[0])
raise ValueError(f'Expected a datetime.time, got: {args[0]}')
self.dt = args[0]
else:
self.dt = time(*args)
@ -757,7 +752,7 @@ class vTime:
timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
return time(*timetuple)
except Exception:
raise ValueError('Expected time, got: %s' % ical)
raise ValueError(f'Expected time, got: {ical}')
class vUri(str):
@ -778,7 +773,7 @@ class vUri(str):
try:
return cls(ical)
except Exception:
raise ValueError('Expected , got: %s' % ical)
raise ValueError(f'Expected , got: {ical}')
class vGeo:
@ -806,7 +801,7 @@ class vGeo:
latitude, longitude = ical.split(';')
return (float(latitude), float(longitude))
except Exception:
raise ValueError("Expected 'float;float' , got: %s" % ical)
raise ValueError(f"Expected 'float;float' , got: {ical}")
class vUTCOffset:
@ -814,9 +809,9 @@ class vUTCOffset:
"""
ignore_exceptions = False # if True, and we cannot parse this
# component, we will silently ignore
# it, rather than let the exception
# propagate upwards
# component, we will silently ignore
# it, rather than let the exception
# propagate upwards
def __init__(self, td):
if not isinstance(td, timedelta):
@ -840,9 +835,9 @@ class vUTCOffset:
minutes = abs((seconds % 3600) // 60)
seconds = abs(seconds % 60)
if seconds:
duration = '%02i%02i%02i' % (hours, minutes, seconds)
duration = f'{hours:02}{minutes:02}{seconds:02}'
else:
duration = '%02i%02i' % (hours, minutes)
duration = f'{hours:02}{minutes:02}'
return sign % duration
@classmethod
@ -856,10 +851,10 @@ class vUTCOffset:
int(ical[5:7] or 0))
offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
except Exception:
raise ValueError('Expected utc offset, got: %s' % ical)
raise ValueError(f'Expected utc offset, got: {ical}')
if not cls.ignore_exceptions and offset >= timedelta(hours=24):
raise ValueError(
'Offset must be less than 24 hours, was %s' % ical)
f'Offset must be less than 24 hours, was {ical}')
if sign == '-':
return -offset
return offset

Wyświetl plik

@ -0,0 +1,41 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT

Wyświetl plik

@ -0,0 +1,12 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:0cab49a0-1167-40f0-bfed-ecb4d117047d
DTSTAMP:20221019T102950Z
DTSTART;TZID=/Europe/Stockholm:20221021T200000
DTEND;TZID=/Europe/Stockholm:20221021T210000
SUMMARY:Just chatting
DESCRIPTION:Just Chatting.
CATEGORIES:Just Chatting
RRULE:FREQ=WEEKLY;BYDAY=FR
END:VEVENT
END:VCALENDAR

Wyświetl plik

@ -0,0 +1,29 @@
BEGIN:VCALENDAR
BEGIN:VTIMEZONE
TZID:/Europe/CUSTOM
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
UID:0cab49a0-1167-40f0-bfed-ecb4d117047d
DTSTAMP:20221019T102950Z
DTSTART;TZID=/Europe/CUSTOM:20221021T200000
DTEND;TZID=/Europe/CUSTOM:20221021T210000
SUMMARY:Just chatting
DESCRIPTION:Just Chatting.
CATEGORIES:Just Chatting
RRULE:FREQ=WEEKLY;BYDAY=FR
END:VEVENT
END:VCALENDAR

Wyświetl plik

@ -0,0 +1,7 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
SUMMARY:Example calendar with a ': ' in the summary
END:VEVENT
BEGIN:VEVENT
SUMMARY:Another event with a ': ' in the summary
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=Society\, 2014:that
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=Society\\ 2014:that
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=Society\; 2014:that
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=Society\: 2014:that
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=that\, that\; %th%%at%\ that\::это\, то\; that\ %th%%at%\:
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org
END:VEVENT

Wyświetl plik

@ -0,0 +1,7 @@
BEGIN:VEVENT
SUMMARY:RDATE period
DTSTART:19961230T020000Z
DTEND:19961230T060000Z
UID:rdate_period
RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,199709T180000Z/PT5H30M
END:VEVENT

Wyświetl plik

@ -0,0 +1,41 @@
import pytest
from icalendar import Event, Calendar
def test_ignore_exceptions_on_broken_events_issue_104(events):
''' Issue #104 - line parsing error in a VEVENT
(which has ignore_exceptions). Should mark the event broken
but not raise an exception.
https://github.com/collective/icalendar/issues/104
'''
assert events.issue_104_mark_events_broken.is_broken # TODO: REMOVE FOR NEXT MAJOR RELEASE
assert events.issue_104_mark_events_broken.errors == [(None, "Content line could not be parsed into parts: 'X': Invalid content line")]
def test_dont_ignore_exceptions_on_broken_calendars_issue_104(calendars):
'''Issue #104 - line parsing error in a VCALENDAR
(which doesn't have ignore_exceptions). Should raise an exception.
'''
with pytest.raises(ValueError):
calendars.issue_104_broken_calendar
def test_rdate_dosent_become_none_on_invalid_input_issue_464(events):
'''Issue #464 - [BUG] RDATE can become None if value is invalid
https://github.com/collective/icalendar/issues/464
'''
assert events.issue_464_invalid_rdate.is_broken
assert ('RDATE', 'Expected period format, got: 199709T180000Z/PT5H30M') in events.issue_464_invalid_rdate.errors
assert not b'RDATE:None' in events.issue_464_invalid_rdate.to_ical()
@pytest.mark.parametrize('calendar_name', [
'big_bad_calendar',
'small_bad_calendar',
'multiple_calendar_components',
'pr_480_summary_with_colon',
])
def test_error_message_doesnt_get_too_big(calendars, calendar_name):
with pytest.raises(ValueError) as exception:
calendars[calendar_name]
# Ignore part before first : for the test.
assert len(str(exception).split(': ', 1)[1]) <= 100

Wyświetl plik

@ -23,11 +23,14 @@ def test_event_from_ical_respects_unicode(test_input, field, expected_value, eve
event = events[test_input]
assert event[field].to_ical().decode('utf-8') == expected_value
def test_events_parameter_unicoded(events):
'''chokes on umlauts in ORGANIZER
https://github.com/collective/icalendar/issues/101
'''
assert events.issue_101_icalendar_chokes_on_umlauts_in_organizer['ORGANIZER'].params['CN'] == 'acme, ädmin'
@pytest.mark.parametrize('test_input, expected_output', [
# chokes on umlauts in ORGANIZER
# https://github.com/collective/icalendar/issues/101
('issue_101_icalendar_chokes_on_umlauts_in_organizer', 'acme, ädmin'),
('event_with_unicode_organizer', 'Джон Доу'),
])
def test_events_parameter_unicoded(events, test_input, expected_output):
assert events[test_input]['ORGANIZER'].params['CN'] == expected_output
def test_parses_event_with_non_ascii_tzid_issue_237(calendars, in_timezone):
"""Issue #237 - Fail to parse timezone with non-ascii TZID

Wyświetl plik

@ -1,20 +0,0 @@
import pytest
from icalendar import Event, Calendar
def test_ignore_exceptions_on_broken_events_issue_104(events):
''' Issue #104 - line parsing error in a VEVENT
(which has ignore_exceptions). Should mark the event broken
but not raise an exception.
https://github.com/collective/icalendar/issues/104
'''
assert events.issue_104_mark_events_broken.is_broken # TODO: REMOVE FOR NEXT MAJOR RELEASE
assert events.issue_104_mark_events_broken.errors == [(None, "Content line could not be parsed into parts: 'X': Invalid content line")]
def test_dont_ignore_exceptions_on_broken_calendars_issue_104(calendars):
'''Issue #104 - line parsing error in a VCALENDAR
(which doesn't have ignore_exceptions). Should raise an exception.
'''
with pytest.raises(ValueError):
calendars.issue_104_broken_calendar

Wyświetl plik

@ -79,6 +79,7 @@ def test_issue_157_removes_trailing_semicolon(events):
# PERIOD should be put back into shape
'issue_156_RDATE_with_PERIOD',
'issue_156_RDATE_with_PERIOD_list',
'event_with_unicode_organizer',
])
def test_event_to_ical_is_inverse_of_from_ical(events, event_name):
"""Make sure that an event's ICS is equal to the ICS it was made from."""
@ -160,3 +161,29 @@ def test_creates_event_with_base64_encoded_attachment_issue_82(events):
event = Event()
event.add('ATTACH', b)
assert event.to_ical() == events.issue_82_expected_output.raw_ics
@pytest.mark.parametrize('calendar_name', [
# Issue #466 - [BUG] TZID timezone is ignored when forward-slash is used
# https://github.com/collective/icalendar/issues/466
'issue_466_respect_unique_timezone',
'issue_466_convert_tzid_with_slash'
])
def test_handles_unique_tzid(calendars, in_timezone, calendar_name):
calendar = calendars[calendar_name]
start_dt = calendar.walk('VEVENT')[0]['dtstart'].dt
end_dt = calendar.walk('VEVENT')[0]['dtend'].dt
assert start_dt == in_timezone(datetime(2022, 10, 21, 20, 0, 0), 'Europe/Stockholm')
assert end_dt == in_timezone(datetime(2022, 10, 21, 21, 0, 0), 'Europe/Stockholm')
@pytest.mark.parametrize('event_name, expected_cn, expected_ics', [
('event_with_escaped_characters', r'that, that; %th%%at%\ that:', 'это, то; that\\ %th%%at%:'),
('event_with_escaped_character1', r'Society, 2014', 'that'),
('event_with_escaped_character2', r'Society\ 2014', 'that'),
('event_with_escaped_character3', r'Society; 2014', 'that'),
('event_with_escaped_character4', r'Society: 2014', 'that'),
])
def test_escaped_characters_read(event_name, expected_cn, expected_ics, events):
event = events[event_name]
assert event['ORGANIZER'].params['CN'] == expected_cn
assert event['ORGANIZER'].to_ical() == expected_ics.encode('utf-8')

Wyświetl plik

@ -1,12 +1,71 @@
from icalendar import Calendar
from icalendar import Event
from icalendar import Parameters
from icalendar import vCalAddress
import unittest
import pytest
from icalendar import Calendar, Event, Parameters, vCalAddress
import unittest
import icalendar
import re
@pytest.mark.parametrize('parameter, expected', [
# Simple parameter:value pair
(Parameters(parameter1='Value1'), b'PARAMETER1=Value1'),
# Parameter with list of values must be separated by comma
(Parameters({'parameter1': ['Value1', 'Value2']}), b'PARAMETER1=Value1,Value2'),
# Multiple parameters must be separated by a semicolon
(Parameters({'RSVP': 'TRUE', 'ROLE': 'REQ-PARTICIPANT'}), b'ROLE=REQ-PARTICIPANT;RSVP=TRUE'),
# Parameter values containing ',;:' must be double quoted
(Parameters({'ALTREP': 'http://www.wiz.org'}), b'ALTREP="http://www.wiz.org"'),
# list items must be quoted separately
(Parameters({'MEMBER': ['MAILTO:projectA@host.com',
'MAILTO:projectB@host.com']}),
b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'),
(Parameters({'parameter1': 'Value1',
'parameter2': ['Value2', 'Value3'],
'ALTREP': ['http://www.wiz.org', 'value4']}),
b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'),
# Including empty strings
(Parameters({'PARAM': ''}), b'PARAM='),
# We can also parse parameter strings
(Parameters({'MEMBER': ['MAILTO:projectA@host.com',
'MAILTO:projectB@host.com']}),
b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'),
# We can also parse parameter strings
(Parameters({'PARAMETER1': 'Value1',
'ALTREP': ['http://www.wiz.org', 'value4'],
'PARAMETER2': ['Value2', 'Value3']}),
b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'),
])
def test_parameter_to_ical_is_inverse_of_from_ical(parameter, expected):
assert parameter.to_ical() == expected
assert Parameters.from_ical(expected.decode('utf-8')) == parameter
def test_parse_parameter_string_without_quotes():
assert Parameters.from_ical('PARAM1=Value 1;PARA2=Value 2') == Parameters({'PARAM1': 'Value 1', 'PARA2': 'Value 2'})
def test_parametr_is_case_insensitive():
parameter = Parameters(parameter1='Value1')
assert parameter['parameter1'] == parameter['PARAMETER1'] == parameter['PaRaMeTer1']
def test_parameter_keys_are_uppercase():
parameter = Parameters(parameter1='Value1')
assert list(parameter.keys()) == ['PARAMETER1']
@pytest.mark.parametrize('cn_param, cn_quoted', [
# not double-quoted
('Aramis', 'Aramis'),
# if a space is present - enclose in double quotes
('Aramis Alameda', '"Aramis Alameda"'),
# 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"'),
])
def test_quoting(cn_param, cn_quoted):
event = Event()
attendee = vCalAddress('test@example.com')
attendee.params['CN'] = cn_param
event.add('ATTENDEE', attendee)
assert f'ATTENDEE;CN={cn_quoted}:test@example.com' in event.to_ical().decode('utf-8')
class TestPropertyParams(unittest.TestCase):
@ -30,146 +89,6 @@ class TestPropertyParams(unittest.TestCase):
ical2 = Calendar.from_ical(ical_str)
self.assertEqual(ical2.get('ORGANIZER').params.get('CN'), 'Doe, John')
def test_unicode_param(self):
cal_address = vCalAddress('mailto:john.doe@example.org')
cal_address.params["CN"] = "Джон Доу"
vevent = Event()
vevent['ORGANIZER'] = cal_address
self.assertEqual(
vevent.to_ical().decode('utf-8'),
'BEGIN:VEVENT\r\n'
'ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org\r\n'
'END:VEVENT\r\n'
)
self.assertEqual(vevent['ORGANIZER'].params['CN'],
'Джон Доу')
def test_quoting(self):
# not double-quoted
self._test_quoting("Aramis", 'Aramis')
# if a space is present - enclose in double quotes
self._test_quoting("Aramis Alameda", '"Aramis Alameda"')
# a single quote in parameter value - double quote the value
self._test_quoting("Aramis d'Alameda", '"Aramis d\'Alameda"')
# double quote is replaced with single quote
self._test_quoting("Aramis d\"Alameda", '"Aramis d\'Alameda"')
self._test_quoting("Арамис д'Аламеда", '"Арамис д\'Аламеда"')
def _test_quoting(self, cn_param, cn_quoted):
"""
@param cn_param: CN parameter value to test for quoting
@param cn_quoted: expected quoted parameter in icalendar format
"""
vevent = Event()
attendee = vCalAddress('test@mail.com')
attendee.params['CN'] = cn_param
vevent.add('ATTENDEE', attendee)
self.assertEqual(
vevent.to_ical(),
b'BEGIN:VEVENT\r\nATTENDEE;CN=' + cn_quoted.encode('utf-8') +
b':test@mail.com\r\nEND:VEVENT\r\n'
)
def test_escaping(self):
# verify that escaped non safe chars are decoded correctly
NON_SAFE_CHARS = ',\\;:'
for char in NON_SAFE_CHARS:
cn_escaped = "Society\\%s 2014" % char
cn_decoded = "Society%s 2014" % char
vevent = Event.from_ical(
'BEGIN:VEVENT\r\n'
'ORGANIZER;CN=%s:that\r\n'
'END:VEVENT\r\n' % cn_escaped
)
self.assertEqual(vevent['ORGANIZER'].params['CN'], cn_decoded)
vevent = Event.from_ical(
'BEGIN:VEVENT\r\n'
'ORGANIZER;CN=that\\, that\\; %th%%at%\\\\ that\\:'
':это\\, то\\; that\\\\ %th%%at%\\:\r\n'
'END:VEVENT\r\n'
)
self.assertEqual(
vevent['ORGANIZER'].params['CN'],
r'that, that; %th%%at%\ that:'
)
self.assertEqual(
vevent['ORGANIZER'].to_ical().decode('utf-8'),
'это, то; that\\ %th%%at%:'
)
def test_parameters_class(self):
# Simple parameter:value pair
p = Parameters(parameter1='Value1')
self.assertEqual(p.to_ical(), b'PARAMETER1=Value1')
# keys are converted to upper
self.assertEqual(list(p.keys()), ['PARAMETER1'])
# Parameters are case insensitive
self.assertEqual(p['parameter1'], 'Value1')
self.assertEqual(p['PARAMETER1'], 'Value1')
# Parameter with list of values must be separated by comma
p = Parameters({'parameter1': ['Value1', 'Value2']})
self.assertEqual(p.to_ical(), b'PARAMETER1=Value1,Value2')
# Multiple parameters must be separated by a semicolon
p = Parameters({'RSVP': 'TRUE', 'ROLE': 'REQ-PARTICIPANT'})
self.assertEqual(p.to_ical(), b'ROLE=REQ-PARTICIPANT;RSVP=TRUE')
# Parameter values containing ',;:' must be double quoted
p = Parameters({'ALTREP': 'http://www.wiz.org'})
self.assertEqual(p.to_ical(), b'ALTREP="http://www.wiz.org"')
# list items must be quoted separately
p = Parameters({'MEMBER': ['MAILTO:projectA@host.com',
'MAILTO:projectB@host.com']})
self.assertEqual(
p.to_ical(),
b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'
)
# Now the whole sheebang
p = Parameters({'parameter1': 'Value1',
'parameter2': ['Value2', 'Value3'],
'ALTREP': ['http://www.wiz.org', 'value4']})
self.assertEqual(
p.to_ical(),
(b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;'
b'PARAMETER2=Value2,Value3')
)
# We can also parse parameter strings
self.assertEqual(
Parameters.from_ical('PARAMETER1=Value 1;param2=Value 2'),
Parameters({'PARAMETER1': 'Value 1', 'PARAM2': 'Value 2'})
)
# Including empty strings
self.assertEqual(Parameters.from_ical('param='),
Parameters({'PARAM': ''}))
# We can also parse parameter strings
self.assertEqual(
Parameters.from_ical(
'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'
),
Parameters({'MEMBER': ['MAILTO:projectA@host.com',
'MAILTO:projectB@host.com']})
)
# We can also parse parameter strings
self.assertEqual(
Parameters.from_ical('ALTREP="http://www.wiz.org",value4;'
'PARAMETER1=Value1;PARAMETER2=Value2,Value3'),
Parameters({'PARAMETER1': 'Value1',
'ALTREP': ['http://www.wiz.org', 'value4'],
'PARAMETER2': ['Value2', 'Value3']})
)
def test_parse_and_access_property_params(self):
"""Parse an ics string and access some property parameters then.
This is a follow-up of a question received per email.

Wyświetl plik

@ -30,6 +30,4 @@ class UIDGenerator:
host_name = to_unicode(host_name)
unique = unique or UIDGenerator.rnd_string()
today = to_unicode(vDatetime(datetime.today()).to_ical())
return vText('{}-{}@{}'.format(today,
unique,
host_name))
return vText(f'{today}-{unique}@{host_name}')