<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[15377] PyCalendar/branches/patch/src/pycalendar</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/15377">15377</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2015-12-10 14:01:17 -0800 (Thu, 10 Dec 2015)</dd>
</dl>
<h3>Log Message</h3>
<pre>Initial support for icalendar-patch.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#PyCalendarbranchespatchsrcpycalendarcomponentbasepy">PyCalendar/branches/patch/src/pycalendar/componentbase.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendardurationpy">PyCalendar/branches/patch/src/pycalendar/duration.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarfloatvaluepy">PyCalendar/branches/patch/src/pycalendar/floatvalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendargeovaluepy">PyCalendar/branches/patch/src/pycalendar/geovalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendaricalendarcalendarpy">PyCalendar/branches/patch/src/pycalendar/icalendar/calendar.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendaricalendarcomponentrecurpy">PyCalendar/branches/patch/src/pycalendar/icalendar/componentrecur.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendaricalendarrecurrencepy">PyCalendar/branches/patch/src/pycalendar/icalendar/recurrence.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendaricalendarrequeststatusvaluepy">PyCalendar/branches/patch/src/pycalendar/icalendar/requeststatusvalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendaricalendartestsrrule_examplesjson">PyCalendar/branches/patch/src/pycalendar/icalendar/tests/rrule_examples.json</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendaricalendarteststest_recurrencepy">PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_recurrence.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarintegervaluepy">PyCalendar/branches/patch/src/pycalendar/integervalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarmultivaluepy">PyCalendar/branches/patch/src/pycalendar/multivalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarparserpy">PyCalendar/branches/patch/src/pycalendar/parser.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarperiodpy">PyCalendar/branches/patch/src/pycalendar/period.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarplaintextvaluepy">PyCalendar/branches/patch/src/pycalendar/plaintextvalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendartextvaluepy">PyCalendar/branches/patch/src/pycalendar/textvalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarurivaluepy">PyCalendar/branches/patch/src/pycalendar/urivalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarutcoffsetvaluepy">PyCalendar/branches/patch/src/pycalendar/utcoffsetvalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarutilspy">PyCalendar/branches/patch/src/pycalendar/utils.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarvaluepy">PyCalendar/branches/patch/src/pycalendar/value.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarvalueutilspy">PyCalendar/branches/patch/src/pycalendar/valueutils.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarvcardadrpy">PyCalendar/branches/patch/src/pycalendar/vcard/adr.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarvcardnpy">PyCalendar/branches/patch/src/pycalendar/vcard/n.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarvcardorgvaluepy">PyCalendar/branches/patch/src/pycalendar/vcard/orgvalue.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarvcardteststest_adrpy">PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_adr.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendarvcardteststest_npy">PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_n.py</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#PyCalendarbranchespatchsrcpycalendaricalendarpatchpy">PyCalendar/branches/patch/src/pycalendar/icalendar/patch.py</a></li>
<li><a href="#PyCalendarbranchespatchsrcpycalendaricalendarteststest_patchpy">PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_patch.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="PyCalendarbranchespatchsrcpycalendarcomponentbasepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/componentbase.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/componentbase.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/componentbase.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -153,6 +153,14 @@
</span><span class="cx"> self.mComponents.remove(component)
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def removeFromParent(self):
+ """
+ Remove this L{ComponentBase} from its parent
+ """
+ if self.mParentComponent is not None:
+ self.mParentComponent.removeComponent(self)
+
+
</ins><span class="cx"> def removeAllComponent(self, compname=None):
</span><span class="cx"> if compname:
</span><span class="cx"> compname = compname.upper()
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendardurationpy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/duration.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/duration.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/duration.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -241,33 +241,40 @@
</span><span class="cx">
</span><span class="cx"> def generate(self, os):
</span><span class="cx"> try:
</span><del>- if not self.mForward and (self.mWeeks or self.mDays or self.mHours or self.mMinutes or self.mSeconds):
- os.write("-")
- os.write("P")
</del><ins>+ os.write(self.getText())
+ except:
+ pass
</ins><span class="cx">
</span><del>- if self.mWeeks != 0:
- os.write("%dW" % (self.mWeeks,))
- else:
- if self.mDays != 0:
- os.write("%dD" % (self.mDays,))
</del><span class="cx">
</span><del>- if (self.mHours != 0) or (self.mMinutes != 0) or (self.mSeconds != 0):
- os.write("T")
</del><ins>+ def getText(self):
+ result = []
+ if not self.mForward and (self.mWeeks or self.mDays or self.mHours or self.mMinutes or self.mSeconds):
+ result.append("-")
+ result.append("P")
</ins><span class="cx">
</span><del>- if self.mHours != 0:
- os.write("%dH" % (self.mHours,))
</del><ins>+ if self.mWeeks != 0:
+ result.append("%dW" % (self.mWeeks,))
+ else:
+ if self.mDays != 0:
+ result.append("%dD" % (self.mDays,))
</ins><span class="cx">
</span><del>- if (self.mMinutes != 0) or ((self.mHours != 0) and (self.mSeconds != 0)):
- os.write("%dM" % (self.mMinutes,))
</del><ins>+ if (self.mHours != 0) or (self.mMinutes != 0) or (self.mSeconds != 0):
+ result.append("T")
</ins><span class="cx">
</span><del>- if self.mSeconds != 0:
- os.write("%dS" % (self.mSeconds,))
- elif self.mDays == 0:
- os.write("T0S")
- except:
- pass
</del><ins>+ if self.mHours != 0:
+ result.append("%dH" % (self.mHours,))
</ins><span class="cx">
</span><ins>+ if (self.mMinutes != 0) or ((self.mHours != 0) and (self.mSeconds != 0)):
+ result.append("%dM" % (self.mMinutes,))
</ins><span class="cx">
</span><ins>+ if self.mSeconds != 0:
+ result.append("%dS" % (self.mSeconds,))
+ elif self.mDays == 0:
+ result.append("T0S")
+
+ return "".join(result)
+
+
</ins><span class="cx"> def writeXML(self, node, namespace):
</span><span class="cx"> node.text = self.getText()
</span><span class="cx">
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarfloatvaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/floatvalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/floatvalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/floatvalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -45,6 +45,10 @@
</span><span class="cx"> pass
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def getTextValue(self):
+ return str(self.mValue)
+
+
</ins><span class="cx"> def writeXML(self, node, namespace):
</span><span class="cx"> value = self.getXMLNode(node, namespace)
</span><span class="cx"> value.text = str(self.mValue)
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendargeovaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/geovalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/geovalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/geovalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -63,9 +63,13 @@
</span><span class="cx">
</span><span class="cx"> # os - StringIO object
</span><span class="cx"> def generate(self, os):
</span><del>- os.write("%s;%s" % (self.mValue[0], self.mValue[1],))
</del><ins>+ os.write(self.getTextValue())
</ins><span class="cx">
</span><span class="cx">
</span><ins>+ def getTextValue(self):
+ return "%s;%s" % (self.mValue[0], self.mValue[1],)
+
+
</ins><span class="cx"> def writeXML(self, node, namespace):
</span><span class="cx"> value = self.getXMLNode(node, namespace)
</span><span class="cx">
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendaricalendarcalendarpy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/calendar.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/icalendar/calendar.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/icalendar/calendar.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -365,49 +365,10 @@
</span><span class="cx"> master = self.masterComponent()
</span><span class="cx"> if master is None:
</span><span class="cx"> return None
</span><del>-
- # Create the derived instance
- newcomp = master.duplicate()
-
- # Strip out unwanted recurrence properties
- for propname in (
- definitions.cICalProperty_RRULE,
- definitions.cICalProperty_RDATE,
- definitions.cICalProperty_EXRULE,
- definitions.cICalProperty_EXDATE,
- definitions.cICalProperty_RECURRENCE_ID,
- ):
- newcomp.removeProperties(propname)
-
- # New DTSTART is the RECURRENCE-ID we are deriving but adjusted to the
- # original DTSTART's localtime
- dtstart = newcomp.getStart()
- dtend = newcomp.getEnd()
- oldduration = dtend - dtstart
-
- newdtstartValue = recurrenceID.duplicate()
- if not dtstart.isDateOnly():
- if dtstart.local():
- newdtstartValue.adjustTimezone(dtstart.getTimezone())
</del><span class="cx"> else:
</span><del>- newdtstartValue.setDateOnly(True)
</del><ins>+ return master.deriveComponent(recurrenceID)
</ins><span class="cx">
</span><del>- newcomp.removeProperties(definitions.cICalProperty_DTSTART)
- newcomp.removeProperties(definitions.cICalProperty_DTEND)
- prop = Property(definitions.cICalProperty_DTSTART, newdtstartValue)
- newcomp.addProperty(prop)
- if not newcomp.useDuration():
- prop = Property(definitions.cICalProperty_DTEND, newdtstartValue + oldduration)
- newcomp.addProperty(prop)
</del><span class="cx">
</span><del>- newcomp.addProperty(Property("RECURRENCE-ID", newdtstartValue))
-
- # After creating/changing a component we need to do this to keep PyCalendar happy
- newcomp.finalise()
-
- return newcomp
-
-
</del><span class="cx"> def masterComponent(self):
</span><span class="cx"> """
</span><span class="cx"> Return the first sub-component of a recurring type that represents the master
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendaricalendarcomponentrecurpy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/componentrecur.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/icalendar/componentrecur.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/icalendar/componentrecur.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -719,5 +719,60 @@
</span><span class="cx"> self.mEnd = self.mStart + temp
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def deriveComponent(self, recurrenceID):
+ """
+ Derive an overridden component for the associated RECURRENCE-ID. This assumes
+ that the R-ID is valid for the actual recurrence being used. It also assumes
+ that this component is the master.
+
+ @param recurrenceID: the recurrence instance
+ @type recurrenceID: L{DateTime}
+
+ @return: the derived component
+ @rtype: L{ComponentRecur} or L{None}
+ """
+
+ # Create the derived instance
+ newcomp = self.duplicate()
+
+ # Strip out unwanted recurrence properties
+ for propname in (
+ definitions.cICalProperty_RRULE,
+ definitions.cICalProperty_RDATE,
+ definitions.cICalProperty_EXRULE,
+ definitions.cICalProperty_EXDATE,
+ definitions.cICalProperty_RECURRENCE_ID,
+ ):
+ newcomp.removeProperties(propname)
+
+ # New DTSTART is the RECURRENCE-ID we are deriving but adjusted to the
+ # original DTSTART's localtime
+ dtstart = newcomp.getStart()
+ dtend = newcomp.getEnd()
+ oldduration = dtend - dtstart
+
+ newdtstartValue = recurrenceID.duplicate()
+ if not dtstart.isDateOnly():
+ if dtstart.local():
+ newdtstartValue.adjustTimezone(dtstart.getTimezone())
+ else:
+ newdtstartValue.setDateOnly(True)
+
+ newcomp.removeProperties(definitions.cICalProperty_DTSTART)
+ newcomp.removeProperties(definitions.cICalProperty_DTEND)
+ prop = Property(definitions.cICalProperty_DTSTART, newdtstartValue)
+ newcomp.addProperty(prop)
+ if not newcomp.useDuration():
+ prop = Property(definitions.cICalProperty_DTEND, newdtstartValue + oldduration)
+ newcomp.addProperty(prop)
+
+ newcomp.addProperty(Property("RECURRENCE-ID", newdtstartValue))
+
+ # After creating/changing a component we need to do this to keep PyCalendar happy
+ newcomp.finalise()
+
+ return newcomp
+
+
</ins><span class="cx"> def createExpanded(self, master, recurid):
</span><span class="cx"> return ComponentExpanded(master, recurid)
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendaricalendarpatchpy"></a>
<div class="addfile"><h4>Added: PyCalendar/branches/patch/src/pycalendar/icalendar/patch.py (0 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/icalendar/patch.py         (rev 0)
+++ PyCalendar/branches/patch/src/pycalendar/icalendar/patch.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -0,0 +1,714 @@
</span><ins>+##
+# Copyright (c) 2015 Cyrus Daboo. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import operator
+from urlparse import unquote
+from pycalendar.icalendar.componentrecur import ComponentRecur
+from pycalendar.componentbase import ComponentBase
+from pycalendar.datetime import DateTime
+from pycalendar.icalendar.property import Property
+from pycalendar.icalendar.calendar import Calendar
+
+class PatchDocument(object):
+ """
+ Represents an entire patch document by maintaining a list of all its commands.
+ """
+
+ def __init__(self, text=None):
+ self.commands = []
+ if text:
+ self.parseText(text)
+
+
+ def parseText(self, text):
+
+ # Split into lines and parse a sequence of commands from each
+ lines = text.splitlines()
+ while lines:
+ command = Command.parseFromText(lines)
+ if command is None:
+ break
+ self.commands.append(command)
+
+ if lines:
+ raise ValueError("Lines left after parsing commands: {}".format(lines))
+
+
+ def applyPatch(self, calendar):
+ """
+ Apply the patch to the specified calendar. The supplied L{Calendar} object will be
+ changed in place.
+
+ @param calendar: calendar to patch
+ @type calendar: L{Calendar}
+ """
+ for command in self.commands:
+ command.applyPatch(calendar)
+
+
+
+class Command(object):
+ """
+ Represents a patch document command.
+ """
+
+ CREATE = "create"
+ UPDATE = "update"
+ DELETE = "delete"
+ ADD = "add"
+ REMOVE = "remove"
+ ACTIONS = (CREATE, UPDATE, DELETE, ADD, REMOVE)
+
+ def __init__(self):
+ self.action = None
+ self.path = None
+ self.data = None
+
+
+ @classmethod
+ def create(cls, action, path, data=None):
+ if action not in cls.ACTIONS:
+ raise ValueError("Invalid action: {}".format(action))
+ if isinstance(path, str):
+ path = Path(path)
+ elif not isinstance(path, Path):
+ raise ValueError("Invalid path: {}".format(path))
+ if data is not None and not isinstance(data, str):
+ raise ValueError("Invalid data: {}".format(data))
+ if action == Command.DELETE:
+ if data is not None:
+ raise ValueError("Must not have data for action: {}".format(action))
+ else:
+ if data is None:
+ raise ValueError("Must have data for action: {}".format(action))
+
+ command = Command()
+ command.action = action
+ command.path = path
+ command.data = data
+ return command
+
+
+ @classmethod
+ def parseFromText(cls, lines):
+ """
+ Parse a command from a list of text format lines.
+
+ @param lines: lines in the patch document. The lines
+ parsed from the list will be removed from the list.
+ @type lines: L{list}
+
+ @return: L{Command} if a command was parsed, L{None} if not
+ """
+
+ # First line must be "<<action>> <<path>>"
+ line = lines.pop(0)
+ action, path = line.split(" ", 1)
+ if action not in Command.ACTIONS:
+ raise ValueError("Invalid action: {}".format(line))
+ try:
+ path = Path(path)
+ except ValueError:
+ raise ValueError("Invalid path: {}".format(line))
+
+ # All but the "delete" action require data
+ data = None
+ if action != Command.DELETE:
+ data = []
+ while lines:
+ line = lines.pop(0)
+ if line == ".":
+ break
+ data.append(line)
+ else:
+ raise ValueError("Invalid data: {}".format(data))
+
+ return Command.create(action, path, "\r\n".join(data) if data else None)
+
+
+ def applyPatch(self, calendar):
+ """
+ Apply the patch to the specified calendar. The supplied L{Calendar} object will be
+ changed in place.
+
+ @param calendar: calendar to patch
+ @type calendar: L{Calendar}
+ """
+ matching_items = self.path.match(calendar, for_update=(self.action == Command.UPDATE))
+ call = getattr(self, "{}Action".format(self.action))
+ if call is not None:
+ call(matching_items)
+
+
+ def createAction(self, matches):
+ """
+ Execute a create action on the matched items.
+
+ @param matches: list of matched components/properties/parameters
+ @type matches: L{list}
+ """
+ if self.path.targetComponent():
+ # Data is a list of components
+ newcomponents = self.componentData()
+ for component in matches:
+ for newcomponent in newcomponents:
+ component.addComponent(newcomponent.duplicate())
+
+ elif self.path.targetPropertyNoName():
+ # Data is a list of properties
+ newproperties = self.propertyData()
+ for component, _ignore_property in matches:
+ for newproperty in newproperties:
+ component.addProperty(newproperty.duplicate())
+
+ elif self.path.targetParameterNoName():
+ # Data is a list of parameters
+ newparameters = self.parameterData()
+ for _ignore_component, property, _ignore_parameter_name in matches:
+ for parameter in newparameters:
+ # Remove existing, then add
+ property.removeParameters(parameter.getName())
+ property.addParameter(parameter.duplicate())
+ else:
+ raise ValueError("create action path is not valid: {}".format(self.path))
+
+
+ def updateAction(self, matches):
+ """
+ Execute an update action on the matched items.
+
+ @param matches: list of matched components/properties/parameters
+ @type matches: L{list}
+ """
+
+ if self.path.targetComponent():
+ # First remove matched components and record the parent
+ parent = None
+ for component in matches:
+ parent = component.getParentComponent()
+ component.removeFromParent()
+
+ # Now add new components (from the data) to the parent
+ if parent is not None:
+ newcomponents = self.componentData()
+ for component in matches:
+ for newcomponent in newcomponents:
+ parent.addComponent(newcomponent.duplicate())
+
+ elif self.path.targetProperty():
+ # First remove matched properties and record the parent components
+ components = set()
+ for component, property in matches:
+ components.add(component)
+ if property is not None:
+ component.removeProperty(property)
+
+ # Now add new properties (from the data) to each parent component
+ newproperties = self.propertyData()
+ for component in components:
+ for newproperty in newproperties:
+ component.addProperty(newproperty.duplicate())
+
+ elif self.path.targetParameterNoName():
+ # First remove matched parameters and record the parent properties
+ properties = set()
+ for _ignore_component, property, parameter_name in matches:
+ properties.add(properties)
+ property.removeParameters(parameter_name)
+
+ # Now add new parameters (from the data) to each parent property
+ newparameters = self.parameterData()
+ for property in properties:
+ for parameter in newparameters:
+ # Remove existing, then add
+ property.removeParameters(parameter.getName())
+ property.addParameter(parameter.duplicate())
+ else:
+ raise ValueError("update action path is not valid: {}".format(self.path))
+
+
+ def deleteAction(self, matches):
+ """
+ Execute a delete action on the matched items.
+
+ @param matches: list of matched components/properties/parameters
+ @type matches: L{list}
+ """
+ if self.path.targetComponent():
+ for component in matches:
+ component.removeFromParent()
+
+ elif self.path.targetProperty():
+ for component, property in matches:
+ component.removeProperty(property)
+
+ elif self.path.targetParameter():
+ for _ignore_component, property, parameter_name in matches:
+ property.removeParameters(parameter_name)
+ else:
+ raise ValueError("delete action path is not valid: {}".format(self.path))
+
+
+ def addAction(self, matches):
+ pass
+
+
+ def removeAction(self, matches):
+ pass
+
+
+ def componentData(self):
+ """
+ Parse the data item into a list of components.
+
+ @return: list of components
+ @rtype: L{list} of L{Component}
+ """
+
+ # Data must be a set of components. Wrap the data inside a VCALENDAR and parse
+ newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:ignore
+{}
+END:VCALENDAR
+""".format(self.data)
+ calendar = Calendar.parseText(newdata)
+ return calendar.getComponents()
+
+
+ def propertyData(self):
+ """
+ Parse the data item into a list of properties.
+
+ @return: list of components
+ @rtype: L{list} of L{Property}
+ """
+ return [Property.parseText(line) for line in self.data.splitlines()]
+
+
+ def parameterData(self):
+ """
+ Parse the data item into a list of parameters.
+
+ @return: list of components
+ @rtype: L{list} of L{Parameter}
+ """
+
+ # Data must be a sets of parameters. Wrap each set inside a property and then return them all
+ newparameters = []
+ newproperties = [Property.parseText("X-FOO{}:ignore".format(line)) for line in self.data.splitlines()]
+ for newproperty in newproperties:
+ for parameters in newproperty.getParameters().values():
+ newparameters.extend(parameters)
+ return newparameters
+
+
+
+class Path(object):
+ """
+ A path item used to select one or more iCalendar elements
+ """
+
+ def __init__(self, path):
+ """
+ Create a L{Path} by parsing a text path.
+
+ @param path: the path to parse
+ @type path: L{str}
+ """
+ self.components = []
+ self.property = None
+ self.parameter = None
+ self._parsePath(path)
+
+
+ def targetComponent(self):
+ """
+ Indicate whether the path targets a component.
+
+ @return: L{True} for a component target, L{False} otherwise.
+ @rtype: L{bool}
+ """
+ return self.property is None
+
+
+ def targetProperty(self):
+ """
+ Indicate whether the path targets a property.
+
+ @return: L{True} for a property target, L{False} otherwise.
+ @rtype: L{bool}
+ """
+ return (
+ self.property is not None and
+ not self.property.noName() and
+ self.parameter is None
+ )
+
+
+ def targetPropertyNoName(self):
+ """
+ Indicate whether the path targets a property.
+
+ @return: L{True} for a property target, L{False} otherwise.
+ @rtype: L{bool}
+ """
+ return self.property is not None and self.property.noName()
+
+
+ def targetParameter(self):
+ """
+ Indicate whether the path targets a parameter.
+
+ @return: L{True} for a parameter target, L{False} otherwise.
+ @rtype: L{bool}
+ """
+ return (
+ self.property is not None and
+ self.parameter is not None and
+ not self.parameter.noName()
+ )
+
+
+ def targetParameterNoName(self):
+ """
+ Indicate whether the path targets a parameter.
+
+ @return: L{True} for a parameter target, L{False} otherwise.
+ @rtype: L{bool}
+ """
+ return (
+ self.property is not None and
+ self.parameter is not None and
+ self.parameter.noName()
+ )
+
+
+ def _parsePath(self, path):
+ """
+ Parse a text path into its constituent segments.
+
+ @param path: the path to parse
+ @type path: L{str}
+ """
+
+ segments = path.split("/")
+ property_segment = None
+ parameter_segment = None
+ if segments[0] != "":
+ raise ValueError("Invalid path: {}".format(path))
+ del segments[0]
+ if "#" in segments[-1]:
+ segments[-1], property_segment = segments[-1].split("#", 1)
+ if ";" in property_segment:
+ property_segment, parameter_segment = property_segment.split(";", 1)
+
+ for item in range(len(segments)):
+ self.components.append(Path.ComponentSegment(segments[item]))
+ if property_segment is not None:
+ self.property = Path.PropertySegment(property_segment)
+ if parameter_segment is not None:
+ self.parameter = Path.ParameterSegment(parameter_segment)
+
+
+ class ComponentSegment(object):
+ """
+ Represents a component segment of an L{Path}.
+ """
+
+ def __init__(self, segment):
+ """
+ Create a component segment of a path by parsing the text.
+
+ @param path: the segment to parse
+ @type path: L{str}
+ """
+ self.name = None
+ self.uid = None
+ self.rid = None
+ self.rid_value = None
+
+ self._parseSegment(segment)
+
+
+ def __repr__(self):
+ return "<ComponentSegment: {name}[{uid}][{rid}]".format(
+ name=self.name,
+ uid=self.uid,
+ rid=(self.rid_value if self.rid_value is not None else "*") if self.rid else None
+ )
+
+
+ def __eq__(self, other):
+ return (self.name == other.name) and \
+ (self.uid == other.uid) and \
+ (self.rid == other.rid) and \
+ (self.rid_value == other.rid_value)
+
+
+ def _parseSegment(self, segment):
+ """
+ Parse a component segment of a path into its constituent parts.
+
+ @param path: the segment to parse
+ @type path: L{str}
+ """
+ pos = segment.find("[")
+ if pos != -1:
+ self.name, segment_rest = segment.split("[", 1)
+ segments = segment_rest.split("[")
+ if segments[0].startswith("UID=") and segments[0][-1] == "]":
+ self.uid = unquote(segments[0][4:-1])
+ del segments[0]
+ if segments and segments[0].startswith("RECURRENCE-ID=") and segments[0][-1] == "]":
+ rid = unquote(segments[0][14:-1])
+ try:
+ self.rid_value = DateTime.parseText(rid) if rid else None
+ except ValueError:
+ raise ValueError("Invalid component match {}".format(segment))
+ self.rid = True
+ del segments[0]
+
+ if segments:
+ raise ValueError("Invalid component match {}".format(segment))
+ else:
+ self.name = segment
+
+ self.name = self.name.upper()
+
+
+ def match(self, items):
+ """
+ Returns all sub-components of the components passed in via the L{items} list
+ that match this path.
+
+ @param items: calendar items to match
+ @type items: L{list}
+
+ @return: items matched
+ @rtype: L{list}
+ """
+
+ results = []
+ for item in items:
+ assert(isinstance(item, ComponentBase))
+ matches = item.getComponents(self.name)
+ if self.uid and matches:
+ matches = [item for item in matches if item.getUID() == self.uid]
+ if self.rid and matches:
+ # self.rid is None if no RECURRENCE-ID= appears in the path.
+ # self.rid_value is None if RECURRENCE-ID= appears with no value - match the master instance
+ # Otherwise match the specific self.rid value.
+ rid_matches = [item for item in matches if isinstance(item, ComponentRecur) and item.getRecurrenceID() == self.rid_value]
+ if len(rid_matches) == 0:
+ if self.rid_value:
+ # Try deriving an instance - fail if cannot
+ # Need to have the master first
+ masters = [item for item in matches if isinstance(item, ComponentRecur) and item.getRecurrenceID() is None]
+ if not masters:
+ raise ValueError("No master component for path {}".format(self))
+ elif len(masters) > 1:
+ raise ValueError("Too many master components for path {}".format(self))
+ derived = masters[0].deriveComponent(self.rid_value)
+ masters[0].getParentComponent().addComponent(derived)
+ rid_matches.append(derived)
+ matches = rid_matches
+ results.extend(matches)
+
+ return results
+
+
+ class PropertySegment(object):
+ """
+ Represents a property segment of an L{Path}.
+ """
+
+ def __init__(self, segment):
+ """
+ Create a property segment of a path by parsing the text.
+
+ @param path: the segment to parse
+ @type path: L{str}
+ """
+ self.name = None
+ self.matchCondition = None
+ self._parseSegment(segment)
+
+
+ def __repr__(self):
+ return "<PropertySegment: {s.name}[{s.matchCondition}]".format(s=self)
+
+
+ def __eq__(self, other):
+ return (self.name == other.name) and \
+ (self.matchCondition == other.matchCondition)
+
+
+ def _parseSegment(self, segment):
+ """
+ Parse a property segment of a path into its constituent parts.
+
+ @param path: the segment to parse
+ @type path: L{str}
+ """
+ if "[" in segment:
+ self.name, segment_rest = segment.split("[", 1)
+ matches = segment_rest.split("[")
+ if len(matches) != 1:
+ raise ValueError("Invalid property match {}".format(segment))
+ if matches[0][-1] != "]" or len(matches[0]) < 4:
+ raise ValueError("Invalid property match {}".format(segment))
+ if matches[0][0] == "=":
+ op = operator.eq
+ elif matches[0][0] == "!":
+ op = operator.ne
+ else:
+ raise ValueError("Invalid property match {}".format(segment))
+ self.matchCondition = (unquote(matches[0][1:-1]), op,)
+ else:
+ self.name = segment
+
+
+ def noName(self):
+ return self.name == ""
+
+
+ def match(self, components, for_update):
+ """
+ Returns all properties of the components passed in via the L{items} list
+ that match this path.
+
+ @param components: components to match
+ @type components: L{list}
+
+ @return: items matched
+ @rtype: L{list}
+ """
+
+ # Empty name is used for create
+ if self.name:
+ results = []
+ for component in components:
+ assert(isinstance(component, ComponentBase))
+ if self.matchCondition is not None:
+ matches = [(component, prop,) for prop in component.getProperties(self.name) if self.matchCondition[1](prop.getValue().getTextValue(), self.matchCondition[0])]
+ else:
+ matches = [(component, prop,) for prop in component.getProperties(self.name)]
+ if len(matches) == 0 and for_update:
+ # If no property exists, return L{None} so that an update action will add one
+ matches = [(component, None)]
+ results.extend(matches)
+ else:
+ results = [(component, None,) for component in components]
+
+ return results
+
+
+ class ParameterSegment(object):
+ """
+ Represents a parameter segment of an L{Path}.
+ """
+
+ def __init__(self, segment):
+ """
+ Create a parameter segment of a path by parsing the text.
+
+ @param path: the segment to parse
+ @type path: L{str}
+ """
+ self.name = None
+ self._parseSegment(segment)
+
+
+ def __repr__(self):
+ return "<ParameterSegment: {s.name}".format(s=self)
+
+
+ def __eq__(self, other):
+ return (self.name == other.name)
+
+
+ def _parseSegment(self, segment):
+ """
+ Parse a parameter segment of a path into its constituent parts.
+
+ @param path: the segment to parse
+ @type path: L{str}
+ """
+ if "[" in segment:
+ raise ValueError("Invalid parameter segment {}".format(segment))
+ else:
+ self.name = segment
+
+
+ def noName(self):
+ return self.name == ""
+
+
+ def match(self, properties):
+ """
+ Returns all properties of the components passed in via the L{items} list
+ that match this path, together with the parameter name being targeted.
+
+ @param properties: properties to match
+ @type properties: L{list}
+
+ @return: items matched
+ @rtype: L{list}
+ """
+
+ # Empty name is used for create
+ if self.name:
+ results = []
+ for component, property in properties:
+ assert(isinstance(component, ComponentBase))
+ assert(isinstance(property, Property))
+ results.append((component, property, self.name,))
+ else:
+ results = [(component, property, None,) for component, property in properties]
+
+ return results
+
+
+ def match(self, calendar, for_update=False):
+ """
+ Return the list of matching items in the specified calendar.
+
+ @param calendar: calendar to match
+ @type calendar: L{Calendar}
+ @param for_update: L{True} if a property match should return an empty
+ result when there is no match item and no matching property
+ @type for_update: L{bool}
+
+ @return: items matched
+ @rtype: L{list}
+ """
+
+ # First segment of path is always assumed to be VCALENDAR - we double check that
+ if self.components[0].name != "VCALENDAR" or calendar.getType().upper() != "VCALENDAR":
+ return []
+
+ # Start with the VCALENDAR object as the initial match
+ results = [calendar]
+ for component_segment in self.components[1:]:
+ results = component_segment.match(results)
+
+ if self.property is not None:
+ results = self.property.match(results, for_update)
+ if self.parameter is not None:
+ results = self.parameter.match(results)
+
+ return results
</ins></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendaricalendarrecurrencepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/recurrence.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/icalendar/recurrence.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/icalendar/recurrence.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -543,134 +543,134 @@
</span><span class="cx">
</span><span class="cx"> def generate(self, os):
</span><span class="cx"> try:
</span><del>- os.write(definitions.cICalValue_RECUR_FREQ)
- os.write("=")
</del><ins>+ os.write(self.getText())
+ except:
+ pass
</ins><span class="cx">
</span><del>- if self.mFreq == definitions.eRecurrence_SECONDLY:
- os.write(definitions.cICalValue_RECUR_SECONDLY)
</del><span class="cx">
</span><del>- elif self.mFreq == definitions.eRecurrence_MINUTELY:
- os.write(definitions.cICalValue_RECUR_MINUTELY)
</del><ins>+ def getText(self):
+ result = []
+ result.append(definitions.cICalValue_RECUR_FREQ)
+ result.append("=")
</ins><span class="cx">
</span><del>- elif self.mFreq == definitions.eRecurrence_HOURLY:
- os.write(definitions.cICalValue_RECUR_HOURLY)
</del><ins>+ if self.mFreq == definitions.eRecurrence_SECONDLY:
+ result.append(definitions.cICalValue_RECUR_SECONDLY)
</ins><span class="cx">
</span><del>- elif self.mFreq == definitions.eRecurrence_DAILY:
- os.write(definitions.cICalValue_RECUR_DAILY)
</del><ins>+ elif self.mFreq == definitions.eRecurrence_MINUTELY:
+ result.append(definitions.cICalValue_RECUR_MINUTELY)
</ins><span class="cx">
</span><del>- elif self.mFreq == definitions.eRecurrence_WEEKLY:
- os.write(definitions.cICalValue_RECUR_WEEKLY)
</del><ins>+ elif self.mFreq == definitions.eRecurrence_HOURLY:
+ result.append(definitions.cICalValue_RECUR_HOURLY)
</ins><span class="cx">
</span><del>- elif self.mFreq == definitions.eRecurrence_MONTHLY:
- os.write(definitions.cICalValue_RECUR_MONTHLY)
</del><ins>+ elif self.mFreq == definitions.eRecurrence_DAILY:
+ result.append(definitions.cICalValue_RECUR_DAILY)
</ins><span class="cx">
</span><del>- elif self.mFreq == definitions.eRecurrence_YEARLY:
- os.write(definitions.cICalValue_RECUR_YEARLY)
</del><ins>+ elif self.mFreq == definitions.eRecurrence_WEEKLY:
+ result.append(definitions.cICalValue_RECUR_WEEKLY)
</ins><span class="cx">
</span><del>- if self.mUseCount:
- os.write(";")
- os.write(definitions.cICalValue_RECUR_COUNT)
- os.write("=")
- os.write(str(self.mCount))
- elif self.mUseUntil:
- os.write(";")
- os.write(definitions.cICalValue_RECUR_UNTIL)
- os.write("=")
- self.mUntil.generate(os)
</del><ins>+ elif self.mFreq == definitions.eRecurrence_MONTHLY:
+ result.append(definitions.cICalValue_RECUR_MONTHLY)
</ins><span class="cx">
</span><del>- if self.mInterval > 1:
- os.write(";")
- os.write(definitions.cICalValue_RECUR_INTERVAL)
- os.write("=")
- os.write(str(self.mInterval))
</del><ins>+ elif self.mFreq == definitions.eRecurrence_YEARLY:
+ result.append(definitions.cICalValue_RECUR_YEARLY)
</ins><span class="cx">
</span><del>- self.generateList(os, definitions.cICalValue_RECUR_BYSECOND, self.mBySeconds)
- self.generateList(os, definitions.cICalValue_RECUR_BYMINUTE, self.mByMinutes)
- self.generateList(os, definitions.cICalValue_RECUR_BYHOUR, self.mByHours)
</del><ins>+ if self.mUseCount:
+ result.append(";")
+ result.append(definitions.cICalValue_RECUR_COUNT)
+ result.append("=")
+ result.append(str(self.mCount))
+ elif self.mUseUntil:
+ result.append(";")
+ result.append(definitions.cICalValue_RECUR_UNTIL)
+ result.append("=")
+ result.append(self.mUntil.getText())
</ins><span class="cx">
</span><del>- if (self.mByDay is not None) and (len(self.mByDay) != 0):
- os.write(";")
- os.write(definitions.cICalValue_RECUR_BYDAY)
- os.write("=")
- comma = False
- for iter in self.mByDay:
- if comma:
- os.write(",")
- comma = True
</del><ins>+ if self.mInterval > 1:
+ result.append(";")
+ result.append(definitions.cICalValue_RECUR_INTERVAL)
+ result.append("=")
+ result.append(str(self.mInterval))
</ins><span class="cx">
</span><del>- if iter[0] != 0:
- os.write(str(iter[0]))
</del><ins>+ result.append(self.getList(definitions.cICalValue_RECUR_BYSECOND, self.mBySeconds))
+ result.append(self.getList(definitions.cICalValue_RECUR_BYMINUTE, self.mByMinutes))
+ result.append(self.getList(definitions.cICalValue_RECUR_BYHOUR, self.mByHours))
</ins><span class="cx">
</span><del>- if iter[1] == definitions.eRecurrence_WEEKDAY_SU:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_SU)
</del><ins>+ if (self.mByDay is not None) and (len(self.mByDay) != 0):
+ result.append(";")
+ result.append(definitions.cICalValue_RECUR_BYDAY)
+ result.append("=")
+ comma = False
+ for iter in self.mByDay:
+ if comma:
+ result.append(",")
+ comma = True
</ins><span class="cx">
</span><del>- elif iter[1] == definitions.eRecurrence_WEEKDAY_MO:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_MO)
</del><ins>+ if iter[0] != 0:
+ result.append(str(iter[0]))
</ins><span class="cx">
</span><del>- elif iter[1] == definitions.eRecurrence_WEEKDAY_TU:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_TU)
</del><ins>+ if iter[1] == definitions.eRecurrence_WEEKDAY_SU:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_SU)
</ins><span class="cx">
</span><del>- elif iter[1] == definitions.eRecurrence_WEEKDAY_WE:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_WE)
</del><ins>+ elif iter[1] == definitions.eRecurrence_WEEKDAY_MO:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_MO)
</ins><span class="cx">
</span><del>- elif iter[1] == definitions.eRecurrence_WEEKDAY_TH:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_TH)
</del><ins>+ elif iter[1] == definitions.eRecurrence_WEEKDAY_TU:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_TU)
</ins><span class="cx">
</span><del>- elif iter[1] == definitions.eRecurrence_WEEKDAY_FR:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_FR)
</del><ins>+ elif iter[1] == definitions.eRecurrence_WEEKDAY_WE:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_WE)
</ins><span class="cx">
</span><del>- elif iter[1] == definitions.eRecurrence_WEEKDAY_SA:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_SA)
</del><ins>+ elif iter[1] == definitions.eRecurrence_WEEKDAY_TH:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_TH)
</ins><span class="cx">
</span><del>- self.generateList(os, definitions.cICalValue_RECUR_BYMONTHDAY, self.mByMonthDay)
- self.generateList(os, definitions.cICalValue_RECUR_BYYEARDAY, self.mByYearDay)
- self.generateList(os, definitions.cICalValue_RECUR_BYWEEKNO, self.mByWeekNo)
- self.generateList(os, definitions.cICalValue_RECUR_BYMONTH, self.mByMonth)
- self.generateList(os, definitions.cICalValue_RECUR_BYSETPOS, self.mBySetPos)
</del><ins>+ elif iter[1] == definitions.eRecurrence_WEEKDAY_FR:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_FR)
</ins><span class="cx">
</span><del>- # MO is the default so we do not need it
- if self.mWeekstart != definitions.eRecurrence_WEEKDAY_MO:
- os.write(";")
- os.write(definitions.cICalValue_RECUR_WKST)
- os.write("=")
</del><ins>+ elif iter[1] == definitions.eRecurrence_WEEKDAY_SA:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_SA)
</ins><span class="cx">
</span><del>- if self.mWeekstart == definitions.eRecurrence_WEEKDAY_SU:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_SU)
</del><ins>+ result.append(self.getList(definitions.cICalValue_RECUR_BYMONTHDAY, self.mByMonthDay))
+ result.append(self.getList(definitions.cICalValue_RECUR_BYYEARDAY, self.mByYearDay))
+ result.append(self.getList(definitions.cICalValue_RECUR_BYWEEKNO, self.mByWeekNo))
+ result.append(self.getList(definitions.cICalValue_RECUR_BYMONTH, self.mByMonth))
+ result.append(self.getList(definitions.cICalValue_RECUR_BYSETPOS, self.mBySetPos))
</ins><span class="cx">
</span><del>- elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_MO:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_MO)
</del><ins>+ # MO is the default so we do not need it
+ if self.mWeekstart != definitions.eRecurrence_WEEKDAY_MO:
+ result.append(";")
+ result.append(definitions.cICalValue_RECUR_WKST)
+ result.append("=")
</ins><span class="cx">
</span><del>- elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_TU:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_TU)
</del><ins>+ if self.mWeekstart == definitions.eRecurrence_WEEKDAY_SU:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_SU)
</ins><span class="cx">
</span><del>- elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_WE:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_WE)
</del><ins>+ elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_MO:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_MO)
</ins><span class="cx">
</span><del>- elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_TH:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_TH)
</del><ins>+ elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_TU:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_TU)
</ins><span class="cx">
</span><del>- elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_FR:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_FR)
</del><ins>+ elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_WE:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_WE)
</ins><span class="cx">
</span><del>- elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_SA:
- os.write(definitions.cICalValue_RECUR_WEEKDAY_SA)
</del><ins>+ elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_TH:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_TH)
</ins><span class="cx">
</span><del>- except:
- pass
</del><ins>+ elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_FR:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_FR)
</ins><span class="cx">
</span><ins>+ elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_SA:
+ result.append(definitions.cICalValue_RECUR_WEEKDAY_SA)
</ins><span class="cx">
</span><del>- def generateList(self, os, title, items):
</del><ins>+ return "".join(result)
</ins><span class="cx">
</span><ins>+
+ def getList(self, title, items):
+
</ins><span class="cx"> if (items is not None) and (len(items) != 0):
</span><del>- os.write(";")
- os.write(title)
- os.write("=")
- comma = False
- for e in items:
- if comma:
- os.write(",")
- comma = True
- os.write(str(e))
</del><ins>+ return ";{}={}".format(title, ",".join([str(item) for item in items]))
+ else:
+ return ""
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> def writeXML(self, node, namespace):
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendaricalendarrequeststatusvaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/requeststatusvalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/icalendar/requeststatusvalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/icalendar/requeststatusvalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -79,9 +79,16 @@
</span><span class="cx">
</span><span class="cx"> # os - StringIO object
</span><span class="cx"> def generate(self, os):
</span><del>- utils.generateTextList(os, self.mValue if len(self.mValue) < 3 or self.mValue[2] else self.mValue[:2])
</del><ins>+ try:
+ os.write(self.getTextValue())
+ except:
+ pass
</ins><span class="cx">
</span><span class="cx">
</span><ins>+ def getTextValue(self):
+ return utils.getTextList(self.mValue if len(self.mValue) < 3 or self.mValue[2] else self.mValue[:2])
+
+
</ins><span class="cx"> def writeXML(self, node, namespace):
</span><span class="cx"> value = self.getXMLNode(node, namespace)
</span><span class="cx">
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendaricalendartestsrrule_examplesjson"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/tests/rrule_examples.json (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/icalendar/tests/rrule_examples.json        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/icalendar/tests/rrule_examples.json        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -15,6 +15,14 @@
</span><span class="cx">                 ]
</span><span class="cx">         },
</span><span class="cx">         {
</span><ins>+                "rule": "FREQ=YEARLY;BYWEEKNO=1",
+                "start": "20141130T000000",
+                "end": "20160101T000000",
+                "results": [
+ "20141229T000000"
+                ]
+        },
+        {
</ins><span class="cx">                 "rule": "FREQ=MONTHLY",
</span><span class="cx">                 "start": "20140140T120000",
</span><span class="cx">                 "end": "20150101T000000",
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendaricalendarteststest_patchpy"></a>
<div class="addfile"><h4>Added: PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_patch.py (0 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_patch.py         (rev 0)
+++ PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_patch.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -0,0 +1,2237 @@
</span><ins>+# -*- coding: utf-8 -*-
+##
+# Copyright (c) 2015 Cyrus Daboo. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+from pycalendar.datetime import DateTime
+from pycalendar.icalendar.calendar import Calendar
+from pycalendar.icalendar.patch import Command, Path, PatchDocument
+import operator
+import unittest
+
+class TestPatchDocument(unittest.TestCase):
+
+
+ def _testPatch(self, data):
+
+ for ctr, items in enumerate(data):
+ calendar = Calendar.parseText(items["before"])
+ patcher = PatchDocument(items["patch"])
+ patcher.applyPatch(calendar)
+ self.assertEqual(str(calendar), items["after"].replace("\n", "\r\n"), msg="Failed test #{}: {}\n{}".format(ctr + 1, items["title"], str(calendar)))
+
+
+ def test_createComponent_Simple(self):
+ """
+ Test that creation of a single component works.
+ """
+
+ data = [
+ {
+ "title": "Add one component to a calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """create /VCALENDAR
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+.
+""",
+ },
+ {
+ "title": "Add two components to a calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:April Fool's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """create /VCALENDAR
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:April Fool's Day
+END:VEVENT
+.
+""",
+ },
+ {
+ "title": "Add one component to an event",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """create /VCALENDAR/VEVENT
+BEGIN:VALARM
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+ACTION:DISPLAY
+END:VALARM
+.
+""",
+ },
+ ]
+
+ self._testPatch(data)
+
+
+ def test_createProperty_Simple(self):
+ """
+ Test that creation of a single property works.
+ """
+
+ data = [
+ {
+ "title": "Add one property to a calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """create /VCALENDAR#
+CALSCALE:GREGORIAN
+.
+""",
+ },
+ {
+ "title": "Add two properties to a calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+REFRESH-INTERVAL:10
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """create /VCALENDAR#
+CALSCALE:GREGORIAN
+REFRESH-INTERVAL:10
+.
+""",
+ },
+ {
+ "title": "Add one property to an event",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:CANCELLED
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """create /VCALENDAR/VEVENT#
+STATUS:CANCELLED
+.
+""",
+ },
+ {
+ "title": "Add two properties to an event",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+STATUS:CANCELLED
+SUMMARY:New Year's Day
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """create /VCALENDAR/VEVENT#
+STATUS:CANCELLED
+TRANSP:TRANSPARENT
+.
+""",
+ },
+ ]
+
+ self._testPatch(data)
+
+
+ def test_createParameter_Simple(self):
+ """
+ Test that creation of a single parameter works.
+ """
+
+ data = [
+ {
+ "title": "Add one parameter to a property",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY;LABEL=Party Time!:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """create /VCALENDAR/VEVENT#SUMMARY;
+;LABEL=Party Time!
+.
+""",
+ },
+ {
+ "title": "Add one parameter to a property with update",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY;LABEL=Holiday:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY;LABEL=Party Time!:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """create /VCALENDAR/VEVENT#SUMMARY;
+;LABEL=Party Time!
+.
+""",
+ },
+ ]
+
+ self._testPatch(data)
+
+
+ def test_updateComponent_Simple(self):
+ """
+ Test that update of components works.
+ """
+
+ data = [
+ {
+ "title": "Update one component in a calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day - party time
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """update /VCALENDAR/VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day - party time
+END:VEVENT
+.
+""",
+ },
+ {
+ "title": "Update one, add another component to a calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day - party time
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20030101
+DTSTART;VALUE=DATE:20030101
+DTEND;VALUE=DATE:20030102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+STATUS:CANCELLED
+SUMMARY:New Year's Day - cancelled
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """update /VCALENDAR/VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day - party time
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20030101
+DTSTART;VALUE=DATE:20030101
+DTEND;VALUE=DATE:20030102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+STATUS:CANCELLED
+SUMMARY:New Year's Day - cancelled
+END:VEVENT
+.
+""",
+ },
+ {
+ "title": "Update one component in a calendar with others present",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day - party time
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20030101
+DTSTART;VALUE=DATE:20030101
+DTEND;VALUE=DATE:20030102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+STATUS:CANCELLED
+SUMMARY:New Year's Day - cancelled
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day - party time
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20030101
+DTSTART;VALUE=DATE:20030101
+DTEND;VALUE=DATE:20030102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day - it is on again!
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """update /VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252][RECURRENCE-ID=20030101]
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20030101
+DTSTART;VALUE=DATE:20030101
+DTEND;VALUE=DATE:20030102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day - it is on again!
+END:VEVENT
+.
+""",
+ },
+ ]
+
+ self._testPatch(data)
+
+
+ def test_updateComponent_Recur(self):
+ """
+ Test that update of components works.
+ """
+
+ data = [
+ {
+ "title": "Update one property in an instance",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20020102
+DTSTART;VALUE=DATE:20020102
+DTEND;VALUE=DATE:20020103
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day - party time
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """update /VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252][RECURRENCE-ID=20020102]#SUMMARY
+SUMMARY:New Year's Day - party time
+.
+""",
+ },
+ ]
+
+ self._testPatch(data)
+
+
+ def test_updateProperty_Simple(self):
+ """
+ Test that update of a single property works.
+ """
+
+ data = [
+ {
+ "title": "Update (add) one property in a calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """update /VCALENDAR/VEVENT#TRANSP
+TRANSP:TRANSPARENT
+.
+""",
+ },
+ {
+ "title": "Update (existing) property in a calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """update /VCALENDAR/VEVENT#TRANSP
+TRANSP:TRANSPARENT
+.
+""",
+ },
+ {
+ "title": "Update one property in all events",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:April Fool's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+STATUS:CONFIRMED
+SUMMARY:April Fool's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+STATUS:CONFIRMED
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """update /VCALENDAR/VEVENT#STATUS
+STATUS:CONFIRMED
+.
+""",
+ },
+ {
+ "title": "Update one property in one event",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+STATUS:CANCELLED
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+STATUS:CANCELLED
+SUMMARY:April Fool's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+STATUS:CANCELLED
+SUMMARY:April Fool's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+STATUS:CONFIRMED
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """update /VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252]#STATUS
+STATUS:CONFIRMED
+.
+""",
+ },
+ ]
+
+ self._testPatch(data)
+
+
+ def test_deleteComponent_Simple(self):
+ """
+ Test that deletion of a single component works.
+ """
+
+ data = [
+ {
+ "title": "Remove one component from single event calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT
+""",
+ },
+ {
+ "title": "Remove one component from multi event calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:April Fool's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:April Fool's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252]
+""",
+ },
+ {
+ "title": "Remove all components from multi event calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:April Fool's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT
+""",
+ },
+ {
+ "title": "Remove one alarm from single event calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT/VALARM
+""",
+ },
+ {
+ "title": "Remove one alarm from single multi alarm event calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+ACTION:DISPLAY
+END:VALARM
+BEGIN:VALARM
+UID:D78F6991-DFDD-491B-8334-FA9BF8E4F11C
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Event reminder
+TRIGGER:-PT8M
+UID:D9D1AC84-F629-4B9D-9B6B-4A6CA9A11FEF
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT/VALARM[UID=D78F6991-DFDD-491B-8334-FA9BF8E4F11C]
+""",
+ },
+ ]
+
+ self._testPatch(data)
+
+
+ def test_deleteProperty_Simple(self):
+ """
+ Test that deletion of a single property works.
+ """
+
+ data = [
+ {
+ "title": "Remove one property from VCALENDAR",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR#CALSCALE
+""",
+ },
+ {
+ "title": "Remove one property from VEVENT",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT#RRULE
+""",
+ },
+ {
+ "title": "Remove one property from multi event calendar",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:DFAEA248-AC4E-44D6-8FA3-ACAAA7BA7943
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:April Fool's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:DFAEA248-AC4E-44D6-8FA3-ACAAA7BA7943
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:April Fool's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252]#RRULE
+""",
+ },
+ {
+ "title": "Remove one of many properties from VEVENT",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT#ATTENDEE[=mailto:user03@example.com]
+""",
+ },
+ {
+ "title": "Remove one of many properties from one of many VEVENTs",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20120101
+DTSTART;VALUE=DATE:20120101
+DTEND;VALUE=DATE:20120102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20120101
+DTSTART;VALUE=DATE:20120101
+DTEND;VALUE=DATE:20120102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT[RECURRENCE-ID=20120101]#ATTENDEE[=mailto:user03@example.com]
+""",
+ },
+ ]
+
+ self._testPatch(data)
+
+
+ def test_deleteParameter_Simple(self):
+ """
+ Test that deletion of a single parameter works.
+ """
+
+ data = [
+ {
+ "title": "Remove parameter from all properties in VEVENT",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE:mailto:user01@example.com
+ATTENDEE:mailto:user02@example.com
+ATTENDEE:mailto:user03@example.com
+ATTENDEE:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT#ATTENDEE;PARTSTAT
+""",
+ },
+ {
+ "title": "Remove parameter from one property in VEVENT",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT#ATTENDEE[=mailto:user03@example.com];PARTSTAT
+""",
+ },
+ {
+ "title": "Remove one parameter from one of many parameters from one of many VEVENTs",
+ "before": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20120101
+DTSTART;VALUE=DATE:20120101
+DTEND;VALUE=DATE:20120102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "after": """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID;VALUE=DATE:20120101
+DTSTART;VALUE=DATE:20120101
+DTEND;VALUE=DATE:20120102
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user01@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02@example.com
+ATTENDEE:mailto:user03@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04@example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01@example.com
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+ "patch": """delete /VCALENDAR/VEVENT[RECURRENCE-ID=20120101]#ATTENDEE[=mailto:user03@example.com];PARTSTAT
+""",
+ },
+ ]
+
+ self._testPatch(data)
+
+
+
+class TestCommand(unittest.TestCase):
+
+ def testCreate(self):
+ test_data = (
+ # Valid
+ (Command.CREATE, "/VCALENDAR", "BEGIN:VEVENT\r\nEND:VEVENT\r\n", True,),
+ (Command.CREATE, Path("/VCALENDAR"), "BEGIN:VEVENT\r\nEND:VEVENT\r\n", True,),
+ (Command.UPDATE, "/VCALENDAR#VERSION", ":2.0\r\n", True,),
+ (Command.UPDATE, Path("/VCALENDAR#VERSION"), ":2.0\r\n", True,),
+ (Command.DELETE, "/VCALENDAR#VERSION", None, True,),
+
+ # Invalid
+ ("foo", "/VCALENDAR", "BEGIN:VEVENT\r\nEND:VEVENT\r\n", False,),
+ (Command.CREATE, 1, "BEGIN:VEVENT\r\nEND:VEVENT\r\n", False,),
+ (Command.CREATE, "/VCALENDAR", 1, False,),
+ (Command.CREATE, "/VCALENDAR", None, False,),
+ (Command.UPDATE, "/VCALENDAR#VERSION", None, False,),
+ (Command.DELETE, "/VCALENDAR#VERSION", ":2.0\r\n", False,),
+ )
+
+ for action, path, data, valid in test_data:
+ try:
+ command = Command.create(action, path, data)
+ except ValueError:
+ self.assertFalse(valid)
+ else:
+ self.assertTrue(valid)
+ self.assertTrue(isinstance(command, Command))
+
+
+ def testParseLines(self):
+ test_data = (
+ # Valid
+ ("""create /VCALENDAR
+BEGIN:VEVENT
+END:VEVENT
+.
+""", True,),
+ ("""update /VCALENDAR#VERSION
+:2.0
+.
+""", True,),
+ ("""delete /VCALENDAR#VERSION
+""", True,),
+
+ # Invalid
+ ("""foo /VCALENDAR
+BEGIN:VEVENT
+END:VEVENT
+.
+""", False,),
+ ("""create 1
+BEGIN:VEVENT
+END:VEVENT
+.
+""", False,),
+ ("""create /VCALENDAR
+""", False,),
+ ("""create /VCALENDAR
+foo
+bar
+""", False,),
+ ("""update /VCALENDAR
+""", False,),
+ ("""update /VCALENDAR
+foo
+bar
+""", False,),
+ )
+
+ for txt, valid in test_data:
+ try:
+ command = Command.parseFromText(txt.splitlines())
+ except ValueError:
+ self.assertFalse(valid, msg=txt)
+ else:
+ self.assertTrue(valid, msg=txt)
+ self.assertTrue(isinstance(command, Command))
+
+
+
+class TestPath(unittest.TestCase):
+
+ test_data = (
+ # Valid
+
+ # Components
+ (
+ "/VCALENDAR",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ ],
+ None,
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT"),
+ ],
+ None,
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234]",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234]"),
+ ],
+ None,
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234%2F4567]",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234/4567]"),
+ ],
+ None,
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]"),
+ ],
+ None,
+ None,
+ ),
+
+ # Properties
+ (
+ "/VCALENDAR#VERSION",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ ],
+ Path.PropertySegment("VERSION"),
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT#SUMMARY",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT"),
+ ],
+ Path.PropertySegment("SUMMARY"),
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT#SUMMARY[=abc]",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT"),
+ ],
+ Path.PropertySegment("SUMMARY[=abc]"),
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT#SUMMARY[=a%2Fc]",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT"),
+ ],
+ Path.PropertySegment("SUMMARY[=a/c]"),
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT#SUMMARY[!abc]",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT"),
+ ],
+ Path.PropertySegment("SUMMARY[!abc]"),
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234]#SUMMARY",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234]"),
+ ],
+ Path.PropertySegment("SUMMARY"),
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234]#SUMMARY[=abc]",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234]"),
+ ],
+ Path.PropertySegment("SUMMARY[=abc]"),
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]#SUMMARY",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]"),
+ ],
+ Path.PropertySegment("SUMMARY"),
+ None,
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]#SUMMARY[=abc]",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]"),
+ ],
+ Path.PropertySegment("SUMMARY[=abc]"),
+ None,
+ ),
+
+ # Parameters
+ (
+ "/VCALENDAR#VERSION;VALUE",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ ],
+ Path.PropertySegment("VERSION"),
+ Path.ParameterSegment("VALUE"),
+ ),
+ (
+ "/VCALENDAR/VEVENT#ATTENDEE;PARTSTAT",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT"),
+ ],
+ Path.PropertySegment("ATTENDEE"),
+ Path.ParameterSegment("PARTSTAT"),
+ ),
+ (
+ "/VCALENDAR/VEVENT#ATTENDEE[=abc];PARTSTAT",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT"),
+ ],
+ Path.PropertySegment("ATTENDEE[=abc]"),
+ Path.ParameterSegment("PARTSTAT"),
+ ),
+ (
+ "/VCALENDAR/VEVENT#ATTENDEE[=a%2Fc];PARTSTAT",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT"),
+ ],
+ Path.PropertySegment("ATTENDEE[=a/c]"),
+ Path.ParameterSegment("PARTSTAT"),
+ ),
+ (
+ "/VCALENDAR/VEVENT#ATTENDEE[!abc];PARTSTAT",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT"),
+ ],
+ Path.PropertySegment("ATTENDEE[!abc]"),
+ Path.ParameterSegment("PARTSTAT"),
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234]#ATTENDEE;PARTSTAT",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234]"),
+ ],
+ Path.PropertySegment("ATTENDEE"),
+ Path.ParameterSegment("PARTSTAT"),
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234]#ATTENDEE[=abc];PARTSTAT",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234]"),
+ ],
+ Path.PropertySegment("ATTENDEE[=abc]"),
+ Path.ParameterSegment("PARTSTAT"),
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]#ATTENDEE;PARTSTAT",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]"),
+ ],
+ Path.PropertySegment("ATTENDEE"),
+ Path.ParameterSegment("PARTSTAT"),
+ ),
+ (
+ "/VCALENDAR/VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]#ATTENDEE[=abc];PARTSTAT",
+ True,
+ [
+ Path.ComponentSegment("VCALENDAR"),
+ Path.ComponentSegment("VEVENT[UID=1234][RECURRENCE-ID=20150907T120000Z]"),
+ ],
+ Path.PropertySegment("ATTENDEE[=abc]"),
+ Path.ParameterSegment("PARTSTAT"),
+ ),
+
+ # Invalid
+ )
+
+ def testParse(self):
+
+ for strpath, valid, components, property, parameter in TestPath.test_data:
+ try:
+ path = Path(strpath)
+ except ValueError:
+ self.assertFalse(valid)
+ else:
+ self.assertTrue(valid)
+ self.assertEqual(path.components, components)
+ self.assertEqual(path.property, property)
+ self.assertEqual(path.parameter, parameter)
+
+
+ def testType(self):
+
+ data = [
+ ("/VCALENDAR", True, False, False, False, False,),
+ ("/VCALENDAR/VEVENT", True, False, False, False, False,),
+ ("/VCALENDAR/VEVENT#SUMMARY", False, True, False, False, False,),
+ ("/VCALENDAR/VEVENT#", False, False, True, False, False,),
+ ("/VCALENDAR/VEVENT#SUMMARY;X-PARAM", False, False, False, True, False,),
+ ("/VCALENDAR/VEVENT#SUMMARY;", False, False, False, False, True,),
+ ]
+
+ for strpath, isComponent, isProperty, isPropertyNoName, isParameter, isParameterNoName in data:
+ path = Path(strpath)
+ self.assertEqual(path.targetComponent(), isComponent)
+ self.assertEqual(path.targetProperty(), isProperty)
+ self.assertEqual(path.targetPropertyNoName(), isPropertyNoName)
+ self.assertEqual(path.targetParameter(), isParameter)
+ self.assertEqual(path.targetParameterNoName(), isParameterNoName)
+
+
+ def testMatch_Components_Simple(self):
+
+ icalendar = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+"""
+
+ calendar = Calendar.parseText(icalendar.replace("\n", "\r\n"))
+ path = Path("/VCALENDAR")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], calendar)
+
+ path = Path("/VCALENDAR/VEVENT")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], calendar.getComponents("VEVENT")[0])
+
+ path = Path("/VCALENDAR/VEVENT[UID=123]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], calendar.getComponents("VEVENT")[0])
+
+ path = Path("/VCALENDAR/VEVENT[UID=123][RECURRENCE-ID=20150101T000000Z]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252][RECURRENCE-ID=]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], calendar.getComponents("VEVENT")[0])
+
+ path = Path("/VCALENDAR/VEVENT[UID=123][RECURRENCE-ID=]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252][RECURRENCE-ID=20020101]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+
+ path = Path("/VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252][RECURRENCE-ID=]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], calendar.getComponents("VEVENT")[0])
+
+
+ def testMatch_Components_Multiple(self):
+
+ icalendar = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:New Year's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:165EF135-BA92-435A-88C9-562F95030908
+DTSTART;VALUE=DATE:20020401
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:April Fool's Day
+END:VEVENT
+BEGIN:VEVENT
+UID:5EA5AF47-77F5-4EEE-9944-69651C97755B
+DTSTART;VALUE=DATE:20020921
+DURATION:P1D
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY
+SUMMARY:Birthday
+END:VEVENT
+END:VCALENDAR
+"""
+
+ calendar = Calendar.parseText(icalendar.replace("\n", "\r\n"))
+ components_by_uid = dict([(component.getUID(), component) for component in calendar.getComponents()])
+
+ path = Path("/VCALENDAR")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], calendar)
+
+ path = Path("/VCALENDAR/VEVENT")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 3)
+ self.assertEqual(
+ set([item.getUID() for item in matched]),
+ set(components_by_uid.keys()),
+ )
+
+ path = Path("/VCALENDAR/VEVENT[UID=123]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ for key in components_by_uid.keys():
+ path = Path("/VCALENDAR/VEVENT[UID={key}]".format(key=key))
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], components_by_uid[key])
+
+ path = Path("/VCALENDAR/VEVENT[UID=123][RECURRENCE-ID=20150101T000000Z]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252][RECURRENCE-ID=]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ components_by_uid["C3184A66-1ED0-11D9-A5E0-000A958A3252"],
+ )
+
+ for key in components_by_uid.keys():
+ path = Path("/VCALENDAR/VEVENT[UID={key}][RECURRENCE-ID=20150101T000000Z]".format(key=key))
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+
+ for key in components_by_uid.keys():
+ path = Path("/VCALENDAR/VEVENT[UID={key}][RECURRENCE-ID=]".format(key=key))
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], components_by_uid[key])
+
+
+ def testMatch_Components_Recurring(self):
+
+ icalendar = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART:20020101T120000Z
+DURATION:PT1H
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=DAILY
+SUMMARY:Meeting
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID:20020102T120000Z
+DTSTART:20020102T130000Z
+DURATION:PT1H
+DTSTAMP:20020101T000000Z
+SUMMARY:Meeting #2
+END:VEVENT
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+RECURRENCE-ID:20020103T120000Z
+DTSTART:20020103T140000Z
+DURATION:PT1H
+DTSTAMP:20020101T000000Z
+SUMMARY:Meeting #3
+END:VEVENT
+END:VCALENDAR
+"""
+
+ calendar = Calendar.parseText(icalendar.replace("\n", "\r\n"))
+ components_by_rid = dict([(component.getRecurrenceID(), component) for component in calendar.getComponents()])
+
+ path = Path("/VCALENDAR")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], calendar)
+
+ path = Path("/VCALENDAR/VEVENT")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 3)
+ self.assertEqual(
+ set([item.getRecurrenceID() for item in matched]),
+ set(components_by_rid.keys()),
+ )
+
+ path = Path("/VCALENDAR/VEVENT[UID=123][RECURRENCE-ID=20150101T000000Z]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ for key in components_by_rid.keys():
+ path = Path("/VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252][RECURRENCE-ID={key}]".format(key=key if key else ""))
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], components_by_rid[key])
+
+ path = Path("/VCALENDAR/VEVENT[UID=C3184A66-1ED0-11D9-A5E0-000A958A3252][RECURRENCE-ID=]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertIs(matched[0], components_by_rid[None])
+
+
+ def testMatch_Properties_Simple(self):
+
+ icalendar = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+"""
+
+ calendar = Calendar.parseText(icalendar.replace("\n", "\r\n"))
+ path = Path("/VCALENDAR#VERSION")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar, calendar.getProperties("VERSION")[0],),
+ )
+
+ path = Path("/VCALENDAR#")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar, None,),
+ )
+
+ path = Path("/VCALENDAR#FOOBAR")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT#SUMMARY")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar.getComponents()[0], calendar.getComponents()[0].getProperties("SUMMARY")[0],),
+ )
+
+ path = Path("/VCALENDAR/VEVENT#")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar.getComponents()[0], None,),
+ )
+
+ # Non-existent - for_update changes behavior
+ path = Path("/VCALENDAR/VEVENT#FOOBAR")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT#FOOBAR")
+ matched = path.match(calendar, for_update=True)
+ self.assertEqual(len(matched), 1)
+
+ path = Path("/VCALENDAR/VEVENT#SUMMARY[=New Year's Day]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar.getComponents()[0], calendar.getComponents()[0].getProperties("SUMMARY")[0],),
+ )
+
+ # Non-existent - for_update does not change behavior
+ path = Path("/VCALENDAR/VEVENT#SUMMARY[=New Years Day]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT#SUMMARY[=New Years Day]")
+ matched = path.match(calendar, for_update=True)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT#DTSTART[=20020101]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar.getComponents()[0], calendar.getComponents()[0].getProperties("DTSTART")[0],),
+ )
+
+ path = Path("/VCALENDAR/VEVENT#RRULE[=20020101]")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+
+ def testMatch_Parameters_Simple(self):
+
+ icalendar = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//mulberrymail.com//Mulberry v4.0//EN
+BEGIN:VEVENT
+UID:C3184A66-1ED0-11D9-A5E0-000A958A3252
+DTSTART;VALUE=DATE:20020101
+DTEND;VALUE=DATE:20020102
+DTSTAMP:20020101T000000Z
+RRULE:FREQ=YEARLY;UNTIL=20031231;BYMONTH=1
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+"""
+
+ calendar = Calendar.parseText(icalendar.replace("\n", "\r\n"))
+
+ path = Path("/VCALENDAR/VEVENT#SUMMARY;X-PARAM")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar.getComponents()[0], calendar.getComponents()[0].getProperties("SUMMARY")[0], "X-PARAM",)
+ )
+
+ path = Path("/VCALENDAR/VEVENT#SUMMARY;")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar.getComponents()[0], calendar.getComponents()[0].getProperties("SUMMARY")[0], None,)
+ )
+
+ path = Path("/VCALENDAR/VEVENT#FOOBAR;X-PARAM")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT#SUMMARY[=New Year's Day];X-PARAM")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar.getComponents()[0], calendar.getComponents()[0].getProperties("SUMMARY")[0], "X-PARAM",)
+ )
+
+ path = Path("/VCALENDAR/VEVENT#SUMMARY[=New Years Day];X-PARAM")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 0)
+
+ path = Path("/VCALENDAR/VEVENT#DTSTART[=20020101];VALUE")
+ matched = path.match(calendar)
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(
+ matched[0],
+ (calendar.getComponents()[0], calendar.getComponents()[0].getProperties("DTSTART")[0], "VALUE",)
+ )
+
+
+
+class TestComponentSegment(unittest.TestCase):
+
+ test_data = (
+ # Valid
+ ("VCALENDAR", True, "VCALENDAR", None, None, None,),
+ ("VCALENDAR[UID=1234]", True, "VCALENDAR", "1234", None, None,),
+ ("VCALENDAR[UID=1234%2F4567]", True, "VCALENDAR", "1234/4567", None, None,),
+ ("VCALENDAR[UID=1234][RECURRENCE-ID=]", True, "VCALENDAR", "1234", True, None,),
+ ("VCALENDAR[UID=1234][RECURRENCE-ID=20150907T120000Z]", True, "VCALENDAR", "1234", True, "20150907T120000Z",),
+
+ # Invalid
+ ("VCALENDAR[]", False, None, None, None, None,),
+ ("VCALENDAR[foo]", False, None, None, None, None,),
+ ("VCALENDAR[foo=bar]", False, None, None, None, None,),
+ ("VCALENDAR[UID=", False, None, None, None, None,),
+ ("VCALENDAR[UID=1234][]", False, None, None, None, None,),
+ ("VCALENDAR[UID=1234][foo=bar]", False, None, None, None, None,),
+ ("VCALENDAR[UID=1234][RECURRENCE-ID=", False, None, None, None, None,),
+ )
+
+ def testParse(self):
+
+ for segment, valid, name, uid, rid, rid_value in TestComponentSegment.test_data:
+ try:
+ component = Path.ComponentSegment(segment)
+ except ValueError:
+ self.assertFalse(valid)
+ else:
+ self.assertTrue(valid)
+ self.assertEqual(component.name, name)
+ self.assertEqual(component.uid, uid)
+ self.assertEqual(component.rid, rid)
+ self.assertEqual(component.rid_value, DateTime.parseText(rid_value) if rid_value else None)
+
+
+
+class TestPropertySegment(unittest.TestCase):
+
+ test_data = (
+ # Valid
+ ("STATUS", True, "STATUS", None,),
+ ("STATUS[=COMPLETED]", True, "STATUS", ("COMPLETED", operator.eq,),),
+ ("STATUS[!COMPLETED]", True, "STATUS", ("COMPLETED", operator.ne,),),
+ ("SUMMARY[=a%2Fb]", True, "SUMMARY", ("a/b", operator.eq,),),
+ ("", True, "", None,),
+
+ # Invalid
+ ("STATUS[]", False, None, None,),
+ ("STATUS[foo]", False, None, None,),
+ ("STATUS[=]", False, None, None,),
+ ("STATUS[=COMPLETED", False, None, None,),
+ ("STATUS[=COMPLETED][=FAILED]", False, None, None,),
+ )
+
+ def testParse(self):
+
+ for segment, valid, name, matchCondition in TestPropertySegment.test_data:
+ try:
+ property = Path.PropertySegment(segment)
+ except ValueError:
+ self.assertFalse(valid)
+ else:
+ self.assertTrue(valid)
+ self.assertEqual(property.name, name)
+ self.assertEqual(property.matchCondition, matchCondition)
+
+
+
+class TestParameterSegment(unittest.TestCase):
+
+ test_data = (
+ # Valid
+ ("PARTSTAT", True, "PARTSTAT",),
+ ("", True, "",),
+
+ # Invalid
+ ("PARTSTAT[]", False, None,),
+ ("PARTSTAT[", False, None,),
+ ("PARTSTAT[=NEEDS-ACTION]", False, None,),
+ )
+
+ def testParse(self):
+
+ for segment, valid, name in TestParameterSegment.test_data:
+ try:
+ property = Path.ParameterSegment(segment)
+ except ValueError:
+ self.assertFalse(valid)
+ else:
+ self.assertTrue(valid)
+ self.assertEqual(property.name, name)
</ins></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendaricalendarteststest_recurrencepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_recurrence.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_recurrence.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_recurrence.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -208,7 +208,7 @@
</span><span class="cx"> self.assertEqual(
</span><span class="cx"> items,
</span><span class="cx"> results,
</span><del>- msg="Failed rule: #{} {}".format(ctr + 1, i["rule"])
</del><ins>+ msg="Failed rule: #{} {} {}".format(ctr + 1, i["rule"], items)
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx">
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarintegervaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/integervalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/integervalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/integervalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -40,11 +40,15 @@
</span><span class="cx"> # os - StringIO object
</span><span class="cx"> def generate(self, os):
</span><span class="cx"> try:
</span><del>- os.write(str(self.mValue))
</del><ins>+ os.write(self.getTextValue())
</ins><span class="cx"> except:
</span><span class="cx"> pass
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def getTextValue(self):
+ return str(self.mValue)
+
+
</ins><span class="cx"> def writeXML(self, node, namespace):
</span><span class="cx"> value = self.getXMLNode(node, namespace)
</span><span class="cx"> value.text = str(self.mValue)
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarmultivaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/multivalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/multivalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/multivalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -78,17 +78,15 @@
</span><span class="cx">
</span><span class="cx"> def generate(self, os):
</span><span class="cx"> try:
</span><del>- first = True
- for iter in self.mValues:
- if first:
- first = False
- else:
- os.write(",")
- iter.generate(os)
</del><ins>+ os.write(self.getTextValue())
</ins><span class="cx"> except:
</span><span class="cx"> pass
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def getTextValue(self):
+ return ",".join([value.getText() for value in self.mValues])
+
+
</ins><span class="cx"> def writeXML(self, node, namespace):
</span><span class="cx"> for iter in self.mValues:
</span><span class="cx"> iter.writeXML(node, namespace)
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarparserpy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/parser.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/parser.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/parser.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -79,4 +79,3 @@
</span><span class="cx"> ParserContext.INVALID_REQUEST_STATUS_VALUE = ParserContext.PARSER_RAISE
</span><span class="cx"> ParserContext.BACKSLASH_IN_URI_VALUE = ParserContext.PARSER_RAISE
</span><span class="cx"> ParserContext.BACKSLASH_IN_GEO_VALUE = ParserContext.PARSER_RAISE
</span><del>- ParserContext.INVALID_REQUEST_STATUS = ParserContext.PARSER_RAISE
</del></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarperiodpy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/period.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/period.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/period.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -104,16 +104,18 @@
</span><span class="cx">
</span><span class="cx"> def generate(self, os):
</span><span class="cx"> try:
</span><del>- self.mStart.generate(os)
- os.write("/")
- if self.mUseDuration:
- self.mDuration.generate(os)
- else:
- self.mEnd.generate(os)
</del><ins>+ os.write(self.getText())
</ins><span class="cx"> except:
</span><span class="cx"> pass
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def getText(self):
+ return "{}/{}".format(
+ self.mStart.getText(),
+ self.mDuration.getText() if self.mUseDuration else self.mEnd.getText(),
+ )
+
+
</ins><span class="cx"> def writeXML(self, node, namespace):
</span><span class="cx"> start = XML.SubElement(node, xmlutils.makeTag(namespace, xmldefinitions.period_start))
</span><span class="cx"> start.text = self.mStart.getXMLText()
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarplaintextvaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/plaintextvalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/plaintextvalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/plaintextvalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -42,6 +42,10 @@
</span><span class="cx"> pass
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def getTextValue(self):
+ return self.mValue
+
+
</ins><span class="cx"> def writeXML(self, node, namespace):
</span><span class="cx"> value = self.getXMLNode(node, namespace)
</span><span class="cx"> value.text = self.mValue
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendartextvaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/textvalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/textvalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/textvalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -34,9 +34,14 @@
</span><span class="cx"> # os - StringIO object
</span><span class="cx"> def generate(self, os):
</span><span class="cx"> try:
</span><del>- # Encoding required
- utils.writeTextValue(os, self.mValue)
</del><ins>+ os.write(self.getTextValue())
</ins><span class="cx"> except:
</span><span class="cx"> pass
</span><span class="cx">
</span><ins>+
+ def getTextValue(self):
+ # Encoding required
+ return utils.getTextValue(self.mValue)
+
+
</ins><span class="cx"> Value.registerType(Value.VALUETYPE_TEXT, TextValue, xmldefinitions.value_text)
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarurivaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/urivalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/urivalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/urivalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -43,13 +43,18 @@
</span><span class="cx"> Handle a client bug where it sometimes includes a \n in the value and we need
</span><span class="cx"> to make sure that gets encoded rather than included literally which would break syntax.
</span><span class="cx"> """
</span><ins>+ try:
+ os.write(self.getTextValue())
+ except:
+ pass
+
+
+ def getTextValue(self):
</ins><span class="cx"> if '\n' in self.mValue:
</span><del>- try:
- # No encoding required
- os.write(self.mValue.replace("\n", "\\n"))
- except:
- pass
</del><ins>+ # No encoding required
+ return self.mValue.replace("\n", "\\n")
</ins><span class="cx"> else:
</span><del>- super(URIValue, self).generate(os)
</del><ins>+ return super(URIValue, self).getTextValue()
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> Value.registerType(Value.VALUETYPE_URI, URIValue, xmldefinitions.value_uri)
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarutcoffsetvaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/utcoffsetvalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/utcoffsetvalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/utcoffsetvalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -70,26 +70,29 @@
</span><span class="cx"> # os - StringIO object
</span><span class="cx"> def generate(self, os, fullISO=False):
</span><span class="cx"> try:
</span><del>- abs_value = self.mValue
- if abs_value < 0 :
- sign = "-"
- abs_value = -self.mValue
- else:
- sign = "+"
</del><ins>+ os.write(self.getTextValue(fullISO))
+ except:
+ pass
</ins><span class="cx">
</span><del>- secs = abs_value % 60
- mins = (abs_value / 60) % 60
- hours = abs_value / (60 * 60)
</del><span class="cx">
</span><del>- s = ("%s%02d:%02d" if fullISO else "%s%02d%02d") % (sign, hours, mins,)
- if (secs != 0):
- s = ("%s:%02d" if fullISO else "%s%02d") % (s, secs,)
</del><ins>+ def getTextValue(self, fullISO=False):
+ abs_value = self.mValue
+ if abs_value < 0 :
+ sign = "-"
+ abs_value = -self.mValue
+ else:
+ sign = "+"
</ins><span class="cx">
</span><del>- os.write(s)
- except:
- pass
</del><ins>+ secs = abs_value % 60
+ mins = (abs_value / 60) % 60
+ hours = abs_value / (60 * 60)
</ins><span class="cx">
</span><ins>+ s = ("%s%02d:%02d" if fullISO else "%s%02d%02d") % (sign, hours, mins,)
+ if (secs != 0):
+ s = ("%s:%02d" if fullISO else "%s%02d") % (s, secs,)
+ return s
</ins><span class="cx">
</span><ins>+
</ins><span class="cx"> def writeXML(self, node, namespace):
</span><span class="cx">
</span><span class="cx"> os = StringIO()
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarutilspy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/utils.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/utils.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/utils.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -89,51 +89,43 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><del>-def escapeTextValue(value):
- os = StringIO.StringIO()
- writeTextValue(os, value)
- return os.getvalue()
</del><ins>+def getTextValue(value):
+ result = []
+ start_pos = 0
+ end_pos = find_first_of(value, "\r\n;\\,", start_pos)
+ if end_pos != -1:
+ while True:
+ # Write current segment
+ result.append(value[start_pos:end_pos])
</ins><span class="cx">
</span><ins>+ # Write escape
+ result.append("\\")
+ c = value[end_pos]
+ if c == '\r':
+ result.append("r")
+ elif c == '\n':
+ result.append("n")
+ elif c == ';':
+ result.append(";")
+ elif c == '\\':
+ result.append("\\")
+ elif c == ',':
+ result.append(",")
</ins><span class="cx">
</span><ins>+ # Bump past escapee and look for next segment
+ start_pos = end_pos + 1
</ins><span class="cx">
</span><del>-def writeTextValue(os, value):
- try:
- start_pos = 0
- end_pos = find_first_of(value, "\r\n;\\,", start_pos)
- if end_pos != -1:
- while True:
- # Write current segment
- os.write(value[start_pos:end_pos])
</del><ins>+ end_pos = find_first_of(value, "\r\n;\\,", start_pos)
+ if end_pos == -1:
+ result.append(value[start_pos:])
+ break
+ else:
+ result.append(value)
</ins><span class="cx">
</span><del>- # Write escape
- os.write("\\")
- c = value[end_pos]
- if c == '\r':
- os.write("r")
- elif c == '\n':
- os.write("n")
- elif c == ';':
- os.write(";")
- elif c == '\\':
- os.write("\\")
- elif c == ',':
- os.write(",")
</del><ins>+ return "".join(result)
</ins><span class="cx">
</span><del>- # Bump past escapee and look for next segment
- start_pos = end_pos + 1
</del><span class="cx">
</span><del>- end_pos = find_first_of(value, "\r\n;\\,", start_pos)
- if end_pos == -1:
- os.write(value[start_pos:])
- break
- else:
- os.write(value)
</del><span class="cx">
</span><del>- except:
- pass
-
-
-
</del><span class="cx"> def decodeTextValue(value):
</span><span class="cx"> os = StringIO.StringIO()
</span><span class="cx">
</span><span class="lines">@@ -287,17 +279,15 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><del>-def generateTextList(os, data, sep=';'):
</del><ins>+def getTextList(data, sep=';'):
</ins><span class="cx"> """
</span><span class="cx"> Each element of the list must be separately escaped
</span><span class="cx"> """
</span><del>- try:
- if isinstance(data, basestring):
- data = (data,)
- results = [escapeTextValue(value) for value in data]
- os.write(sep.join(results))
- except:
- pass
</del><ins>+ if isinstance(data, basestring):
+ data = (data,)
+ elif data is None:
+ data = ("",)
+ return sep.join([getTextValue(value) for value in data])
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -343,26 +333,10 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><del>-def generateDoubleNestedList(os, data):
- try:
- def _writeElement(item):
- if isinstance(item, basestring):
- writeTextValue(os, item)
- else:
- if item:
- writeTextValue(os, item[0])
- for bit in item[1:]:
- os.write(",")
- writeTextValue(os, bit)
</del><ins>+def getDoubleNestedList(data):
+ return ";".join([getTextList(item, ",") for item in data])
</ins><span class="cx">
</span><del>- for item in data[:-1]:
- _writeElement(item)
- os.write(";")
- _writeElement(data[-1])
</del><span class="cx">
</span><del>- except:
- pass
-
</del><span class="cx"> # Date/time calcs
</span><span class="cx"> days_in_month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
</span><span class="cx"> days_in_month_leap = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarvaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/value.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/value.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/value.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -96,6 +96,10 @@
</span><span class="cx"> raise NotImplementedError
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def getTextValue(self):
+ raise NotImplementedError
+
+
</ins><span class="cx"> def setValue(self, value):
</span><span class="cx"> raise NotImplementedError
</span><span class="cx">
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarvalueutilspy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/valueutils.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/valueutils.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/valueutils.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -106,5 +106,9 @@
</span><span class="cx"> return self.mValue
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def getTextValue(self):
+ return self.mValue.getText()
+
+
</ins><span class="cx"> def setValue(self, value):
</span><span class="cx"> self.mValue = value
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarvcardadrpy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/vcard/adr.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/vcard/adr.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/vcard/adr.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -116,9 +116,16 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> def generate(self, os):
</span><del>- utils.generateDoubleNestedList(os, self.mValue)
</del><ins>+ try:
+ os.write(self.getText())
+ except:
+ pass
</ins><span class="cx">
</span><span class="cx">
</span><ins>+ def getText(self):
+ return utils.getDoubleNestedList(self.mValue)
+
+
</ins><span class="cx"> def parseJSON(self, jobject):
</span><span class="cx"> self.mValue = tuple(map(lambda x: x.encode("utf-8"), jobject))
</span><span class="cx">
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarvcardnpy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/vcard/n.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/vcard/n.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/vcard/n.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -101,7 +101,7 @@
</span><span class="cx">
</span><span class="cx"> results = []
</span><span class="cx"> for i in (N.PREFIX, N.FIRST, N.MIDDLE, N.LAST, N.SUFFIX):
</span><del>- result = _stringOrList(self.mValue[i])
</del><ins>+ result = _stringOrList(self.mValue[i]) if self.mValue[i] else ""
</ins><span class="cx"> if result:
</span><span class="cx"> results.append(result)
</span><span class="cx">
</span><span class="lines">@@ -113,9 +113,16 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> def generate(self, os):
</span><del>- utils.generateDoubleNestedList(os, self.mValue)
</del><ins>+ try:
+ os.write(self.getText())
+ except:
+ pass
</ins><span class="cx">
</span><span class="cx">
</span><ins>+ def getText(self):
+ return utils.getDoubleNestedList(self.mValue)
+
+
</ins><span class="cx"> def parseJSON(self, jobject):
</span><span class="cx"> self.mValue = tuple(map(lambda x: x.encode("utf-8"), jobject))
</span><span class="cx">
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarvcardorgvaluepy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/vcard/orgvalue.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/vcard/orgvalue.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/vcard/orgvalue.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -41,9 +41,16 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> def generate(self, os):
</span><del>- utils.generateTextList(os, self.mValue, ';')
</del><ins>+ try:
+ os.write(self.getTextValue())
+ except:
+ pass
</ins><span class="cx">
</span><span class="cx">
</span><ins>+ def getTextValue(self):
+ return utils.getTextList(self.mValue, ';')
+
+
</ins><span class="cx"> def parseJSONValue(self, jobject):
</span><span class="cx"> self.mValue = tuple(map(lambda x: x.encode("utf-8"), jobject))
</span><span class="cx">
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarvcardteststest_adrpy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_adr.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_adr.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_adr.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -29,6 +29,10 @@
</span><span class="cx"> (("pobox",), ("extended",), ("street1", "street2",), "locality", "region", (), "country"),
</span><span class="cx"> "pobox;extended;street1,street2;locality;region;;country",
</span><span class="cx"> ),
</span><ins>+ (
+ ("", None, ("street1", "street2",), "locality", "region", (), "country"),
+ ";;street1,street2;locality;region;;country",
+ ),
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx"> for args, result in data:
</span></span></pre></div>
<a id="PyCalendarbranchespatchsrcpycalendarvcardteststest_npy"></a>
<div class="modfile"><h4>Modified: PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_n.py (15376 => 15377)</h4>
<pre class="diff"><span>
<span class="info">--- PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_n.py        2015-12-10 21:47:23 UTC (rev 15376)
+++ PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_n.py        2015-12-10 22:01:17 UTC (rev 15377)
</span><span class="lines">@@ -32,6 +32,11 @@
</span><span class="cx"> "last;first;middle1,middle2;;suffix",
</span><span class="cx"> "first middle1 middle2 last suffix",
</span><span class="cx"> ),
</span><ins>+ (
+ ("last", ("first",), "", None, ("suffix",)),
+ "last;first;;;suffix",
+ "first last suffix",
+ ),
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx"> for args, result, fullName in data:
</span></span></pre>
</div>
</div>
</body>
</html>