Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 

227 rader
7.7 KiB

  1. from __future__ import print_function
  2. from optparse import OptionParser
  3. import vobject
  4. """
  5. Compare VTODOs and VEVENTs in two iCalendar sources.
  6. """
  7. def getSortKey(component):
  8. def getUID(component):
  9. return component.getChildValue('uid', '')
  10. # it's not quite as simple as getUID, need to account for recurrenceID and
  11. # sequence
  12. def getSequence(component):
  13. sequence = component.getChildValue('sequence', 0)
  14. return "{0:05d}".format(int(sequence))
  15. def getRecurrenceID(component):
  16. recurrence_id = component.getChildValue('recurrence_id', None)
  17. if recurrence_id is None:
  18. return '0000-00-00'
  19. else:
  20. return recurrence_id.isoformat()
  21. return getUID(component) + getSequence(component) + getRecurrenceID(component)
  22. def sortByUID(components):
  23. return sorted(components, key=getSortKey)
  24. def deleteExtraneous(component, ignore_dtstamp=False):
  25. """
  26. Recursively walk the component's children, deleting extraneous details like
  27. X-VOBJ-ORIGINAL-TZID.
  28. """
  29. for comp in component.components():
  30. deleteExtraneous(comp, ignore_dtstamp)
  31. for line in component.lines():
  32. if 'X-VOBJ-ORIGINAL-TZID' in line.params:
  33. del line.params['X-VOBJ-ORIGINAL-TZID']
  34. if ignore_dtstamp and hasattr(component, 'dtstamp_list'):
  35. del component.dtstamp_list
  36. def diff(left, right):
  37. """
  38. Take two VCALENDAR components, compare VEVENTs and VTODOs in them,
  39. return a list of object pairs containing just UID and the bits
  40. that didn't match, using None for objects that weren't present in one
  41. version or the other.
  42. When there are multiple ContentLines in one VEVENT, for instance many
  43. DESCRIPTION lines, such lines original order is assumed to be
  44. meaningful. Order is also preserved when comparing (the unlikely case
  45. of) multiple parameters of the same type in a ContentLine
  46. """
  47. def processComponentLists(leftList, rightList):
  48. output = []
  49. rightIndex = 0
  50. rightListSize = len(rightList)
  51. for comp in leftList:
  52. if rightIndex >= rightListSize:
  53. output.append((comp, None))
  54. else:
  55. leftKey = getSortKey(comp)
  56. rightComp = rightList[rightIndex]
  57. rightKey = getSortKey(rightComp)
  58. while leftKey > rightKey:
  59. output.append((None, rightComp))
  60. rightIndex += 1
  61. if rightIndex >= rightListSize:
  62. output.append((comp, None))
  63. break
  64. else:
  65. rightComp = rightList[rightIndex]
  66. rightKey = getSortKey(rightComp)
  67. if leftKey < rightKey:
  68. output.append((comp, None))
  69. elif leftKey == rightKey:
  70. rightIndex += 1
  71. matchResult = processComponentPair(comp, rightComp)
  72. if matchResult is not None:
  73. output.append(matchResult)
  74. return output
  75. def newComponent(name, body):
  76. if body is None:
  77. return None
  78. else:
  79. c = vobject.base.Component(name)
  80. c.behavior = vobject.base.getBehavior(name)
  81. c.isNative = True
  82. return c
  83. def processComponentPair(leftComp, rightComp):
  84. """
  85. Return None if a match, or a pair of components including UIDs and
  86. any differing children.
  87. """
  88. leftChildKeys = leftComp.contents.keys()
  89. rightChildKeys = rightComp.contents.keys()
  90. differentContentLines = []
  91. differentComponents = {}
  92. for key in leftChildKeys:
  93. rightList = rightComp.contents.get(key, [])
  94. if isinstance(leftComp.contents[key][0], vobject.base.Component):
  95. compDifference = processComponentLists(leftComp.contents[key],
  96. rightList)
  97. if len(compDifference) > 0:
  98. differentComponents[key] = compDifference
  99. elif leftComp.contents[key] != rightList:
  100. differentContentLines.append((leftComp.contents[key],
  101. rightList))
  102. for key in rightChildKeys:
  103. if key not in leftChildKeys:
  104. if isinstance(rightComp.contents[key][0], vobject.base.Component):
  105. differentComponents[key] = ([], rightComp.contents[key])
  106. else:
  107. differentContentLines.append(([], rightComp.contents[key]))
  108. if len(differentContentLines) == 0 and len(differentComponents) == 0:
  109. return None
  110. else:
  111. left = newFromBehavior(leftComp.name)
  112. right = newFromBehavior(leftComp.name)
  113. # add a UID, if one existed, despite the fact that they'll always be
  114. # the same
  115. uid = leftComp.getChildValue('uid')
  116. if uid is not None:
  117. left.add('uid').value = uid
  118. right.add('uid').value = uid
  119. for name, childPairList in differentComponents.items():
  120. leftComponents, rightComponents = zip(*childPairList)
  121. if len(leftComponents) > 0:
  122. # filter out None
  123. left.contents[name] = filter(None, leftComponents)
  124. if len(rightComponents) > 0:
  125. # filter out None
  126. right.contents[name] = filter(None, rightComponents)
  127. for leftChildLine, rightChildLine in differentContentLines:
  128. nonEmpty = leftChildLine or rightChildLine
  129. name = nonEmpty[0].name
  130. if leftChildLine is not None:
  131. left.contents[name] = leftChildLine
  132. if rightChildLine is not None:
  133. right.contents[name] = rightChildLine
  134. return left, right
  135. vevents = processComponentLists(sortByUID(getattr(left, 'vevent_list', [])),
  136. sortByUID(getattr(right, 'vevent_list', [])))
  137. vtodos = processComponentLists(sortByUID(getattr(left, 'vtodo_list', [])),
  138. sortByUID(getattr(right, 'vtodo_list', [])))
  139. return vevents + vtodos
  140. def prettyDiff(leftObj, rightObj):
  141. for left, right in diff(leftObj, rightObj):
  142. print("<<<<<<<<<<<<<<<")
  143. if left is not None:
  144. left.prettyPrint()
  145. print("===============")
  146. if right is not None:
  147. right.prettyPrint()
  148. print(">>>>>>>>>>>>>>>")
  149. print
  150. def main():
  151. options, args = getOptions()
  152. if args:
  153. ignore_dtstamp = options.ignore
  154. ics_file1, ics_file2 = args
  155. with open(ics_file1) as f, open(ics_file2) as g:
  156. cal1 = readOne(f)
  157. cal2 = readOne(g)
  158. deleteExtraneous(cal1, ignore_dtstamp=ignore_dtstamp)
  159. deleteExtraneous(cal2, ignore_dtstamp=ignore_dtstamp)
  160. prettyDiff(cal1, cal2)
  161. def getOptions():
  162. ##### Configuration options #####
  163. usage = "usage: %prog [options] ics_file1 ics_file2"
  164. parser = OptionParser(usage=usage, version=vobject.VERSION)
  165. parser.set_description("ics_diff will print a comparison of two iCalendar files ")
  166. parser.add_option("-i", "--ignore-dtstamp", dest="ignore", action="store_true",
  167. default=False, help="ignore DTSTAMP lines [default: False]")
  168. (cmdline_options, args) = parser.parse_args()
  169. if len(args) < 2:
  170. print("error: too few arguments given")
  171. print
  172. print(parser.format_help())
  173. return False, False
  174. return cmdline_options, args
  175. if __name__ == "__main__":
  176. try:
  177. main()
  178. except KeyboardInterrupt:
  179. print("Aborted")