[CalendarServer-changes] [15377] PyCalendar/branches/patch/src/pycalendar

source_changes at macosforge.org source_changes at macosforge.org
Thu Dec 10 14:01:17 PST 2015


Revision: 15377
          http://trac.calendarserver.org//changeset/15377
Author:   cdaboo at apple.com
Date:     2015-12-10 14:01:17 -0800 (Thu, 10 Dec 2015)
Log Message:
-----------
Initial support for icalendar-patch.

Modified Paths:
--------------
    PyCalendar/branches/patch/src/pycalendar/componentbase.py
    PyCalendar/branches/patch/src/pycalendar/duration.py
    PyCalendar/branches/patch/src/pycalendar/floatvalue.py
    PyCalendar/branches/patch/src/pycalendar/geovalue.py
    PyCalendar/branches/patch/src/pycalendar/icalendar/calendar.py
    PyCalendar/branches/patch/src/pycalendar/icalendar/componentrecur.py
    PyCalendar/branches/patch/src/pycalendar/icalendar/recurrence.py
    PyCalendar/branches/patch/src/pycalendar/icalendar/requeststatusvalue.py
    PyCalendar/branches/patch/src/pycalendar/icalendar/tests/rrule_examples.json
    PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_recurrence.py
    PyCalendar/branches/patch/src/pycalendar/integervalue.py
    PyCalendar/branches/patch/src/pycalendar/multivalue.py
    PyCalendar/branches/patch/src/pycalendar/parser.py
    PyCalendar/branches/patch/src/pycalendar/period.py
    PyCalendar/branches/patch/src/pycalendar/plaintextvalue.py
    PyCalendar/branches/patch/src/pycalendar/textvalue.py
    PyCalendar/branches/patch/src/pycalendar/urivalue.py
    PyCalendar/branches/patch/src/pycalendar/utcoffsetvalue.py
    PyCalendar/branches/patch/src/pycalendar/utils.py
    PyCalendar/branches/patch/src/pycalendar/value.py
    PyCalendar/branches/patch/src/pycalendar/valueutils.py
    PyCalendar/branches/patch/src/pycalendar/vcard/adr.py
    PyCalendar/branches/patch/src/pycalendar/vcard/n.py
    PyCalendar/branches/patch/src/pycalendar/vcard/orgvalue.py
    PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_adr.py
    PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_n.py

Added Paths:
-----------
    PyCalendar/branches/patch/src/pycalendar/icalendar/patch.py
    PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_patch.py

Modified: PyCalendar/branches/patch/src/pycalendar/componentbase.py
===================================================================
--- 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)
@@ -153,6 +153,14 @@
         self.mComponents.remove(component)
 
 
+    def removeFromParent(self):
+        """
+        Remove this L{ComponentBase} from its parent
+        """
+        if self.mParentComponent is not None:
+            self.mParentComponent.removeComponent(self)
+
+
     def removeAllComponent(self, compname=None):
         if compname:
             compname = compname.upper()

Modified: PyCalendar/branches/patch/src/pycalendar/duration.py
===================================================================
--- 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)
@@ -241,33 +241,40 @@
 
     def generate(self, os):
         try:
-            if not self.mForward and (self.mWeeks or self.mDays or self.mHours or self.mMinutes or self.mSeconds):
-                os.write("-")
-            os.write("P")
+            os.write(self.getText())
+        except:
+            pass
 
-            if self.mWeeks != 0:
-                os.write("%dW" % (self.mWeeks,))
-            else:
-                if self.mDays != 0:
-                    os.write("%dD" % (self.mDays,))
 
-                if (self.mHours != 0) or (self.mMinutes != 0) or (self.mSeconds != 0):
-                    os.write("T")
+    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")
 
-                    if self.mHours != 0:
-                        os.write("%dH" % (self.mHours,))
+        if self.mWeeks != 0:
+            result.append("%dW" % (self.mWeeks,))
+        else:
+            if self.mDays != 0:
+                result.append("%dD" % (self.mDays,))
 
