kopia lustrzana https://github.com/collective/icalendar
Merge branch 'master' into doctest
commit
4d899a275b
|
@ -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
|
||||||
|
|
27
CHANGES.rst
27
CHANGES.rst
|
@ -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)
|
||||||
------------------
|
------------------
|
||||||
|
|
|
@ -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::
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
__version__ = '5.0.0'
|
__version__ = '5.0.1'
|
||||||
|
|
||||||
from icalendar.cal import (
|
from icalendar.cal import (
|
||||||
Calendar,
|
Calendar,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
BEGIN:VEVENT
|
||||||
|
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ORGANIZER;CN=Society\, 2014:that
|
||||||
|
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ORGANIZER;CN=Society\\ 2014:that
|
||||||
|
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ORGANIZER;CN=Society\; 2014:that
|
||||||
|
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ORGANIZER;CN=Society\: 2014:that
|
||||||
|
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ORGANIZER;CN=that\, that\; %th%%at%\ that\::это\, то\; that\ %th%%at%\:
|
||||||
|
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
||||||
|
BEGIN:VEVENT
|
||||||
|
ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org
|
||||||
|
END:VEVENT
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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))
|
|
||||||
|
|
Ładowanie…
Reference in New Issue