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. --> <!-- A clear and concise description of what you expected to happen. -->
## Environment ## Environment

Wyświetl plik

@ -1,12 +1,16 @@
Changelog Changelog
========= =========
5.0.1 (unreleased) 5.0.2 (unreleased)
------------------ ------------------
Minor changes: 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: Breaking changes:
@ -18,7 +22,24 @@ New features:
Bug fixes: 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) 5.0.0 (2022-10-17)
------------------ ------------------

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -90,6 +90,7 @@ DSTDIFF = DSTOFFSET - STDOFFSET
class FixedOffset(tzinfo): class FixedOffset(tzinfo):
"""Fixed offset in minutes east from UTC. """Fixed offset in minutes east from UTC.
""" """
def __init__(self, offset, name): def __init__(self, offset, name):
self.__offset = timedelta(minutes=offset) self.__offset = timedelta(minutes=offset)
self.__name = name self.__name = name
@ -107,17 +108,12 @@ class FixedOffset(tzinfo):
class LocalTimezone(tzinfo): class LocalTimezone(tzinfo):
"""Timezone of the machine where the code is running. """Timezone of the machine where the code is running.
""" """
def utcoffset(self, dt): def utcoffset(self, dt):
if self._isdst(dt): return DSTOFFSET if self._isdst(dt) else STDOFFSET
return DSTOFFSET
else:
return STDOFFSET
def dst(self, dt): def dst(self, dt):
if self._isdst(dt): return DSTDIFF if self._isdst(dt) else ZERO
return DSTDIFF
else:
return ZERO
def tzname(self, dt): def tzname(self, dt):
return _time.tzname[self._isdst(dt)] return _time.tzname[self._isdst(dt)]
@ -140,7 +136,7 @@ class vBinary:
self.params = Parameters(encoding='BASE64', value="BINARY") self.params = Parameters(encoding='BASE64', value="BINARY")
def __repr__(self): def __repr__(self):
return "vBinary('%s')" % self.to_ical() return f"vBinary('{self.to_ical()}')"
def to_ical(self): def to_ical(self):
return binascii.b2a_base64(self.obj.encode('utf-8'))[:-1] return binascii.b2a_base64(self.obj.encode('utf-8'))[:-1]
@ -164,16 +160,14 @@ class vBoolean(int):
return self return self
def to_ical(self): def to_ical(self):
if self: return b'TRUE' if self else b'FALSE'
return b'TRUE'
return b'FALSE'
@classmethod @classmethod
def from_ical(cls, ical): def from_ical(cls, ical):
try: try:
return cls.BOOL_MAP[ical] return cls.BOOL_MAP[ical]
except Exception: except Exception:
raise ValueError("Expected 'TRUE' or 'FALSE'. Got %s" % ical) raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}")
class vCalAddress(str): class vCalAddress(str):
@ -186,7 +180,7 @@ class vCalAddress(str):
return self return self
def __repr__(self): def __repr__(self):
return "vCalAddress('%s')" % self.to_ical() return f"vCalAddress('{self.to_ical()}')"
def to_ical(self): def to_ical(self):
return self.encode(DEFAULT_ENCODING) return self.encode(DEFAULT_ENCODING)
@ -212,7 +206,7 @@ class vFloat(float):
try: try:
return cls(ical) return cls(ical)
except Exception: except Exception:
raise ValueError('Expected float value, got: %s' % ical) raise ValueError(f'Expected float value, got: {ical}')
class vInt(int): class vInt(int):
@ -231,12 +225,13 @@ class vInt(int):
try: try:
return cls(ical) return cls(ical)
except Exception: except Exception:
raise ValueError('Expected int, got: %s' % ical) raise ValueError(f'Expected int, got: {ical}')
class vDDDLists: class vDDDLists:
"""A list of vDDDTypes values. """A list of vDDDTypes values.
""" """
def __init__(self, dt_list): def __init__(self, dt_list):
if not hasattr(dt_list, '__iter__'): if not hasattr(dt_list, '__iter__'):
dt_list = [dt_list] dt_list = [dt_list]
@ -265,6 +260,7 @@ class vDDDLists:
out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone)) out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone))
return out return out
class vCategory: class vCategory:
def __init__(self, c_list): def __init__(self, c_list):
@ -287,6 +283,7 @@ class vDDDTypes:
cannot be confused, and often values can be of either types. cannot be confused, and often values can be of either types.
So this is practical. So this is practical.
""" """
def __init__(self, dt): def __init__(self, dt):
if not isinstance(dt, (datetime, date, timedelta, time, tuple)): if not isinstance(dt, (datetime, date, timedelta, time, tuple)):
raise ValueError('You must use datetime, date, timedelta, ' raise ValueError('You must use datetime, date, timedelta, '
@ -346,13 +343,14 @@ class vDDDTypes:
return vTime.from_ical(ical) return vTime.from_ical(ical)
else: else:
raise ValueError( raise ValueError(
"Expected datetime, date, or time, got: '%s'" % ical f"Expected datetime, date, or time, got: '{ical}'"
) )
class vDate: class vDate:
"""Render and generates iCalendar date format. """Render and generates iCalendar date format.
""" """
def __init__(self, dt): def __init__(self, dt):
if not isinstance(dt, date): if not isinstance(dt, date):
raise ValueError('Value MUST be a date instance') raise ValueError('Value MUST be a date instance')
@ -360,7 +358,7 @@ class vDate:
self.params = Parameters({'value': 'DATE'}) self.params = Parameters({'value': 'DATE'})
def to_ical(self): 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') return s.encode('utf-8')
@staticmethod @staticmethod
@ -373,7 +371,7 @@ class vDate:
) )
return date(*timetuple) return date(*timetuple)
except Exception: except Exception:
raise ValueError('Wrong date format %s' % ical) raise ValueError(f'Wrong date format {ical}')
class vDatetime: class vDatetime:
@ -387,6 +385,7 @@ class vDatetime:
created. Be aware that there are certain limitations with timezone naive created. Be aware that there are certain limitations with timezone naive
DATE-TIME components in the icalendar standard. DATE-TIME components in the icalendar standard.
""" """
def __init__(self, dt): def __init__(self, dt):
self.dt = dt self.dt = dt
self.params = Parameters() self.params = Parameters()
@ -395,14 +394,7 @@ class vDatetime:
dt = self.dt dt = self.dt
tzid = tzid_from_dt(dt) tzid = tzid_from_dt(dt)
s = "%04d%02d%02dT%02d%02d%02d" % ( s = f"{dt.year:04}{dt.month:02}{dt.day:02}T{dt.hour:02}{dt.minute:02}{dt.second:02}"
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second
)
if tzid == 'UTC': if tzid == 'UTC':
s += "Z" s += "Z"
elif tzid: elif tzid:
@ -414,10 +406,11 @@ class vDatetime:
tzinfo = None tzinfo = None
if timezone: if timezone:
try: try:
tzinfo = pytz.timezone(timezone) tzinfo = pytz.timezone(timezone.strip('/'))
except pytz.UnknownTimeZoneError: except pytz.UnknownTimeZoneError:
if timezone in WINDOWS_TO_OLSON: if timezone in WINDOWS_TO_OLSON:
tzinfo = pytz.timezone(WINDOWS_TO_OLSON.get(timezone)) tzinfo = pytz.timezone(
WINDOWS_TO_OLSON.get(timezone.strip('/')))
else: else:
tzinfo = _timezone_cache.get(timezone, None) tzinfo = _timezone_cache.get(timezone, None)
@ -439,7 +432,7 @@ class vDatetime:
else: else:
raise ValueError(ical) raise ValueError(ical)
except Exception: except Exception:
raise ValueError('Wrong datetime format: %s' % ical) raise ValueError(f'Wrong datetime format: {ical}')
class vDuration: class vDuration:
@ -466,18 +459,18 @@ class vDuration:
minutes = td.seconds % 3600 // 60 minutes = td.seconds % 3600 // 60
seconds = td.seconds % 60 seconds = td.seconds % 60
if hours: if hours:
timepart += "%dH" % hours timepart += f"{hours}H"
if minutes or (hours and seconds): if minutes or (hours and seconds):
timepart += "%dM" % minutes timepart += f"{minutes}M"
if seconds: if seconds:
timepart += "%dS" % seconds timepart += f"{seconds}S"
if td.days == 0 and timepart: if td.days == 0 and timepart:
return (str(sign).encode('utf-8') + b'P' + return (str(sign).encode('utf-8') + b'P'
str(timepart).encode('utf-8')) + str(timepart).encode('utf-8'))
else: else:
return (str(sign).encode('utf-8') + b'P' + return (str(sign).encode('utf-8') + b'P'
str(abs(td.days)).encode('utf-8') + + str(abs(td.days)).encode('utf-8')
b'D' + str(timepart).encode('utf-8')) + b'D' + str(timepart).encode('utf-8'))
@staticmethod @staticmethod
def from_ical(ical): def from_ical(ical):
@ -495,19 +488,20 @@ class vDuration:
value = -value value = -value
return value return value
except Exception: except Exception:
raise ValueError('Invalid iCalendar duration: %s' % ical) raise ValueError(f'Invalid iCalendar duration: {ical}')
class vPeriod: class vPeriod:
"""A precise period of time. """A precise period of time.
""" """
def __init__(self, per): def __init__(self, per):
start, end_or_duration = per start, end_or_duration = per
if not (isinstance(start, datetime) or isinstance(start, date)): if not (isinstance(start, datetime) or isinstance(start, date)):
raise ValueError('Start value MUST be a datetime or date instance') raise ValueError('Start value MUST be a datetime or date instance')
if not (isinstance(end_or_duration, datetime) or if not (isinstance(end_or_duration, datetime)
isinstance(end_or_duration, date) or or isinstance(end_or_duration, date)
isinstance(end_or_duration, timedelta)): or isinstance(end_or_duration, timedelta)):
raise ValueError('end_or_duration MUST be a datetime, ' raise ValueError('end_or_duration MUST be a datetime, '
'date or timedelta instance') 'date or timedelta instance')
by_duration = 0 by_duration = 0
@ -535,7 +529,8 @@ class vPeriod:
def __cmp__(self, other): def __cmp__(self, other):
if not isinstance(other, vPeriod): 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)) return cmp((self.start, self.end), (other.start, other.end))
def overlaps(self, other): def overlaps(self, other):
@ -547,10 +542,10 @@ class vPeriod:
def to_ical(self): def to_ical(self):
if self.by_duration: if self.by_duration:
return (vDatetime(self.start).to_ical() + b'/' + return (vDatetime(self.start).to_ical() + b'/'
vDuration(self.duration).to_ical()) + vDuration(self.duration).to_ical())
return (vDatetime(self.start).to_ical() + b'/' + return (vDatetime(self.start).to_ical() + b'/'
vDatetime(self.end).to_ical()) + vDatetime(self.end).to_ical())
@staticmethod @staticmethod
def from_ical(ical): def from_ical(ical):
@ -560,7 +555,7 @@ class vPeriod:
end_or_duration = vDDDTypes.from_ical(end_or_duration) end_or_duration = vDDDTypes.from_ical(end_or_duration)
return (start, end_or_duration) return (start, end_or_duration)
except Exception: except Exception:
raise ValueError('Expected period format, got: %s' % ical) raise ValueError(f'Expected period format, got: {ical}')
def __repr__(self): def __repr__(self):
if self.by_duration: if self.by_duration:
@ -582,13 +577,13 @@ class vWeekday(str):
self = super().__new__(cls, value) self = super().__new__(cls, value)
match = WEEKDAY_RULE.match(self) match = WEEKDAY_RULE.match(self)
if match is None: if match is None:
raise ValueError('Expected weekday abbrevation, got: %s' % self) raise ValueError(f'Expected weekday abbrevation, got: {self}')
match = match.groupdict() match = match.groupdict()
sign = match['signal'] sign = match['signal']
weekday = match['weekday'] weekday = match['weekday']
relative = match['relative'] relative = match['relative']
if weekday not in vWeekday.week_days or sign not in '+-': 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.relative = relative and int(relative) or None
self.params = Parameters() self.params = Parameters()
return self return self
@ -601,7 +596,7 @@ class vWeekday(str):
try: try:
return cls(ical.upper()) return cls(ical.upper())
except Exception: except Exception:
raise ValueError('Expected weekday abbrevation, got: %s' % ical) raise ValueError(f'Expected weekday abbrevation, got: {ical}')
class vFrequency(str): class vFrequency(str):
@ -622,7 +617,7 @@ class vFrequency(str):
value = to_unicode(value, encoding=encoding) value = to_unicode(value, encoding=encoding)
self = super().__new__(cls, value) self = super().__new__(cls, value)
if self not in vFrequency.frequencies: if self not in vFrequency.frequencies:
raise ValueError('Expected frequency, got: %s' % self) raise ValueError(f'Expected frequency, got: {self}')
self.params = Parameters() self.params = Parameters()
return self return self
@ -634,7 +629,7 @@ class vFrequency(str):
try: try:
return cls(ical.upper()) return cls(ical.upper())
except Exception: except Exception:
raise ValueError('Expected frequency, got: %s' % ical) raise ValueError(f'Expected frequency, got: {ical}')
class vRecur(CaselessDict): class vRecur(CaselessDict):
@ -708,7 +703,7 @@ class vRecur(CaselessDict):
recur[key] = cls.parse_type(key, vals) recur[key] = cls.parse_type(key, vals)
return dict(recur) return dict(recur)
except Exception: except Exception:
raise ValueError('Error in recurrence rule: %s' % ical) raise ValueError(f'Error in recurrence rule: {ical}')
class vText(str): class vText(str):
@ -723,7 +718,7 @@ class vText(str):
return self return self
def __repr__(self): def __repr__(self):
return "vText('%s')" % self.to_ical() return f"vText('{self.to_ical()}')"
def to_ical(self): def to_ical(self):
return escape_char(self).encode(self.encoding) return escape_char(self).encode(self.encoding)
@ -741,7 +736,7 @@ class vTime:
def __init__(self, *args): def __init__(self, *args):
if len(args) == 1: if len(args) == 1:
if not isinstance(args[0], (time, datetime)): 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] self.dt = args[0]
else: else:
self.dt = time(*args) self.dt = time(*args)
@ -757,7 +752,7 @@ class vTime:
timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6])) timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
return time(*timetuple) return time(*timetuple)
except Exception: except Exception:
raise ValueError('Expected time, got: %s' % ical) raise ValueError(f'Expected time, got: {ical}')
class vUri(str): class vUri(str):
@ -778,7 +773,7 @@ class vUri(str):
try: try:
return cls(ical) return cls(ical)
except Exception: except Exception:
raise ValueError('Expected , got: %s' % ical) raise ValueError(f'Expected , got: {ical}')
class vGeo: class vGeo:
@ -806,7 +801,7 @@ class vGeo:
latitude, longitude = ical.split(';') latitude, longitude = ical.split(';')
return (float(latitude), float(longitude)) return (float(latitude), float(longitude))
except Exception: except Exception:
raise ValueError("Expected 'float;float' , got: %s" % ical) raise ValueError(f"Expected 'float;float' , got: {ical}")
class vUTCOffset: class vUTCOffset:
@ -814,9 +809,9 @@ class vUTCOffset:
""" """
ignore_exceptions = False # if True, and we cannot parse this ignore_exceptions = False # if True, and we cannot parse this
# component, we will silently ignore # component, we will silently ignore
# it, rather than let the exception # it, rather than let the exception
# propagate upwards # propagate upwards
def __init__(self, td): def __init__(self, td):
if not isinstance(td, timedelta): if not isinstance(td, timedelta):
@ -840,9 +835,9 @@ class vUTCOffset:
minutes = abs((seconds % 3600) // 60) minutes = abs((seconds % 3600) // 60)
seconds = abs(seconds % 60) seconds = abs(seconds % 60)
if seconds: if seconds:
duration = '%02i%02i%02i' % (hours, minutes, seconds) duration = f'{hours:02}{minutes:02}{seconds:02}'
else: else:
duration = '%02i%02i' % (hours, minutes) duration = f'{hours:02}{minutes:02}'
return sign % duration return sign % duration
@classmethod @classmethod
@ -856,10 +851,10 @@ class vUTCOffset:
int(ical[5:7] or 0)) int(ical[5:7] or 0))
offset = timedelta(hours=hours, minutes=minutes, seconds=seconds) offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
except Exception: 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): if not cls.ignore_exceptions and offset >= timedelta(hours=24):
raise ValueError( raise ValueError(
'Offset must be less than 24 hours, was %s' % ical) f'Offset must be less than 24 hours, was {ical}')
if sign == '-': if sign == '-':
return -offset return -offset
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] event = events[test_input]
assert event[field].to_ical().decode('utf-8') == expected_value assert event[field].to_ical().decode('utf-8') == expected_value
def test_events_parameter_unicoded(events): @pytest.mark.parametrize('test_input, expected_output', [
'''chokes on umlauts in ORGANIZER # chokes on umlauts in ORGANIZER
https://github.com/collective/icalendar/issues/101 # https://github.com/collective/icalendar/issues/101
''' ('issue_101_icalendar_chokes_on_umlauts_in_organizer', 'acme, ädmin'),
assert events.issue_101_icalendar_chokes_on_umlauts_in_organizer['ORGANIZER'].params['CN'] == '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): def test_parses_event_with_non_ascii_tzid_issue_237(calendars, in_timezone):
"""Issue #237 - Fail to parse timezone with non-ascii TZID """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 # PERIOD should be put back into shape
'issue_156_RDATE_with_PERIOD', 'issue_156_RDATE_with_PERIOD',
'issue_156_RDATE_with_PERIOD_list', 'issue_156_RDATE_with_PERIOD_list',
'event_with_unicode_organizer',
]) ])
def test_event_to_ical_is_inverse_of_from_ical(events, event_name): 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.""" """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 = Event()
event.add('ATTACH', b) event.add('ATTACH', b)
assert event.to_ical() == events.issue_82_expected_output.raw_ics 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 import pytest
from icalendar import Event from icalendar import Calendar, Event, Parameters, vCalAddress
from icalendar import Parameters
from icalendar import vCalAddress
import unittest
import unittest
import icalendar import icalendar
import re 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): class TestPropertyParams(unittest.TestCase):
@ -30,146 +89,6 @@ class TestPropertyParams(unittest.TestCase):
ical2 = Calendar.from_ical(ical_str) ical2 = Calendar.from_ical(ical_str)
self.assertEqual(ical2.get('ORGANIZER').params.get('CN'), 'Doe, John') 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): def test_parse_and_access_property_params(self):
"""Parse an ics string and access some property parameters then. """Parse an ics string and access some property parameters then.
This is a follow-up of a question received per email. 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) host_name = to_unicode(host_name)
unique = unique or UIDGenerator.rnd_string() unique = unique or UIDGenerator.rnd_string()
today = to_unicode(vDatetime(datetime.today()).to_ical()) today = to_unicode(vDatetime(datetime.today()).to_ical())
return vText('{}-{}@{}'.format(today, return vText(f'{today}-{unique}@{host_name}')
unique,
host_name))