-                    if (self.mMinutes != 0) or ((self.mHours != 0) and (self.mSeconds != 0)):
-                        os.write("%dM" % (self.mMinutes,))
+            if (self.mHours != 0) or (self.mMinutes != 0) or (self.mSeconds != 0):
+                result.append("T")
 
-                    if self.mSeconds != 0:
-                        os.write("%dS" % (self.mSeconds,))
-                elif self.mDays == 0:
-                    os.write("T0S")
-        except:
-            pass
+                if self.mHours != 0:
+                    result.append("%dH" % (self.mHours,))
 
+                if (self.mMinutes != 0) or ((self.mHours != 0) and (self.mSeconds != 0)):
+                    result.append("%dM" % (self.mMinutes,))
 
+                if self.mSeconds != 0:
+                    result.append("%dS" % (self.mSeconds,))
+            elif self.mDays == 0:
+                result.append("T0S")
+
+        return "".join(result)
+
+
     def writeXML(self, node, namespace):
         node.text = self.getText()
 

Modified: PyCalendar/branches/patch/src/pycalendar/floatvalue.py
===================================================================
--- 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)
@@ -45,6 +45,10 @@
             pass
 
 
+    def getTextValue(self):
+        return str(self.mValue)
+
+
     def writeXML(self, node, namespace):
         value = self.getXMLNode(node, namespace)
         value.text = str(self.mValue)

Modified: PyCalendar/branches/patch/src/pycalendar/geovalue.py
===================================================================
--- 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)
@@ -63,9 +63,13 @@
 
     # os - StringIO object
     def generate(self, os):
-        os.write("%s;%s" % (self.mValue[0], self.mValue[1],))
+        os.write(self.getTextValue())
 
 
+    def getTextValue(self):
+        return "%s;%s" % (self.mValue[0], self.mValue[1],)
+
+
     def writeXML(self, node, namespace):
         value = self.getXMLNode(node, namespace)
 

Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/calendar.py
===================================================================
--- 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)
@@ -365,49 +365,10 @@
         master = self.masterComponent()
         if master is None:
             return None
-
-        # 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())
         else:
-            newdtstartValue.setDateOnly(True)
+            return master.deriveComponent(recurrenceID)
 
