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. -->
|
||||
|
||||
## Environment
|
||||
|
|
27
CHANGES.rst
27
CHANGES.rst
|
@ -1,12 +1,16 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
5.0.1 (unreleased)
|
||||
5.0.2 (unreleased)
|
||||
------------------
|
||||
|
||||
Minor changes:
|
||||
|
||||
- fixed setuptools deprecation warnings [mgorny]
|
||||
- Refactored cal.py, tools.py and completed remaining minimal refactoring in parser.py. Ref: #481 [pronoym99]
|
||||
- Calendar.from_ical no longer throws long errors
|
||||
Ref: #473
|
||||
Fixes: #472
|
||||
[jacadzaca]
|
||||
|
||||
Breaking changes:
|
||||
|
||||
|
@ -18,7 +22,24 @@ New features:
|
|||
|
||||
Bug fixes:
|
||||
|
||||
- ...
|
||||
- broken properties are not added to the parent component
|
||||
Ref: #471
|
||||
Fixes: #464
|
||||
[jacadzaca]
|
||||
|
||||
5.0.1 (2022-10-22)
|
||||
------------------
|
||||
|
||||
Minor changes:
|
||||
|
||||
- fixed setuptools deprecation warnings [mgorny]
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- a well-known timezone timezone prefixed with a `/` is treated as if the slash wasn't present
|
||||
Ref: #467
|
||||
Fixes: #466
|
||||
[jacadzaca]
|
||||
|
||||
5.0.0 (2022-10-17)
|
||||
------------------
|
||||
|
|
|
@ -64,6 +64,7 @@ icalendar contributors
|
|||
- Mauro Amico <mauro.amico@gmail.com>
|
||||
- Alexander Pitkin <peleccom@gmail.com>
|
||||
- Michał Górny <mgorny@gentoo.org>
|
||||
- Pronoy <lukex9442@gmail.com>
|
||||
|
||||
Find out who contributed::
|
||||
|
||||
|
|
|
@ -7,10 +7,9 @@ ignore =
|
|||
|
||||
[zest.releaser]
|
||||
python-file-with-version = src/icalendar/__init__.py
|
||||
create-wheel = yes
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
universal = 0
|
||||
|
||||
[metadata]
|
||||
license_files = LICENSE.rst
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
__version__ = '5.0.0'
|
||||
__version__ = '5.0.1'
|
||||
|
||||
from icalendar.cal import (
|
||||
Calendar,
|
||||
|
|
|
@ -370,8 +370,7 @@ class Component(CaselessDict):
|
|||
factory = types_factory.for_property(name)
|
||||
component = stack[-1] if stack else None
|
||||
if not component:
|
||||
raise ValueError('Property "{prop}" does not have '
|
||||
'a parent component.'.format(prop=name))
|
||||
raise ValueError(f'Property "{name}" does not have a parent component.')
|
||||
datetime_names = ('DTSTART', 'DTEND', 'RECURRENCE-ID', 'DUE',
|
||||
'FREEBUSY', 'RDATE', 'EXDATE')
|
||||
try:
|
||||
|
@ -383,7 +382,6 @@ class Component(CaselessDict):
|
|||
if not component.ignore_exceptions:
|
||||
raise
|
||||
component.errors.append((uname, str(e)))
|
||||
component.add(name, None, encode=0)
|
||||
else:
|
||||
vals.params = params
|
||||
component.add(name, vals, encode=0)
|
||||
|
@ -391,14 +389,22 @@ class Component(CaselessDict):
|
|||
if multiple:
|
||||
return comps
|
||||
if len(comps) > 1:
|
||||
raise ValueError(f'Found multiple components where '
|
||||
f'only one is allowed: {st!r}')
|
||||
raise ValueError(cls._format_error(
|
||||
'Found multiple components where only one is allowed', st))
|
||||
if len(comps) < 1:
|
||||
raise ValueError(f'Found no components where '
|
||||
f'exactly one is required: '
|
||||
f'{st!r}')
|
||||
raise ValueError(cls._format_error(
|
||||
'Found no components where exactly one is required', st))
|
||||
return comps[0]
|
||||
|
||||
def _format_error(error_description, bad_input, elipsis='[...]'):
|
||||
# there's three character more in the error, ie. ' ' x2 and a ':'
|
||||
max_error_length = 100 - 3
|
||||
if len(error_description) + len(bad_input) + len(elipsis) > max_error_length:
|
||||
truncate_to = max_error_length - len(error_description) - len(elipsis)
|
||||
return f'{error_description}: {bad_input[:truncate_to]} {elipsis}'
|
||||
else:
|
||||
return f'{error_description}: {bad_input}'
|
||||
|
||||
def content_line(self, name, value, sorted=True):
|
||||
"""Returns property as content line.
|
||||
"""
|
||||
|
@ -427,12 +433,8 @@ class Component(CaselessDict):
|
|||
def __repr__(self):
|
||||
"""String representation of class with all of it's subcomponents.
|
||||
"""
|
||||
subs = ', '.join([str(it) for it in self.subcomponents])
|
||||
return '{}({}{})'.format(
|
||||
self.name or type(self).__name__,
|
||||
dict(self),
|
||||
', %s' % subs if subs else ''
|
||||
)
|
||||
subs = ', '.join(str(it) for it in self.subcomponents)
|
||||
return f"{self.name or type(self).__name__}({dict(self)}{', ' + subs if subs else ''})"
|
||||
|
||||
|
||||
#######################################
|
||||
|
@ -605,12 +607,10 @@ class Timezone(Component):
|
|||
tzname = component['TZNAME'].encode('ascii', 'replace')
|
||||
tzname = self._make_unique_tzname(tzname, tznames)
|
||||
except KeyError:
|
||||
tzname = '{}_{}_{}_{}'.format(
|
||||
zone,
|
||||
component['DTSTART'].to_ical().decode('utf-8'),
|
||||
component['TZOFFSETFROM'].to_ical(), # for whatever reason this is str/unicode
|
||||
component['TZOFFSETTO'].to_ical(), # for whatever reason this is str/unicode
|
||||
)
|
||||
# for whatever reason this is str/unicode
|
||||
tzname = f"{zone}_{component['DTSTART'].to_ical().decode('utf-8')}_" + \
|
||||
f"{component['TZOFFSETFROM'].to_ical()}_" + \
|
||||
f"{component['TZOFFSETTO'].to_ical()}"
|
||||
tzname = self._make_unique_tzname(tzname, tznames)
|
||||
|
||||
dst[tzname], component_transitions = self._extract_offsets(
|
||||
|
|
|
@ -51,13 +51,14 @@ def tzid_from_dt(dt):
|
|||
if hasattr(dt.tzinfo, 'zone'):
|
||||
tzid = dt.tzinfo.zone # pytz implementation
|
||||
elif hasattr(dt.tzinfo, 'key'):
|
||||
tzid = dt.tzinfo.key # ZoneInfo implementation
|
||||
tzid = dt.tzinfo.key # ZoneInfo implementation
|
||||
elif hasattr(dt.tzinfo, 'tzname'):
|
||||
# dateutil implementation, but this is broken
|
||||
# See https://github.com/collective/icalendar/issues/333 for details
|
||||
tzid = dt.tzinfo.tzname(dt)
|
||||
return tzid
|
||||
|
||||
|
||||
def foldline(line, limit=75, fold_sep='\r\n '):
|
||||
"""Make a string folded as defined in RFC5545
|
||||
Lines of text SHOULD NOT be longer than 75 octets, excluding the line
|
||||
|
@ -142,7 +143,7 @@ def dquote(val):
|
|||
# so replace it with a single-quote character
|
||||
val = val.replace('"', "'")
|
||||
if QUOTABLE.search(val):
|
||||
return '"%s"' % val
|
||||
return f'"{val}"'
|
||||
return val
|
||||
|
||||
|
||||
|
@ -158,8 +159,7 @@ def q_split(st, sep=',', maxsplit=-1):
|
|||
length = len(st)
|
||||
inquote = 0
|
||||
splits = 0
|
||||
for i in range(length):
|
||||
ch = st[i]
|
||||
for i, ch in enumerate(st):
|
||||
if ch == '"':
|
||||
inquote = not inquote
|
||||
if not inquote and ch == sep:
|
||||
|
@ -255,13 +255,13 @@ class Parameters(CaselessDict):
|
|||
else:
|
||||
result[key] = vals
|
||||
except ValueError as exc:
|
||||
raise ValueError('%r is not a valid parameter string: %s'
|
||||
% (param, exc))
|
||||
raise ValueError(
|
||||
f'{param!r} is not a valid parameter string: {exc}')
|
||||
return result
|
||||
|
||||
|
||||
def escape_string(val):
|
||||
# '%{:02X}'.format(i)
|
||||
# f'{i:02X}'
|
||||
return val.replace(r'\,', '%2C').replace(r'\:', '%3A')\
|
||||
.replace(r'\;', '%3B').replace(r'\\', '%5C')
|
||||
|
||||
|
@ -288,7 +288,7 @@ class Contentline(str):
|
|||
def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING):
|
||||
value = to_unicode(value, encoding=encoding)
|
||||
assert '\n' not in value, ('Content line can not contain unescaped '
|
||||
'new line characters.')
|
||||
'new line characters.')
|
||||
self = super().__new__(cls, value)
|
||||
self.strict = strict
|
||||
return self
|
||||
|
@ -346,9 +346,7 @@ class Contentline(str):
|
|||
return (name, params, values)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
"Content line could not be parsed into parts: '%s': %s"
|
||||
% (self, exc)
|
||||
)
|
||||
f"Content line could not be parsed into parts: '{self}': {exc}")
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical, strict=False):
|
||||
|
@ -370,6 +368,7 @@ class Contentlines(list):
|
|||
Then this should be efficient. for Huge files, an iterator should probably
|
||||
be used instead.
|
||||
"""
|
||||
|
||||
def to_ical(self):
|
||||
"""Simply join self.
|
||||
"""
|
||||
|
|
|
@ -90,6 +90,7 @@ DSTDIFF = DSTOFFSET - STDOFFSET
|
|||
class FixedOffset(tzinfo):
|
||||
"""Fixed offset in minutes east from UTC.
|
||||
"""
|
||||
|
||||
def __init__(self, offset, name):
|
||||
self.__offset = timedelta(minutes=offset)
|
||||
self.__name = name
|
||||
|
@ -107,17 +108,12 @@ class FixedOffset(tzinfo):
|
|||
class LocalTimezone(tzinfo):
|
||||
"""Timezone of the machine where the code is running.
|
||||
"""
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if self._isdst(dt):
|
||||
return DSTOFFSET
|
||||
else:
|
||||
return STDOFFSET
|
||||
return DSTOFFSET if self._isdst(dt) else STDOFFSET
|
||||
|
||||
def dst(self, dt):
|
||||
if self._isdst(dt):
|
||||
return DSTDIFF
|
||||
else:
|
||||
return ZERO
|
||||
return DSTDIFF if self._isdst(dt) else ZERO
|
||||
|
||||
def tzname(self, dt):
|
||||
return _time.tzname[self._isdst(dt)]
|
||||
|
@ -140,7 +136,7 @@ class vBinary:
|
|||
self.params = Parameters(encoding='BASE64', value="BINARY")
|
||||
|
||||
def __repr__(self):
|
||||
return "vBinary('%s')" % self.to_ical()
|
||||
return f"vBinary('{self.to_ical()}')"
|
||||
|
||||
def to_ical(self):
|
||||
return binascii.b2a_base64(self.obj.encode('utf-8'))[:-1]
|
||||
|
@ -164,16 +160,14 @@ class vBoolean(int):
|
|||
return self
|
||||
|
||||
def to_ical(self):
|
||||
if self:
|
||||
return b'TRUE'
|
||||
return b'FALSE'
|
||||
return b'TRUE' if self else b'FALSE'
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical):
|
||||
try:
|
||||
return cls.BOOL_MAP[ical]
|
||||
except Exception:
|
||||
raise ValueError("Expected 'TRUE' or 'FALSE'. Got %s" % ical)
|
||||
raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}")
|
||||
|
||||
|
||||
class vCalAddress(str):
|
||||
|
@ -186,7 +180,7 @@ class vCalAddress(str):
|
|||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return "vCalAddress('%s')" % self.to_ical()
|
||||
return f"vCalAddress('{self.to_ical()}')"
|
||||
|
||||
def to_ical(self):
|
||||
return self.encode(DEFAULT_ENCODING)
|
||||
|
@ -212,7 +206,7 @@ class vFloat(float):
|
|||
try:
|
||||
return cls(ical)
|
||||
except Exception:
|
||||
raise ValueError('Expected float value, got: %s' % ical)
|
||||
raise ValueError(f'Expected float value, got: {ical}')
|
||||
|
||||
|
||||
class vInt(int):
|
||||
|
@ -231,12 +225,13 @@ class vInt(int):
|
|||
try:
|
||||
return cls(ical)
|
||||
except Exception:
|
||||
raise ValueError('Expected int, got: %s' % ical)
|
||||
raise ValueError(f'Expected int, got: {ical}')
|
||||
|
||||
|
||||
class vDDDLists:
|
||||
"""A list of vDDDTypes values.
|
||||
"""
|
||||
|
||||
def __init__(self, dt_list):
|
||||
if not hasattr(dt_list, '__iter__'):
|
||||
dt_list = [dt_list]
|
||||
|
@ -265,6 +260,7 @@ class vDDDLists:
|
|||
out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone))
|
||||
return out
|
||||
|
||||
|
||||
class vCategory:
|
||||
|
||||
def __init__(self, c_list):
|
||||
|
@ -287,6 +283,7 @@ class vDDDTypes:
|
|||
cannot be confused, and often values can be of either types.
|
||||
So this is practical.
|
||||
"""
|
||||
|
||||
def __init__(self, dt):
|
||||
if not isinstance(dt, (datetime, date, timedelta, time, tuple)):
|
||||
raise ValueError('You must use datetime, date, timedelta, '
|
||||
|
@ -346,13 +343,14 @@ class vDDDTypes:
|
|||
return vTime.from_ical(ical)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Expected datetime, date, or time, got: '%s'" % ical
|
||||
f"Expected datetime, date, or time, got: '{ical}'"
|
||||
)
|
||||
|
||||
|
||||
class vDate:
|
||||
"""Render and generates iCalendar date format.
|
||||
"""
|
||||
|
||||
def __init__(self, dt):
|
||||
if not isinstance(dt, date):
|
||||
raise ValueError('Value MUST be a date instance')
|
||||
|
@ -360,7 +358,7 @@ class vDate:
|
|||
self.params = Parameters({'value': 'DATE'})
|
||||
|
||||
def to_ical(self):
|
||||
s = "%04d%02d%02d" % (self.dt.year, self.dt.month, self.dt.day)
|
||||
s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}"
|
||||
return s.encode('utf-8')
|
||||
|
||||
@staticmethod
|
||||
|
@ -373,7 +371,7 @@ class vDate:
|
|||
)
|
||||
return date(*timetuple)
|
||||
except Exception:
|
||||
raise ValueError('Wrong date format %s' % ical)
|
||||
raise ValueError(f'Wrong date format {ical}')
|
||||
|
||||
|
||||
class vDatetime:
|
||||
|
@ -387,6 +385,7 @@ class vDatetime:
|
|||
created. Be aware that there are certain limitations with timezone naive
|
||||
DATE-TIME components in the icalendar standard.
|
||||
"""
|
||||
|
||||
def __init__(self, dt):
|
||||
self.dt = dt
|
||||
self.params = Parameters()
|
||||
|
@ -395,14 +394,7 @@ class vDatetime:
|
|||
dt = self.dt
|
||||
tzid = tzid_from_dt(dt)
|
||||
|
||||
s = "%04d%02d%02dT%02d%02d%02d" % (
|
||||
dt.year,
|
||||
dt.month,
|
||||
dt.day,
|
||||
dt.hour,
|
||||
dt.minute,
|
||||
dt.second
|
||||
)
|
||||
s = f"{dt.year:04}{dt.month:02}{dt.day:02}T{dt.hour:02}{dt.minute:02}{dt.second:02}"
|
||||
if tzid == 'UTC':
|
||||
s += "Z"
|
||||
elif tzid:
|
||||
|
@ -414,10 +406,11 @@ class vDatetime:
|
|||
tzinfo = None
|
||||
if timezone:
|
||||
try:
|
||||
tzinfo = pytz.timezone(timezone)
|
||||
tzinfo = pytz.timezone(timezone.strip('/'))
|
||||
except pytz.UnknownTimeZoneError:
|
||||
if timezone in WINDOWS_TO_OLSON:
|
||||
tzinfo = pytz.timezone(WINDOWS_TO_OLSON.get(timezone))
|
||||
tzinfo = pytz.timezone(
|
||||
WINDOWS_TO_OLSON.get(timezone.strip('/')))
|
||||
else:
|
||||
tzinfo = _timezone_cache.get(timezone, None)
|
||||
|
||||
|
@ -439,7 +432,7 @@ class vDatetime:
|
|||
else:
|
||||
raise ValueError(ical)
|
||||
except Exception:
|
||||
raise ValueError('Wrong datetime format: %s' % ical)
|
||||
raise ValueError(f'Wrong datetime format: {ical}')
|
||||
|
||||
|
||||
class vDuration:
|
||||
|
@ -466,18 +459,18 @@ class vDuration:
|
|||
minutes = td.seconds % 3600 // 60
|
||||
seconds = td.seconds % 60
|
||||
if hours:
|
||||
timepart += "%dH" % hours
|
||||
timepart += f"{hours}H"
|
||||
if minutes or (hours and seconds):
|
||||
timepart += "%dM" % minutes
|
||||
timepart += f"{minutes}M"
|
||||
if seconds:
|
||||
timepart += "%dS" % seconds
|
||||
timepart += f"{seconds}S"
|
||||
if td.days == 0 and timepart:
|
||||
return (str(sign).encode('utf-8') + b'P' +
|
||||
str(timepart).encode('utf-8'))
|
||||
return (str(sign).encode('utf-8') + b'P'
|
||||
+ str(timepart).encode('utf-8'))
|
||||
else:
|
||||
return (str(sign).encode('utf-8') + b'P' +
|
||||
str(abs(td.days)).encode('utf-8') +
|
||||
b'D' + str(timepart).encode('utf-8'))
|
||||
return (str(sign).encode('utf-8') + b'P'
|
||||
+ str(abs(td.days)).encode('utf-8')
|
||||
+ b'D' + str(timepart).encode('utf-8'))
|
||||
|
||||
@staticmethod
|
||||
def from_ical(ical):
|
||||
|
@ -495,19 +488,20 @@ class vDuration:
|
|||
value = -value
|
||||
return value
|
||||
except Exception:
|
||||
raise ValueError('Invalid iCalendar duration: %s' % ical)
|
||||
raise ValueError(f'Invalid iCalendar duration: {ical}')
|
||||
|
||||
|
||||
class vPeriod:
|
||||
"""A precise period of time.
|
||||
"""
|
||||
|
||||
def __init__(self, per):
|
||||
start, end_or_duration = per
|
||||
if not (isinstance(start, datetime) or isinstance(start, date)):
|
||||
raise ValueError('Start value MUST be a datetime or date instance')
|
||||
if not (isinstance(end_or_duration, datetime) or
|
||||
isinstance(end_or_duration, date) or
|
||||
isinstance(end_or_duration, timedelta)):
|
||||
if not (isinstance(end_or_duration, datetime)
|
||||
or isinstance(end_or_duration, date)
|
||||
or isinstance(end_or_duration, timedelta)):
|
||||
raise ValueError('end_or_duration MUST be a datetime, '
|
||||
'date or timedelta instance')
|
||||
by_duration = 0
|
||||
|
@ -535,7 +529,8 @@ class vPeriod:
|
|||
|
||||
def __cmp__(self, other):
|
||||
if not isinstance(other, vPeriod):
|
||||
raise NotImplementedError('Cannot compare vPeriod with %r' % other)
|
||||
raise NotImplementedError(
|
||||
f'Cannot compare vPeriod with {other!r}')
|
||||
return cmp((self.start, self.end), (other.start, other.end))
|
||||
|
||||
def overlaps(self, other):
|
||||
|
@ -547,10 +542,10 @@ class vPeriod:
|
|||
|
||||
def to_ical(self):
|
||||
if self.by_duration:
|
||||
return (vDatetime(self.start).to_ical() + b'/' +
|
||||
vDuration(self.duration).to_ical())
|
||||
return (vDatetime(self.start).to_ical() + b'/' +
|
||||
vDatetime(self.end).to_ical())
|
||||
return (vDatetime(self.start).to_ical() + b'/'
|
||||
+ vDuration(self.duration).to_ical())
|
||||
return (vDatetime(self.start).to_ical() + b'/'
|
||||
+ vDatetime(self.end).to_ical())
|
||||
|
||||
@staticmethod
|
||||
def from_ical(ical):
|
||||
|
@ -560,7 +555,7 @@ class vPeriod:
|
|||
end_or_duration = vDDDTypes.from_ical(end_or_duration)
|
||||
return (start, end_or_duration)
|
||||
except Exception:
|
||||
raise ValueError('Expected period format, got: %s' % ical)
|
||||
raise ValueError(f'Expected period format, got: {ical}')
|
||||
|
||||
def __repr__(self):
|
||||
if self.by_duration:
|
||||
|
@ -582,13 +577,13 @@ class vWeekday(str):
|
|||
self = super().__new__(cls, value)
|
||||
match = WEEKDAY_RULE.match(self)
|
||||
if match is None:
|
||||
raise ValueError('Expected weekday abbrevation, got: %s' % self)
|
||||
raise ValueError(f'Expected weekday abbrevation, got: {self}')
|
||||
match = match.groupdict()
|
||||
sign = match['signal']
|
||||
weekday = match['weekday']
|
||||
relative = match['relative']
|
||||
if weekday not in vWeekday.week_days or sign not in '+-':
|
||||
raise ValueError('Expected weekday abbrevation, got: %s' % self)
|
||||
raise ValueError(f'Expected weekday abbrevation, got: {self}')
|
||||
self.relative = relative and int(relative) or None
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
@ -601,7 +596,7 @@ class vWeekday(str):
|
|||
try:
|
||||
return cls(ical.upper())
|
||||
except Exception:
|
||||
raise ValueError('Expected weekday abbrevation, got: %s' % ical)
|
||||
raise ValueError(f'Expected weekday abbrevation, got: {ical}')
|
||||
|
||||
|
||||
class vFrequency(str):
|
||||
|
@ -622,7 +617,7 @@ class vFrequency(str):
|
|||
value = to_unicode(value, encoding=encoding)
|
||||
self = super().__new__(cls, value)
|
||||
if self not in vFrequency.frequencies:
|
||||
raise ValueError('Expected frequency, got: %s' % self)
|
||||
raise ValueError(f'Expected frequency, got: {self}')
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
||||
|
@ -634,7 +629,7 @@ class vFrequency(str):
|
|||
try:
|
||||
return cls(ical.upper())
|
||||
except Exception:
|
||||
raise ValueError('Expected frequency, got: %s' % ical)
|
||||
raise ValueError(f'Expected frequency, got: {ical}')
|
||||
|
||||
|
||||
class vRecur(CaselessDict):
|
||||
|
@ -708,7 +703,7 @@ class vRecur(CaselessDict):
|
|||
recur[key] = cls.parse_type(key, vals)
|
||||
return dict(recur)
|
||||
except Exception:
|
||||
raise ValueError('Error in recurrence rule: %s' % ical)
|
||||
raise ValueError(f'Error in recurrence rule: {ical}')
|
||||
|
||||
|
||||
class vText(str):
|
||||
|
@ -723,7 +718,7 @@ class vText(str):
|
|||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return "vText('%s')" % self.to_ical()
|
||||
return f"vText('{self.to_ical()}')"
|
||||
|
||||
def to_ical(self):
|
||||
return escape_char(self).encode(self.encoding)
|
||||
|
@ -741,7 +736,7 @@ class vTime:
|
|||
def __init__(self, *args):
|
||||
if len(args) == 1:
|
||||
if not isinstance(args[0], (time, datetime)):
|
||||
raise ValueError('Expected a datetime.time, got: %s' % args[0])
|
||||
raise ValueError(f'Expected a datetime.time, got: {args[0]}')
|
||||
self.dt = args[0]
|
||||
else:
|
||||
self.dt = time(*args)
|
||||
|
@ -757,7 +752,7 @@ class vTime:
|
|||
timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
|
||||
return time(*timetuple)
|
||||
except Exception:
|
||||
raise ValueError('Expected time, got: %s' % ical)
|
||||
raise ValueError(f'Expected time, got: {ical}')
|
||||
|
||||
|
||||
class vUri(str):
|
||||
|
@ -778,7 +773,7 @@ class vUri(str):
|
|||
try:
|
||||
return cls(ical)
|
||||
except Exception:
|
||||
raise ValueError('Expected , got: %s' % ical)
|
||||
raise ValueError(f'Expected , got: {ical}')
|
||||
|
||||
|
||||
class vGeo:
|
||||
|
@ -806,7 +801,7 @@ class vGeo:
|
|||
latitude, longitude = ical.split(';')
|
||||
return (float(latitude), float(longitude))
|
||||
except Exception:
|
||||
raise ValueError("Expected 'float;float' , got: %s" % ical)
|
||||
raise ValueError(f"Expected 'float;float' , got: {ical}")
|
||||
|
||||
|
||||
class vUTCOffset:
|
||||
|
@ -814,9 +809,9 @@ class vUTCOffset:
|
|||
"""
|
||||
|
||||
ignore_exceptions = False # if True, and we cannot parse this
|
||||
# component, we will silently ignore
|
||||
# it, rather than let the exception
|
||||
# propagate upwards
|
||||
# component, we will silently ignore
|
||||
# it, rather than let the exception
|
||||
# propagate upwards
|
||||
|
||||
def __init__(self, td):
|
||||
if not isinstance(td, timedelta):
|
||||
|
@ -840,9 +835,9 @@ class vUTCOffset:
|
|||
minutes = abs((seconds % 3600) // 60)
|
||||
seconds = abs(seconds % 60)
|
||||
if seconds:
|
||||
duration = '%02i%02i%02i' % (hours, minutes, seconds)
|
||||
duration = f'{hours:02}{minutes:02}{seconds:02}'
|
||||
else:
|
||||
duration = '%02i%02i' % (hours, minutes)
|
||||
duration = f'{hours:02}{minutes:02}'
|
||||
return sign % duration
|
||||
|
||||
@classmethod
|
||||
|
@ -856,10 +851,10 @@ class vUTCOffset:
|
|||
int(ical[5:7] or 0))
|
||||
offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
||||
except Exception:
|
||||
raise ValueError('Expected utc offset, got: %s' % ical)
|
||||
raise ValueError(f'Expected utc offset, got: {ical}')
|
||||
if not cls.ignore_exceptions and offset >= timedelta(hours=24):
|
||||
raise ValueError(
|
||||
'Offset must be less than 24 hours, was %s' % ical)
|
||||
f'Offset must be less than 24 hours, was {ical}')
|
||||
if sign == '-':
|
||||
return -offset
|
||||
return offset
|
||||
|
|
|
@ -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]
|
||||
assert event[field].to_ical().decode('utf-8') == expected_value
|
||||
|
||||
def test_events_parameter_unicoded(events):
|
||||
'''chokes on umlauts in ORGANIZER
|
||||
https://github.com/collective/icalendar/issues/101
|
||||
'''
|
||||
assert events.issue_101_icalendar_chokes_on_umlauts_in_organizer['ORGANIZER'].params['CN'] == 'acme, ädmin'
|
||||
@pytest.mark.parametrize('test_input, expected_output', [
|
||||
# chokes on umlauts in ORGANIZER
|
||||
# https://github.com/collective/icalendar/issues/101
|
||||
('issue_101_icalendar_chokes_on_umlauts_in_organizer', 'acme, ädmin'),
|
||||
('event_with_unicode_organizer', 'Джон Доу'),
|
||||
])
|
||||
def test_events_parameter_unicoded(events, test_input, expected_output):
|
||||
assert events[test_input]['ORGANIZER'].params['CN'] == expected_output
|
||||
|
||||
def test_parses_event_with_non_ascii_tzid_issue_237(calendars, in_timezone):
|
||||
"""Issue #237 - Fail to parse timezone with non-ascii TZID
|
||||
|
|
|
@ -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
|
||||
'issue_156_RDATE_with_PERIOD',
|
||||
'issue_156_RDATE_with_PERIOD_list',
|
||||
'event_with_unicode_organizer',
|
||||
])
|
||||
def test_event_to_ical_is_inverse_of_from_ical(events, event_name):
|
||||
"""Make sure that an event's ICS is equal to the ICS it was made from."""
|
||||
|
@ -160,3 +161,29 @@ def test_creates_event_with_base64_encoded_attachment_issue_82(events):
|
|||
event = Event()
|
||||
event.add('ATTACH', b)
|
||||
assert event.to_ical() == events.issue_82_expected_output.raw_ics
|
||||
|
||||
@pytest.mark.parametrize('calendar_name', [
|
||||
# Issue #466 - [BUG] TZID timezone is ignored when forward-slash is used
|
||||
# https://github.com/collective/icalendar/issues/466
|
||||
'issue_466_respect_unique_timezone',
|
||||
'issue_466_convert_tzid_with_slash'
|
||||
])
|
||||
def test_handles_unique_tzid(calendars, in_timezone, calendar_name):
|
||||
calendar = calendars[calendar_name]
|
||||
start_dt = calendar.walk('VEVENT')[0]['dtstart'].dt
|
||||
end_dt = calendar.walk('VEVENT')[0]['dtend'].dt
|
||||
assert start_dt == in_timezone(datetime(2022, 10, 21, 20, 0, 0), 'Europe/Stockholm')
|
||||
assert end_dt == in_timezone(datetime(2022, 10, 21, 21, 0, 0), 'Europe/Stockholm')
|
||||
|
||||
@pytest.mark.parametrize('event_name, expected_cn, expected_ics', [
|
||||
('event_with_escaped_characters', r'that, that; %th%%at%\ that:', 'это, то; that\\ %th%%at%:'),
|
||||
('event_with_escaped_character1', r'Society, 2014', 'that'),
|
||||
('event_with_escaped_character2', r'Society\ 2014', 'that'),
|
||||
('event_with_escaped_character3', r'Society; 2014', 'that'),
|
||||
('event_with_escaped_character4', r'Society: 2014', 'that'),
|
||||
])
|
||||
def test_escaped_characters_read(event_name, expected_cn, expected_ics, events):
|
||||
event = events[event_name]
|
||||
assert event['ORGANIZER'].params['CN'] == expected_cn
|
||||
assert event['ORGANIZER'].to_ical() == expected_ics.encode('utf-8')
|
||||
|
||||
|
|
|
@ -1,12 +1,71 @@
|
|||
from icalendar import Calendar
|
||||
from icalendar import Event
|
||||
from icalendar import Parameters
|
||||
from icalendar import vCalAddress
|
||||
import unittest
|
||||
import pytest
|
||||
from icalendar import Calendar, Event, Parameters, vCalAddress
|
||||
|
||||
import unittest
|
||||
import icalendar
|
||||
import re
|
||||
|
||||
@pytest.mark.parametrize('parameter, expected', [
|
||||
# Simple parameter:value pair
|
||||
(Parameters(parameter1='Value1'), b'PARAMETER1=Value1'),
|
||||
# Parameter with list of values must be separated by comma
|
||||
(Parameters({'parameter1': ['Value1', 'Value2']}), b'PARAMETER1=Value1,Value2'),
|
||||
# Multiple parameters must be separated by a semicolon
|
||||
(Parameters({'RSVP': 'TRUE', 'ROLE': 'REQ-PARTICIPANT'}), b'ROLE=REQ-PARTICIPANT;RSVP=TRUE'),
|
||||
# Parameter values containing ',;:' must be double quoted
|
||||
(Parameters({'ALTREP': 'http://www.wiz.org'}), b'ALTREP="http://www.wiz.org"'),
|
||||
# list items must be quoted separately
|
||||
(Parameters({'MEMBER': ['MAILTO:projectA@host.com',
|
||||
'MAILTO:projectB@host.com']}),
|
||||
b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'),
|
||||
(Parameters({'parameter1': 'Value1',
|
||||
'parameter2': ['Value2', 'Value3'],
|
||||
'ALTREP': ['http://www.wiz.org', 'value4']}),
|
||||
b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'),
|
||||
# Including empty strings
|
||||
(Parameters({'PARAM': ''}), b'PARAM='),
|
||||
# We can also parse parameter strings
|
||||
(Parameters({'MEMBER': ['MAILTO:projectA@host.com',
|
||||
'MAILTO:projectB@host.com']}),
|
||||
b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'),
|
||||
# We can also parse parameter strings
|
||||
(Parameters({'PARAMETER1': 'Value1',
|
||||
'ALTREP': ['http://www.wiz.org', 'value4'],
|
||||
'PARAMETER2': ['Value2', 'Value3']}),
|
||||
b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'),
|
||||
])
|
||||
def test_parameter_to_ical_is_inverse_of_from_ical(parameter, expected):
|
||||
assert parameter.to_ical() == expected
|
||||
assert Parameters.from_ical(expected.decode('utf-8')) == parameter
|
||||
|
||||
def test_parse_parameter_string_without_quotes():
|
||||
assert Parameters.from_ical('PARAM1=Value 1;PARA2=Value 2') == Parameters({'PARAM1': 'Value 1', 'PARA2': 'Value 2'})
|
||||
|
||||
def test_parametr_is_case_insensitive():
|
||||
parameter = Parameters(parameter1='Value1')
|
||||
assert parameter['parameter1'] == parameter['PARAMETER1'] == parameter['PaRaMeTer1']
|
||||
|
||||
def test_parameter_keys_are_uppercase():
|
||||
parameter = Parameters(parameter1='Value1')
|
||||
assert list(parameter.keys()) == ['PARAMETER1']
|
||||
|
||||
@pytest.mark.parametrize('cn_param, cn_quoted', [
|
||||
# not double-quoted
|
||||
('Aramis', 'Aramis'),
|
||||
# if a space is present - enclose in double quotes
|
||||
('Aramis Alameda', '"Aramis Alameda"'),
|
||||
# a single quote in parameter value - double quote the value
|
||||
('Aramis d\'Alameda', '"Aramis d\'Alameda"'),
|
||||
('Арамис д\'Аламеда', '"Арамис д\'Аламеда"'),
|
||||
# double quote is replaced with single quote
|
||||
('Aramis d\"Alameda', '"Aramis d\'Alameda"'),
|
||||
])
|
||||
def test_quoting(cn_param, cn_quoted):
|
||||
event = Event()
|
||||
attendee = vCalAddress('test@example.com')
|
||||
attendee.params['CN'] = cn_param
|
||||
event.add('ATTENDEE', attendee)
|
||||
assert f'ATTENDEE;CN={cn_quoted}:test@example.com' in event.to_ical().decode('utf-8')
|
||||
|
||||
class TestPropertyParams(unittest.TestCase):
|
||||
|
||||
|
@ -30,146 +89,6 @@ class TestPropertyParams(unittest.TestCase):
|
|||
ical2 = Calendar.from_ical(ical_str)
|
||||
self.assertEqual(ical2.get('ORGANIZER').params.get('CN'), 'Doe, John')
|
||||
|
||||
def test_unicode_param(self):
|
||||
cal_address = vCalAddress('mailto:john.doe@example.org')
|
||||
cal_address.params["CN"] = "Джон Доу"
|
||||
vevent = Event()
|
||||
vevent['ORGANIZER'] = cal_address
|
||||
self.assertEqual(
|
||||
vevent.to_ical().decode('utf-8'),
|
||||
'BEGIN:VEVENT\r\n'
|
||||
'ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org\r\n'
|
||||
'END:VEVENT\r\n'
|
||||
)
|
||||
|
||||
self.assertEqual(vevent['ORGANIZER'].params['CN'],
|
||||
'Джон Доу')
|
||||
|
||||
def test_quoting(self):
|
||||
# not double-quoted
|
||||
self._test_quoting("Aramis", 'Aramis')
|
||||
# if a space is present - enclose in double quotes
|
||||
self._test_quoting("Aramis Alameda", '"Aramis Alameda"')
|
||||
# a single quote in parameter value - double quote the value
|
||||
self._test_quoting("Aramis d'Alameda", '"Aramis d\'Alameda"')
|
||||
# double quote is replaced with single quote
|
||||
self._test_quoting("Aramis d\"Alameda", '"Aramis d\'Alameda"')
|
||||
self._test_quoting("Арамис д'Аламеда", '"Арамис д\'Аламеда"')
|
||||
|
||||
def _test_quoting(self, cn_param, cn_quoted):
|
||||
"""
|
||||
@param cn_param: CN parameter value to test for quoting
|
||||
@param cn_quoted: expected quoted parameter in icalendar format
|
||||
"""
|
||||
vevent = Event()
|
||||
attendee = vCalAddress('test@mail.com')
|
||||
attendee.params['CN'] = cn_param
|
||||
vevent.add('ATTENDEE', attendee)
|
||||
self.assertEqual(
|
||||
vevent.to_ical(),
|
||||
b'BEGIN:VEVENT\r\nATTENDEE;CN=' + cn_quoted.encode('utf-8') +
|
||||
b':test@mail.com\r\nEND:VEVENT\r\n'
|
||||
)
|
||||
|
||||
def test_escaping(self):
|
||||
# verify that escaped non safe chars are decoded correctly
|
||||
NON_SAFE_CHARS = ',\\;:'
|
||||
for char in NON_SAFE_CHARS:
|
||||
cn_escaped = "Society\\%s 2014" % char
|
||||
cn_decoded = "Society%s 2014" % char
|
||||
vevent = Event.from_ical(
|
||||
'BEGIN:VEVENT\r\n'
|
||||
'ORGANIZER;CN=%s:that\r\n'
|
||||
'END:VEVENT\r\n' % cn_escaped
|
||||
)
|
||||
self.assertEqual(vevent['ORGANIZER'].params['CN'], cn_decoded)
|
||||
|
||||
vevent = Event.from_ical(
|
||||
'BEGIN:VEVENT\r\n'
|
||||
'ORGANIZER;CN=that\\, that\\; %th%%at%\\\\ that\\:'
|
||||
':это\\, то\\; that\\\\ %th%%at%\\:\r\n'
|
||||
'END:VEVENT\r\n'
|
||||
)
|
||||
self.assertEqual(
|
||||
vevent['ORGANIZER'].params['CN'],
|
||||
r'that, that; %th%%at%\ that:'
|
||||
)
|
||||
self.assertEqual(
|
||||
vevent['ORGANIZER'].to_ical().decode('utf-8'),
|
||||
'это, то; that\\ %th%%at%:'
|
||||
)
|
||||
|
||||
def test_parameters_class(self):
|
||||
|
||||
# Simple parameter:value pair
|
||||
p = Parameters(parameter1='Value1')
|
||||
self.assertEqual(p.to_ical(), b'PARAMETER1=Value1')
|
||||
|
||||
# keys are converted to upper
|
||||
self.assertEqual(list(p.keys()), ['PARAMETER1'])
|
||||
|
||||
# Parameters are case insensitive
|
||||
self.assertEqual(p['parameter1'], 'Value1')
|
||||
self.assertEqual(p['PARAMETER1'], 'Value1')
|
||||
|
||||
# Parameter with list of values must be separated by comma
|
||||
p = Parameters({'parameter1': ['Value1', 'Value2']})
|
||||
self.assertEqual(p.to_ical(), b'PARAMETER1=Value1,Value2')
|
||||
|
||||
# Multiple parameters must be separated by a semicolon
|
||||
p = Parameters({'RSVP': 'TRUE', 'ROLE': 'REQ-PARTICIPANT'})
|
||||
self.assertEqual(p.to_ical(), b'ROLE=REQ-PARTICIPANT;RSVP=TRUE')
|
||||
|
||||
# Parameter values containing ',;:' must be double quoted
|
||||
p = Parameters({'ALTREP': 'http://www.wiz.org'})
|
||||
self.assertEqual(p.to_ical(), b'ALTREP="http://www.wiz.org"')
|
||||
|
||||
# list items must be quoted separately
|
||||
p = Parameters({'MEMBER': ['MAILTO:projectA@host.com',
|
||||
'MAILTO:projectB@host.com']})
|
||||
self.assertEqual(
|
||||
p.to_ical(),
|
||||
b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'
|
||||
)
|
||||
|
||||
# Now the whole sheebang
|
||||
p = Parameters({'parameter1': 'Value1',
|
||||
'parameter2': ['Value2', 'Value3'],
|
||||
'ALTREP': ['http://www.wiz.org', 'value4']})
|
||||
self.assertEqual(
|
||||
p.to_ical(),
|
||||
(b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;'
|
||||
b'PARAMETER2=Value2,Value3')
|
||||
)
|
||||
|
||||
# We can also parse parameter strings
|
||||
self.assertEqual(
|
||||
Parameters.from_ical('PARAMETER1=Value 1;param2=Value 2'),
|
||||
Parameters({'PARAMETER1': 'Value 1', 'PARAM2': 'Value 2'})
|
||||
)
|
||||
|
||||
# Including empty strings
|
||||
self.assertEqual(Parameters.from_ical('param='),
|
||||
Parameters({'PARAM': ''}))
|
||||
|
||||
# We can also parse parameter strings
|
||||
self.assertEqual(
|
||||
Parameters.from_ical(
|
||||
'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'
|
||||
),
|
||||
Parameters({'MEMBER': ['MAILTO:projectA@host.com',
|
||||
'MAILTO:projectB@host.com']})
|
||||
)
|
||||
|
||||
# We can also parse parameter strings
|
||||
self.assertEqual(
|
||||
Parameters.from_ical('ALTREP="http://www.wiz.org",value4;'
|
||||
'PARAMETER1=Value1;PARAMETER2=Value2,Value3'),
|
||||
Parameters({'PARAMETER1': 'Value1',
|
||||
'ALTREP': ['http://www.wiz.org', 'value4'],
|
||||
'PARAMETER2': ['Value2', 'Value3']})
|
||||
)
|
||||
|
||||
def test_parse_and_access_property_params(self):
|
||||
"""Parse an ics string and access some property parameters then.
|
||||
This is a follow-up of a question received per email.
|
||||
|
|
|
@ -30,6 +30,4 @@ class UIDGenerator:
|
|||
host_name = to_unicode(host_name)
|
||||
unique = unique or UIDGenerator.rnd_string()
|
||||
today = to_unicode(vDatetime(datetime.today()).to_ical())
|
||||
return vText('{}-{}@{}'.format(today,
|
||||
unique,
|
||||
host_name))
|
||||
return vText(f'{today}-{unique}@{host_name}')
|
||||
|
|
Ładowanie…
Reference in New Issue