-        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
-
-
     def masterComponent(self):
         """
         Return the first sub-component of a recurring type that represents the master

Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/componentrecur.py
===================================================================
--- 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)
@@ -719,5 +719,60 @@
                 self.mEnd = self.mStart + temp
 
 
+    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
+
+
     def createExpanded(self, master, recurid):
         return ComponentExpanded(master, recurid)

Added: PyCalendar/branches/patch/src/pycalendar/icalendar/patch.py
===================================================================
--- 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)
@@ -0,0 +1,714 @@
+##
+#    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

Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/recurrence.py
===================================================================
--- 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)
@@ -543,134 +543,134 @@
 
     def generate(self, os):
         try:
-            os.write(definitions.cICalValue_RECUR_FREQ)
-            os.write("=")
+            os.write(self.getText())
+        except:
+            pass
 
-            if self.mFreq == definitions.eRecurrence_SECONDLY:
-                os.write(definitions.cICalValue_RECUR_SECONDLY)
 
-            elif self.mFreq == definitions.eRecurrence_MINUTELY:
-                os.write(definitions.cICalValue_RECUR_MINUTELY)
+    def getText(self):
+        result = []
+        result.append(definitions.cICalValue_RECUR_FREQ)
+        result.append("=")
 
-            elif self.mFreq == definitions.eRecurrence_HOURLY:
-                os.write(definitions.cICalValue_RECUR_HOURLY)
+        if self.mFreq == definitions.eRecurrence_SECONDLY:
+            result.append(definitions.cICalValue_RECUR_SECONDLY)
 
-            elif self.mFreq == definitions.eRecurrence_DAILY:
-                os.write(definitions.cICalValue_RECUR_DAILY)
+        elif self.mFreq == definitions.eRecurrence_MINUTELY:
+            result.append(definitions.cICalValue_RECUR_MINUTELY)
 
-            elif self.mFreq == definitions.eRecurrence_WEEKLY:
-                os.write(definitions.cICalValue_RECUR_WEEKLY)
+        elif self.mFreq == definitions.eRecurrence_HOURLY:
+            result.append(definitions.cICalValue_RECUR_HOURLY)
 
-            elif self.mFreq == definitions.eRecurrence_MONTHLY:
-                os.write(definitions.cICalValue_RECUR_MONTHLY)
+        elif self.mFreq == definitions.eRecurrence_DAILY:
+            result.append(definitions.cICalValue_RECUR_DAILY)
 
-            elif self.mFreq == definitions.eRecurrence_YEARLY:
-                os.write(definitions.cICalValue_RECUR_YEARLY)
+        elif self.mFreq == definitions.eRecurrence_WEEKLY:
+            result.append(definitions.cICalValue_RECUR_WEEKLY)
 
-            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)
+        elif self.mFreq == definitions.eRecurrence_MONTHLY:
+            result.append(definitions.cICalValue_RECUR_MONTHLY)
 
-            if self.mInterval > 1:
-                os.write(";")
-                os.write(definitions.cICalValue_RECUR_INTERVAL)
-                os.write("=")
-                os.write(str(self.mInterval))
+        elif self.mFreq == definitions.eRecurrence_YEARLY:
+            result.append(definitions.cICalValue_RECUR_YEARLY)
 
-            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)
+        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())
 
-            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
+        if self.mInterval > 1:
+            result.append(";")
+            result.append(definitions.cICalValue_RECUR_INTERVAL)
+            result.append("=")
+            result.append(str(self.mInterval))
 
-                    if iter[0] != 0:
-                        os.write(str(iter[0]))
+        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))
 
-                    if iter[1] == definitions.eRecurrence_WEEKDAY_SU:
-                        os.write(definitions.cICalValue_RECUR_WEEKDAY_SU)
+        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
 
-                    elif iter[1] == definitions.eRecurrence_WEEKDAY_MO:
-                        os.write(definitions.cICalValue_RECUR_WEEKDAY_MO)
+                if iter[0] != 0:
+                    result.append(str(iter[0]))
 
-                    elif iter[1] == definitions.eRecurrence_WEEKDAY_TU:
-                        os.write(definitions.cICalValue_RECUR_WEEKDAY_TU)
+                if iter[1] == definitions.eRecurrence_WEEKDAY_SU:
+                    result.append(definitions.cICalValue_RECUR_WEEKDAY_SU)
 
-                    elif iter[1] == definitions.eRecurrence_WEEKDAY_WE:
-                        os.write(definitions.cICalValue_RECUR_WEEKDAY_WE)
+                elif iter[1] == definitions.eRecurrence_WEEKDAY_MO:
+                    result.append(definitions.cICalValue_RECUR_WEEKDAY_MO)
 
-                    elif iter[1] == definitions.eRecurrence_WEEKDAY_TH:
-                        os.write(definitions.cICalValue_RECUR_WEEKDAY_TH)
+                elif iter[1] == definitions.eRecurrence_WEEKDAY_TU:
+                    result.append(definitions.cICalValue_RECUR_WEEKDAY_TU)
 
-                    elif iter[1] == definitions.eRecurrence_WEEKDAY_FR:
-                        os.write(definitions.cICalValue_RECUR_WEEKDAY_FR)
+                elif iter[1] == definitions.eRecurrence_WEEKDAY_WE:
+                    result.append(definitions.cICalValue_RECUR_WEEKDAY_WE)
 
-                    elif iter[1] == definitions.eRecurrence_WEEKDAY_SA:
-                        os.write(definitions.cICalValue_RECUR_WEEKDAY_SA)
+                elif iter[1] == definitions.eRecurrence_WEEKDAY_TH:
+                    result.append(definitions.cICalValue_RECUR_WEEKDAY_TH)
 
-            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)
+                elif iter[1] == definitions.eRecurrence_WEEKDAY_FR:
+                    result.append(definitions.cICalValue_RECUR_WEEKDAY_FR)
 
-            # 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("=")
+                elif iter[1] == definitions.eRecurrence_WEEKDAY_SA:
+                    result.append(definitions.cICalValue_RECUR_WEEKDAY_SA)
 
-                if self.mWeekstart == definitions.eRecurrence_WEEKDAY_SU:
-                    os.write(definitions.cICalValue_RECUR_WEEKDAY_SU)
+        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))
 
-                elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_MO:
-                    os.write(definitions.cICalValue_RECUR_WEEKDAY_MO)
+        # 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("=")
 
-                elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_TU:
-                    os.write(definitions.cICalValue_RECUR_WEEKDAY_TU)
+            if self.mWeekstart == definitions.eRecurrence_WEEKDAY_SU:
+                result.append(definitions.cICalValue_RECUR_WEEKDAY_SU)
 
-                elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_WE:
-                    os.write(definitions.cICalValue_RECUR_WEEKDAY_WE)
+            elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_MO:
+                result.append(definitions.cICalValue_RECUR_WEEKDAY_MO)
 
-                elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_TH:
-                    os.write(definitions.cICalValue_RECUR_WEEKDAY_TH)
+            elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_TU:
+                result.append(definitions.cICalValue_RECUR_WEEKDAY_TU)
 
-                elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_FR:
-                    os.write(definitions.cICalValue_RECUR_WEEKDAY_FR)
+            elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_WE:
+                result.append(definitions.cICalValue_RECUR_WEEKDAY_WE)
 
-                elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_SA:
-                    os.write(definitions.cICalValue_RECUR_WEEKDAY_SA)
+            elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_TH:
+                result.append(definitions.cICalValue_RECUR_WEEKDAY_TH)
 
-        except:
-            pass
+            elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_FR:
+                result.append(definitions.cICalValue_RECUR_WEEKDAY_FR)
 
+            elif self.mWeekstart == definitions.eRecurrence_WEEKDAY_SA:
+                result.append(definitions.cICalValue_RECUR_WEEKDAY_SA)
 
-    def generateList(self, os, title, items):
+        return "".join(result)
 
+
+    def getList(self, title, items):
+
         if (items is not None) and (len(items) != 0):
-            os.write(";")
-            os.write(title)
-            os.write("=")
-            comma = False
-            for e in items:
-                if comma:
-                    os.write(",")
-                comma = True
-                os.write(str(e))
+            return ";{}={}".format(title, ",".join([str(item) for item in items]))
+        else:
+            return ""
 
 
     def writeXML(self, node, namespace):

Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/requeststatusvalue.py
===================================================================
--- 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)
@@ -79,9 +79,16 @@
 
     # os - StringIO object
     def generate(self, os):
-        utils.generateTextList(os, self.mValue if len(self.mValue) < 3 or self.mValue[2] else self.mValue[:2])
+        try:
+            os.write(self.getTextValue())
+        except:
+            pass
 
 
+    def getTextValue(self):
+        return utils.getTextList(self.mValue if len(self.mValue) < 3 or self.mValue[2] else self.mValue[:2])
+
+
     def writeXML(self, node, namespace):
         value = self.getXMLNode(node, namespace)
 

Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/tests/rrule_examples.json
===================================================================
--- 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)
@@ -15,6 +15,14 @@
 		]
 	},
 	{
+		"rule": "FREQ=YEARLY;BYWEEKNO=1",
+		"start": "20141130T000000",
+		"end": "20160101T000000",
+		"results": [
+            "20141229T000000"
+		]
+	},
+	{
 		"rule": "FREQ=MONTHLY",
 		"start": "20140140T120000",
 		"end": "20150101T000000",

Added: PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_patch.py
===================================================================
--- 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)
@@ -0,0 +1,2237 @@
+# -*- 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+                "patch": """delete /VCALENDAR/VEVENT[RECURRENCE-ID=20120101]#ATTENDEE[=mailto:user03 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE:mailto:user02 at example.com
+ATTENDEE:mailto:user03 at example.com
+ATTENDEE:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at 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 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+ATTENDEE:mailto:user03 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user04 at example.com
+DTSTAMP:20020101T000000Z
+ORGANIZER:mailto:user01 at example.com
+SUMMARY:New Year's Day
+END:VEVENT
+END:VCALENDAR
+""",
+                "patch": """delete /VCALENDAR/VEVENT[RECURRENCE-ID=20120101]#ATTENDEE[=mailto:user03 at 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)

Modified: PyCalendar/branches/patch/src/pycalendar/icalendar/tests/test_recurrence.py
===================================================================
--- 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)
@@ -208,7 +208,7 @@
             self.assertEqual(
                 items,
                 results,
-                msg="Failed rule: #{} {}".format(ctr + 1, i["rule"])
+                msg="Failed rule: #{} {} {}".format(ctr + 1, i["rule"], items)
             )
 
 

Modified: PyCalendar/branches/patch/src/pycalendar/integervalue.py
===================================================================
--- 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)
@@ -40,11 +40,15 @@
     # os - StringIO object
     def generate(self, os):
         try:
-            os.write(str(self.mValue))
+            os.write(self.getTextValue())
         except:
             pass
 
 
+    def getTextValue(self):
+        return str(self.mValue)
+
+
     def writeXML(self, node, namespace):
         value = self.getXMLNode(node, namespace)
         value.text = str(self.mValue)

Modified: PyCalendar/branches/patch/src/pycalendar/multivalue.py
===================================================================
--- 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)
@@ -78,17 +78,15 @@
 
     def generate(self, os):
         try:
-            first = True
-            for iter in self.mValues:
-                if first:
-                    first = False
-                else:
-                    os.write(",")
-                iter.generate(os)
+            os.write(self.getTextValue())
         except:
             pass
 
 
+    def getTextValue(self):
+        return ",".join([value.getText() for value in self.mValues])
+
+
     def writeXML(self, node, namespace):
         for iter in self.mValues:
             iter.writeXML(node, namespace)

Modified: PyCalendar/branches/patch/src/pycalendar/parser.py
===================================================================
--- 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)
@@ -79,4 +79,3 @@
         ParserContext.INVALID_REQUEST_STATUS_VALUE = ParserContext.PARSER_RAISE
         ParserContext.BACKSLASH_IN_URI_VALUE = ParserContext.PARSER_RAISE
         ParserContext.BACKSLASH_IN_GEO_VALUE = ParserContext.PARSER_RAISE
-        ParserContext.INVALID_REQUEST_STATUS = ParserContext.PARSER_RAISE

Modified: PyCalendar/branches/patch/src/pycalendar/period.py
===================================================================
--- 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)
@@ -104,16 +104,18 @@
 
     def generate(self, os):
         try:
-            self.mStart.generate(os)
-            os.write("/")
-            if self.mUseDuration:
-                self.mDuration.generate(os)
-            else:
-                self.mEnd.generate(os)
+            os.write(self.getText())
         except:
             pass
 
 
+    def getText(self):
+        return "{}/{}".format(
+            self.mStart.getText(),
+            self.mDuration.getText() if self.mUseDuration else self.mEnd.getText(),
+        )
+
+
     def writeXML(self, node, namespace):
         start = XML.SubElement(node, xmlutils.makeTag(namespace, xmldefinitions.period_start))
         start.text = self.mStart.getXMLText()

Modified: PyCalendar/branches/patch/src/pycalendar/plaintextvalue.py
===================================================================
--- 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)
@@ -42,6 +42,10 @@
             pass
 
 
+    def getTextValue(self):
+        return self.mValue
+
+
     def writeXML(self, node, namespace):
         value = self.getXMLNode(node, namespace)
         value.text = self.mValue

Modified: PyCalendar/branches/patch/src/pycalendar/textvalue.py
===================================================================
--- 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)
@@ -34,9 +34,14 @@
     # os - StringIO object
     def generate(self, os):
         try:
-            # Encoding required
-            utils.writeTextValue(os, self.mValue)
+            os.write(self.getTextValue())
         except:
             pass
 
+
+    def getTextValue(self):
+        # Encoding required
+        return utils.getTextValue(self.mValue)
+
+
 Value.registerType(Value.VALUETYPE_TEXT, TextValue, xmldefinitions.value_text)

Modified: PyCalendar/branches/patch/src/pycalendar/urivalue.py
===================================================================
--- 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)
@@ -43,13 +43,18 @@
         Handle a client bug where it sometimes includes a \n in the value and we need
         to make sure that gets encoded rather than included literally which would break syntax.
         """
+        try:
+            os.write(self.getTextValue())
+        except:
+            pass
+
+
+    def getTextValue(self):
         if '\n' in self.mValue:
-            try:
-                # No encoding required
-                os.write(self.mValue.replace("\n", "\\n"))
-            except:
-                pass
+            # No encoding required
+            return self.mValue.replace("\n", "\\n")
         else:
-            super(URIValue, self).generate(os)
+            return super(URIValue, self).getTextValue()
 
+
 Value.registerType(Value.VALUETYPE_URI, URIValue, xmldefinitions.value_uri)

Modified: PyCalendar/branches/patch/src/pycalendar/utcoffsetvalue.py
===================================================================
--- 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)
@@ -70,26 +70,29 @@
     # os - StringIO object
     def generate(self, os, fullISO=False):
         try:
-            abs_value = self.mValue
-            if abs_value < 0 :
-                sign = "-"
-                abs_value = -self.mValue
-            else:
-                sign = "+"
+            os.write(self.getTextValue(fullISO))
+        except:
+            pass
 
-            secs = abs_value % 60
-            mins = (abs_value / 60) % 60
-            hours = abs_value / (60 * 60)
 
-            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,)
+    def getTextValue(self, fullISO=False):
+        abs_value = self.mValue
+        if abs_value < 0 :
+            sign = "-"
+            abs_value = -self.mValue
+        else:
+            sign = "+"
 
-            os.write(s)
-        except:
-            pass
+        secs = abs_value % 60
+        mins = (abs_value / 60) % 60
+        hours = abs_value / (60 * 60)
 
+        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
 
+
     def writeXML(self, node, namespace):
 
         os = StringIO()

Modified: PyCalendar/branches/patch/src/pycalendar/utils.py
===================================================================
--- 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)
@@ -89,51 +89,43 @@
 
 
 
-def escapeTextValue(value):
-    os = StringIO.StringIO()
-    writeTextValue(os, value)
-    return os.getvalue()
+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])
 
+            # 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(",")
 
+            # Bump past escapee and look for next segment
+            start_pos = end_pos + 1
 
-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])
+            end_pos = find_first_of(value, "\r\n;\\,", start_pos)
+            if end_pos == -1:
+                result.append(value[start_pos:])
+                break
+    else:
+        result.append(value)
 
-                # 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(",")
+    return "".join(result)
 
-                # Bump past escapee and look for next segment
-                start_pos = end_pos + 1
 
-                end_pos = find_first_of(value, "\r\n;\\,", start_pos)
-                if end_pos == -1:
-                    os.write(value[start_pos:])
-                    break
-        else:
-            os.write(value)
 
-    except:
-        pass
-
-
-
 def decodeTextValue(value):
     os = StringIO.StringIO()
 
@@ -287,17 +279,15 @@
 
 
 
-def generateTextList(os, data, sep=';'):
+def getTextList(data, sep=';'):
     """
     Each element of the list must be separately escaped
     """
-    try:
-        if isinstance(data, basestring):
-            data = (data,)
-        results = [escapeTextValue(value) for value in data]
-        os.write(sep.join(results))
-    except:
-        pass
+    if isinstance(data, basestring):
+        data = (data,)
+    elif data is None:
+        data = ("",)
+    return sep.join([getTextValue(value) for value in data])
 
 
 
@@ -343,26 +333,10 @@
 
 
 
-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)
+def getDoubleNestedList(data):
+    return ";".join([getTextList(item, ",") for item in data])
 
-        for item in data[:-1]:
-            _writeElement(item)
-            os.write(";")
-        _writeElement(data[-1])
 
-    except:
-        pass
-
 # Date/time calcs
 days_in_month = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
 days_in_month_leap = (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)

Modified: PyCalendar/branches/patch/src/pycalendar/value.py
===================================================================
--- 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)
@@ -96,6 +96,10 @@
         raise NotImplementedError
 
 
+    def getTextValue(self):
+        raise NotImplementedError
+
+
     def setValue(self, value):
         raise NotImplementedError
 

Modified: PyCalendar/branches/patch/src/pycalendar/valueutils.py
===================================================================
--- 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)
@@ -106,5 +106,9 @@
         return self.mValue
 
 
+    def getTextValue(self):
+        return self.mValue.getText()
+
+
     def setValue(self, value):
         self.mValue = value

Modified: PyCalendar/branches/patch/src/pycalendar/vcard/adr.py
===================================================================
--- 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)
@@ -116,9 +116,16 @@
 
 
     def generate(self, os):
-        utils.generateDoubleNestedList(os, self.mValue)
+        try:
+            os.write(self.getText())
+        except:
+            pass
 
 
+    def getText(self):
+        return utils.getDoubleNestedList(self.mValue)
+
+
     def parseJSON(self, jobject):
         self.mValue = tuple(map(lambda x: x.encode("utf-8"), jobject))
 

Modified: PyCalendar/branches/patch/src/pycalendar/vcard/n.py
===================================================================
--- 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)
@@ -101,7 +101,7 @@
 
         results = []
         for i in (N.PREFIX, N.FIRST, N.MIDDLE, N.LAST, N.SUFFIX):
-            result = _stringOrList(self.mValue[i])
+            result = _stringOrList(self.mValue[i]) if self.mValue[i] else ""
             if result:
                 results.append(result)
 
@@ -113,9 +113,16 @@
 
 
     def generate(self, os):
-        utils.generateDoubleNestedList(os, self.mValue)
+        try:
+            os.write(self.getText())
+        except:
+            pass
 
 
+    def getText(self):
+        return utils.getDoubleNestedList(self.mValue)
+
+
     def parseJSON(self, jobject):
         self.mValue = tuple(map(lambda x: x.encode("utf-8"), jobject))
 

Modified: PyCalendar/branches/patch/src/pycalendar/vcard/orgvalue.py
===================================================================
--- 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)
@@ -41,9 +41,16 @@
 
 
     def generate(self, os):
-        utils.generateTextList(os, self.mValue, ';')
+        try:
+            os.write(self.getTextValue())
+        except:
+            pass
 
 
+    def getTextValue(self):
+        return utils.getTextList(self.mValue, ';')
+
+
     def parseJSONValue(self, jobject):
         self.mValue = tuple(map(lambda x: x.encode("utf-8"), jobject))
 

Modified: PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_adr.py
===================================================================
--- 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)
@@ -29,6 +29,10 @@
                 (("pobox",), ("extended",), ("street1", "street2",), "locality", "region", (), "country"),
                 "pobox;extended;street1,street2;locality;region;;country",
             ),
+            (
+                ("", None, ("street1", "street2",), "locality", "region", (), "country"),
+                ";;street1,street2;locality;region;;country",
+            ),
         )
 
         for args, result in data:

Modified: PyCalendar/branches/patch/src/pycalendar/vcard/tests/test_n.py
===================================================================
--- 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)
@@ -32,6 +32,11 @@
                 "last;first;middle1,middle2;;suffix",
                 "first middle1 middle2 last suffix",
             ),
+            (
+                ("last", ("first",), "", None, ("suffix",)),
+                "last;first;;;suffix",
+                "first last suffix",
+            ),
         )
 
         for args, result, fullName in data:
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20151210/f2a460ce/attachment-0001.html>


More information about the calendarserver-changes mailing list