diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcadb2c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25ee2cd..88e7cb5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,10 +5,10 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black - args: [-l, "120", -C] + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: @@ -16,7 +16,15 @@ repos: - id: end-of-file-fixer - id: no-commit-to-branch args: [-b, main, -b, master] + - repo: https://github.com/PyCQA/flake8 rev: 7.0.0 hooks: - id: flake8 + additional_dependencies: [flake8-pyproject] + + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.5 + hooks: + - id: forbid-crlf + - id: remove-crlf diff --git a/ACKNOWLEDGEMENTS.txt b/ACKNOWLEDGEMENTS.txt index c6ebcc9..5fd9b6e 100644 --- a/ACKNOWLEDGEMENTS.txt +++ b/ACKNOWLEDGEMENTS.txt @@ -1,9 +1,9 @@ -Enormous thanks to: -Jeffrey Harris, for his incredible work on the original package -Tim Baxter, for all his work maintaining vobject over the past few years -Adieu, for keeping things alive on github -Kristian Glass, for his enormous help with testing and Python3 matters -Gustavo Niemeyer, for all his work on dateutil -Dave Cridland, for helping talk about vobject and working on vcard -TJ Gabbour, for putting his heart into parsing -Sameen Karim and Will Percival, for maintaining the package at Eventable. +Enormous thanks to: +Jeffrey Harris, for his incredible work on the original package +Tim Baxter, for all his work maintaining vobject over the past few years +Adieu, for keeping things alive on github +Kristian Glass, for his enormous help with testing and Python3 matters +Gustavo Niemeyer, for all his work on dateutil +Dave Cridland, for helping talk about vobject and working on vcard +TJ Gabbour, for putting his heart into parsing +Sameen Karim and Will Percival, for maintaining the package at Eventable. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f2e68e..5a93491 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,4 +34,3 @@ releases. With the possible exception of major releases, all contributions must maintain the existing API's syntax and semantics. - diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c8736f7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.black] +target-version = ["py37", "py38", "py39", "py310", "py311"] +line-length = 120 +skip-magic-trailing-comma = true + +[tool.flake8] +max-line-length = 120 +ignore = ["E203", "E266", "E501", "W503"] +exclude = [".git", "__pycache__", "venv"] +per-file-ignores = ["*/__init__.py: F401"] + +[tool.isort] +profile = "black" +multi_line_output = 3 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 66e5a12..0000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 120 -ignore = E203, E266, E501, W503 -exclude = .git, __pycache__, venv -per-file-ignores = */__init__.py: F401 - -[isort] -profile = black -multi_line_output = 3 diff --git a/setup.py b/setup.py index 76bd4f3..aeba6d1 100755 --- a/setup.py +++ b/setup.py @@ -1,62 +1,62 @@ -""" -VObject: module for reading vCard and vCalendar files - -Description ------------ - -Parses iCalendar and vCard files into Python data structures, decoding the -relevant encodings. Also serializes vobject data structures to iCalendar, vCard, -or (experimentally) hCalendar unicode strings. - -Requirements ------------- - -Requires python 2.7 or later, dateutil 2.4.0 or later and six. - -Recent changes --------------- - - Revert too-strict serialization of timestamp values - broke too many other - implementations - -For older changes, see - - http://py-vobject.github.io/vobject/#release-history or - - http://vobject.skyhouseconsulting.com/history.html -""" - -from setuptools import find_packages, setup - -doclines = (__doc__ or "").splitlines() - -setup( - name="vobject", - version="0.9.7", - author="Jeffrey Harris", - author_email="jeffrey@osafoundation.org", - maintainer="David Arnold", - maintainer_email="davida@pobox.com", - license="Apache", - zip_safe=True, - url="http://py-vobject.github.io/vobject/", - download_url="https://github.com/py-vobject/vobject/tarball/0.9.7", - bugtrack_url="https://github.com/py-vobject/vobject/issues", - entry_points={"console_scripts": ["ics_diff = vobject.ics_diff:main", "change_tz = vobject.change_tz:main"]}, - include_package_data=True, - install_requires=["python-dateutil >= 2.4.0", "six"], - platforms=["any"], - packages=find_packages(), - description="A full-featured Python package for parsing and creating " "iCalendar and vCard files", - long_description="\n".join(doclines[2:]), - keywords=["vobject", "icalendar", "vcard", "ics", "vcs", "hcalendar"], - test_suite="tests", - classifiers=""" - Development Status :: 5 - Production/Stable - Environment :: Console - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Natural Language :: English - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 - Topic :: Text Processing""".strip().splitlines(), -) +""" +VObject: module for reading vCard and vCalendar files + +Description +----------- + +Parses iCalendar and vCard files into Python data structures, decoding the +relevant encodings. Also serializes vobject data structures to iCalendar, vCard, +or (experimentally) hCalendar unicode strings. + +Requirements +------------ + +Requires python 2.7 or later, dateutil 2.4.0 or later and six. + +Recent changes +-------------- + - Revert too-strict serialization of timestamp values - broke too many other + implementations + +For older changes, see + - http://py-vobject.github.io/vobject/#release-history or + - http://vobject.skyhouseconsulting.com/history.html +""" + +from setuptools import find_packages, setup + +doclines = (__doc__ or "").splitlines() + +setup( + name="vobject", + version="0.9.7", + author="Jeffrey Harris", + author_email="jeffrey@osafoundation.org", + maintainer="David Arnold", + maintainer_email="davida@pobox.com", + license="Apache", + zip_safe=True, + url="http://py-vobject.github.io/vobject/", + download_url="https://github.com/py-vobject/vobject/tarball/0.9.7", + bugtrack_url="https://github.com/py-vobject/vobject/issues", + entry_points={"console_scripts": ["ics_diff = vobject.ics_diff:main", "change_tz = vobject.change_tz:main"]}, + include_package_data=True, + install_requires=["python-dateutil >= 2.4.0", "six"], + platforms=["any"], + packages=find_packages(), + description="A full-featured Python package for parsing and creating " "iCalendar and vCard files", + long_description="\n".join(doclines[2:]), + keywords=["vobject", "icalendar", "vcard", "ics", "vcs", "hcalendar"], + test_suite="tests", + classifiers=""" + Development Status :: 5 - Production/Stable + Environment :: Console + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Topic :: Text Processing""".strip().splitlines(), +) diff --git a/test_files/availablity.ics b/test_files/availablity.ics index 446db07..e58a7bd 100644 --- a/test_files/availablity.ics +++ b/test_files/availablity.ics @@ -1,14 +1,14 @@ -BEGIN:VAVAILABILITY -UID:test -DTSTART:20060216T000000Z -DTEND:20060217T000000Z -BEGIN:AVAILABLE -UID:test1 -DTSTART:20060216T090000Z -DTEND:20060216T120000Z -DTSTAMP:20060215T000000Z -SUMMARY:Available in the morning -END:AVAILABLE -BUSYTYPE:BUSY -DTSTAMP:20060215T000000Z -END:VAVAILABILITY +BEGIN:VAVAILABILITY +UID:test +DTSTART:20060216T000000Z +DTEND:20060217T000000Z +BEGIN:AVAILABLE +UID:test1 +DTSTART:20060216T090000Z +DTEND:20060216T120000Z +DTSTAMP:20060215T000000Z +SUMMARY:Available in the morning +END:AVAILABLE +BUSYTYPE:BUSY +DTSTAMP:20060215T000000Z +END:VAVAILABILITY diff --git a/test_files/freebusy.ics b/test_files/freebusy.ics index 92561d5..0871658 100644 --- a/test_files/freebusy.ics +++ b/test_files/freebusy.ics @@ -1,8 +1,8 @@ -BEGIN:VFREEBUSY -UID:test -DTSTART:20060216T010000Z -DTEND:20060216T030000Z -DTSTAMP:20060215T000000Z -FREEBUSY:20060216T010000Z/PT1H -FREEBUSY:20060216T010000Z/20060216T030000Z -END:VFREEBUSY +BEGIN:VFREEBUSY +UID:test +DTSTART:20060216T010000Z +DTEND:20060216T030000Z +DTSTAMP:20060215T000000Z +FREEBUSY:20060216T010000Z/PT1H +FREEBUSY:20060216T010000Z/20060216T030000Z +END:VFREEBUSY diff --git a/test_files/ms_tzid.ics b/test_files/ms_tzid.ics index 0db2c5c..3af37ed 100644 --- a/test_files/ms_tzid.ics +++ b/test_files/ms_tzid.ics @@ -36,4 +36,4 @@ BEGIN:VEVENT UID:CommaTest DTSTART;TZID="Canberra, Melbourne, Sydney":20080530T150000 END:VEVENT -END:VCALENDAR \ No newline at end of file +END:VCALENDAR diff --git a/test_files/recurrence.ics b/test_files/recurrence.ics index f592234..3450efa 100644 --- a/test_files/recurrence.ics +++ b/test_files/recurrence.ics @@ -1,30 +1,30 @@ -BEGIN:VCALENDAR -VERSION - :2.0 -PRODID - :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN -BEGIN:VEVENT -CREATED - :20060327T214227Z -LAST-MODIFIED - :20060313T080829Z -DTSTAMP - :20060116T231602Z -UID - :70922B3051D34A9E852570EC00022388 -SUMMARY - :Monthly - All Hands Meeting with Joe Smith -STATUS - :CONFIRMED -CLASS - :PUBLIC -RRULE - :FREQ=MONTHLY;UNTIL=20061228;INTERVAL=1;BYDAY=4TH -DTSTART - :20060126T230000Z -DTEND - :20060127T000000Z -DESCRIPTION - :Repeat Meeting: - Occurs every 4th Thursday of each month -END:VEVENT -END:VCALENDAR +BEGIN:VCALENDAR +VERSION + :2.0 +PRODID + :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN +BEGIN:VEVENT +CREATED + :20060327T214227Z +LAST-MODIFIED + :20060313T080829Z +DTSTAMP + :20060116T231602Z +UID + :70922B3051D34A9E852570EC00022388 +SUMMARY + :Monthly - All Hands Meeting with Joe Smith +STATUS + :CONFIRMED +CLASS + :PUBLIC +RRULE + :FREQ=MONTHLY;UNTIL=20061228;INTERVAL=1;BYDAY=4TH +DTSTART + :20060126T230000Z +DTEND + :20060127T000000Z +DESCRIPTION + :Repeat Meeting: - Occurs every 4th Thursday of each month +END:VEVENT +END:VCALENDAR diff --git a/test_files/ruby_rrule.ics b/test_files/ruby_rrule.ics index 6999513..7ce44e8 100644 --- a/test_files/ruby_rrule.ics +++ b/test_files/ruby_rrule.ics @@ -13,4 +13,4 @@ SUMMARY:Something DTSTART:20030101T070000 DTSTAMP:20080529T152100 END:VEVENT -END:VCALENDAR \ No newline at end of file +END:VCALENDAR diff --git a/test_files/simple_2_0_test.ics b/test_files/simple_2_0_test.ics index 9e50610..aef8dcb 100644 --- a/test_files/simple_2_0_test.ics +++ b/test_files/simple_2_0_test.ics @@ -1,12 +1,12 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//PYVOBJECT//NONSGML Version 1//EN -BEGIN:VEVENT -UID:Not very random UID -DTSTART:20060509T000000 -ATTENDEE;CN=Fröhlich:mailto:froelich@example.com -CREATED:20060101T180000Z -DESCRIPTION:Test event -DTSTAMP:20170626T000000Z -END:VEVENT -END:VCALENDAR +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//PYVOBJECT//NONSGML Version 1//EN +BEGIN:VEVENT +UID:Not very random UID +DTSTART:20060509T000000 +ATTENDEE;CN=Fröhlich:mailto:froelich@example.com +CREATED:20060101T180000Z +DESCRIPTION:Test event +DTSTAMP:20170626T000000Z +END:VEVENT +END:VCALENDAR diff --git a/test_files/utf8_test.ics b/test_files/utf8_test.ics index fbb9ada..791ea1d 100644 --- a/test_files/utf8_test.ics +++ b/test_files/utf8_test.ics @@ -1,39 +1,39 @@ -BEGIN:VCALENDAR -METHOD:PUBLISH -CALSCALE:GREGORIAN -PRODID:-//EVDB//www.evdb.com//EN -VERSION:2.0 -X-WR-CALNAME:EVDB Event Feed -BEGIN:VEVENT -DTSTART:20060922T000100Z -DTEND:20060922T050100Z -DTSTAMP:20050914T163414Z -SUMMARY:The title こんにちはキティ -DESCRIPTION:hello\nHere is a description\n\n\nこんにちはキティ - \n\n\n\nZwei Java-schwere Entwicklerpositionen und irgendeine Art sond - erbar-klingende Netzsichtbarmachungöffnung\, an einer interessanten F - irma im Gebäude\, in dem ich angerufenen Semantic Research bearbeite. - 1. Zauberer Des Semantica Software Engineer 2. Älterer Semantica Sof - tware-Englisch-3. Graph/Semantica Netz-Visualization/Navigation Sie ei - ngestufte Software-Entwicklung für die Regierung. Die Firma ist stark - und die Projekte sind sehr kühl und schließen irgendeinen Spielraum - ein. Wenn ich Ihnen irgendwie mehr erkläre\, muß ich Sie töten. Ps - . Tat schnell -- jemand ist\, wenn es hier interviewt\, wie ich dieses - schreibe. Er schaut intelligent (er trägt Kleidhosen) Semantica Soft - ware Engineer FIRMA: Semantische Forschung\, Inc. REPORTS ZU: Vizeprä - sident\, Produkt-Entwicklung POSITION: San Diego (Pint Loma) WEB SITE: - www.semanticresearch.com email: dorie@semanticresearch.com FIRMA-HINT - ERGRUND Semantische Forschung ist der führende Versorger der semantis - cher Netzwerkanschluß gegründeten nicht linearen Wissen Darstellung - Werkzeuge. Die Firma stellt diese Werkzeuge zum Intel\, zur reg.\, zum - EDU und zu den kommerziellen Märkten zur Verfügung. BRINGEN SIE ZUS - AMMENFASSUNG IN POSITION Semantische Forschung\, Inc. basiert in San D - iego\, Ca im alten realen Weltsan Diego Haus...\, das wir den Weltbest - en Platz haben zum zu arbeiten. Wir suchen nach Superstarentwicklern\, - um uns in der fortfahrenden Entwicklung unserer Semantica Produktseri - e zu unterstützen. -LOCATION:こんにちはキティ -SEQUENCE:0 -UID:E0-001-000276068-2 -END:VEVENT +BEGIN:VCALENDAR +METHOD:PUBLISH +CALSCALE:GREGORIAN +PRODID:-//EVDB//www.evdb.com//EN +VERSION:2.0 +X-WR-CALNAME:EVDB Event Feed +BEGIN:VEVENT +DTSTART:20060922T000100Z +DTEND:20060922T050100Z +DTSTAMP:20050914T163414Z +SUMMARY:The title こんにちはキティ +DESCRIPTION:hello\nHere is a description\n\n\nこんにちはキティ + \n\n\n\nZwei Java-schwere Entwicklerpositionen und irgendeine Art sond + erbar-klingende Netzsichtbarmachungöffnung\, an einer interessanten F + irma im Gebäude\, in dem ich angerufenen Semantic Research bearbeite. + 1. Zauberer Des Semantica Software Engineer 2. Älterer Semantica Sof + tware-Englisch-3. Graph/Semantica Netz-Visualization/Navigation Sie ei + ngestufte Software-Entwicklung für die Regierung. Die Firma ist stark + und die Projekte sind sehr kühl und schließen irgendeinen Spielraum + ein. Wenn ich Ihnen irgendwie mehr erkläre\, muß ich Sie töten. Ps + . Tat schnell -- jemand ist\, wenn es hier interviewt\, wie ich dieses + schreibe. Er schaut intelligent (er trägt Kleidhosen) Semantica Soft + ware Engineer FIRMA: Semantische Forschung\, Inc. REPORTS ZU: Vizeprä + sident\, Produkt-Entwicklung POSITION: San Diego (Pint Loma) WEB SITE: + www.semanticresearch.com email: dorie@semanticresearch.com FIRMA-HINT + ERGRUND Semantische Forschung ist der führende Versorger der semantis + cher Netzwerkanschluß gegründeten nicht linearen Wissen Darstellung + Werkzeuge. Die Firma stellt diese Werkzeuge zum Intel\, zur reg.\, zum + EDU und zu den kommerziellen Märkten zur Verfügung. BRINGEN SIE ZUS + AMMENFASSUNG IN POSITION Semantische Forschung\, Inc. basiert in San D + iego\, Ca im alten realen Weltsan Diego Haus...\, das wir den Weltbest + en Platz haben zum zu arbeiten. Wir suchen nach Superstarentwicklern\, + um uns in der fortfahrenden Entwicklung unserer Semantica Produktseri + e zu unterstützen. +LOCATION:こんにちはキティ +SEQUENCE:0 +UID:E0-001-000276068-2 +END:VEVENT END:VCALENDAR \ No newline at end of file diff --git a/tests.py b/tests.py index de9ae03..825a14b 100644 --- a/tests.py +++ b/tests.py @@ -1,912 +1,912 @@ -# -*- coding: utf-8 -*- - -from __future__ import print_function - -import datetime -import json -import re -import sys -import unittest - -import dateutil -from dateutil.rrule import MONTHLY, WEEKLY, rrule, rruleset -from dateutil.tz import tzutc - -from vobject import base, iCalendar, icalendar -from vobject.base import ContentLine, ParseError -from vobject.base import __behaviorRegistry as BehaviorRegistry -from vobject.base import parseLine, readComponents, textLineToContentLine -from vobject.change_tz import change_tz -from vobject.icalendar import ( - MultiDateBehavior, - PeriodBehavior, - RecurringComponent, - parseDtstart, - stringToPeriod, - stringToTextValues, - timedeltaToString, - utc, -) - -behavior_registry = BehaviorRegistry -two_hours = datetime.timedelta(hours=2) - - -def get_test_file(path): - """ - Helper function to open and read test files. - """ - filepath = f"test_files/{path}" - if sys.version_info[0] < 3: - # On python 2, this library operates on bytes. - f = open(filepath, "r") - else: - # On python 3, it operates on unicode. We need to specify an encoding - # for systems for which the preferred encoding isn't utf-8 (e.g windows) - f = open(filepath, "r", encoding="utf-8") - text = f.read() - f.close() - return text - - -class TestCalendarSerializing(unittest.TestCase): - """ - Test creating an iCalendar file - """ - - max_diff = None - - def test_scratchbuild(self): - """ - CreateCalendar 2.0 format from scratch - """ - test_cal = get_test_file("simple_2_0_test.ics") - cal = base.newFromBehavior("vcalendar", "2.0") - cal.add("vevent") - cal.vevent.add("dtstart").value = datetime.datetime(2006, 5, 9) - cal.vevent.add("description").value = "Test event" - cal.vevent.add("created").value = datetime.datetime( - 2006, 1, 1, 10, tzinfo=dateutil.tz.tzical("test_files/timezones.ics").get("US/Pacific") - ) - cal.vevent.add("uid").value = "Not very random UID" - cal.vevent.add("dtstamp").value = datetime.datetime(2017, 6, 26, 0, tzinfo=tzutc()) - - cal.vevent.add("attendee").value = "mailto:froelich@example.com" - cal.vevent.attendee.params["CN"] = ["Fröhlich"] - - # Note we're normalizing line endings, because no one got time for that. - self.assertEqual(cal.serialize().replace("\r\n", "\n"), test_cal.replace("\r\n", "\n")) - - def test_unicode(self): - """ - Test unicode characters - """ - test_cal = get_test_file("utf8_test.ics") - vevent = base.readOne(test_cal).vevent - vevent2 = base.readOne(vevent.serialize()) - self.assertEqual(str(vevent), str(vevent2)) - - self.assertEqual(vevent.summary.value, "The title こんにちはキティ") - - if sys.version_info[0] < 3: - test_cal = test_cal.decode("utf-8") - vevent = base.readOne(test_cal).vevent - vevent2 = base.readOne(vevent.serialize()) - self.assertEqual(str(vevent), str(vevent2)) - self.assertEqual(vevent.summary.value, "The title こんにちはキティ") - - def test_wrapping(self): - """ - Should support input file with a long text field covering multiple lines - """ - test_journal = get_test_file("journal.ics") - vobj = base.readOne(test_journal) - vjournal = base.readOne(vobj.serialize()) - self.assertTrue("Joe, Lisa, and Bob" in vjournal.description.value) - self.assertTrue("Tuesday.\n2." in vjournal.description.value) - - def test_multiline(self): - """ - Multi-text serialization test - """ - category = base.newFromBehavior("categories") - category.value = ["Random category"] - self.assertEqual(category.serialize().strip(), "CATEGORIES:Random category") - - category.value.append("Other category") - self.assertEqual(category.serialize().strip(), "CATEGORIES:Random category,Other category") - - def test_semicolon_separated(self): - """ - Semi-colon separated multi-text serialization test - """ - request_status = base.newFromBehavior("request-status") - request_status.value = ["5.1", "Service unavailable"] - self.assertEqual(request_status.serialize().strip(), "REQUEST-STATUS:5.1;Service unavailable") - - @staticmethod - def test_unicode_multiline(): - """ - Test multiline unicode characters - """ - cal = iCalendar() - cal.add("method").value = "REQUEST" - cal.add("vevent") - cal.vevent.add("created").value = datetime.datetime.now() - cal.vevent.add("summary").value = "Классное событие" - cal.vevent.add("description").value = ( - "Классное событие Классное событие Классное событие Классное событие " - "Классное событие Классsdssdное событие" - ) - - # json tries to encode as utf-8 and it would break if some chars could not be encoded - json.dumps(cal.serialize()) - - @staticmethod - def test_ical_to_hcal(): - """ - Serializing iCalendar to hCalendar. - - Since Hcalendar is experimental and the behavior doesn't seem to want to load, - This test will have to wait. - - - tzs = dateutil.tz.tzical("test_files/timezones.ics") - cal = base.newFromBehavior('hcalendar') - self.assertEqual( - str(cal.behavior), - "" - ) - cal.add('vevent') - cal.vevent.add('summary').value = "this is a note" - cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator" - cal.vevent.add('dtstart').value = datetime.date(2006,2,27) - cal.vevent.add('location').value = "a place" - cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2) - - event2 = cal.add('vevent') - event2.add('summary').value = "Another one" - event2.add('description').value = "The greatest thing ever!" - event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = tzs.get('US/Pacific')) - event2.add('location').value = "somewhere else" - event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6) - hcal = cal.serialize() - """ - # self.assertEqual( - # str(hcal), - # """ - # - # this is a note: - # Monday, February 27 - # - Tuesday, February 28 - # at a place - # - # - # - # Another one: - # Thursday, December 17, 16:42 - # - Wednesday, December 23, 16:42 - # at somewhere else - #
The greatest thing ever!
- #
- # """ - # ) - - -class TestBehaviors(unittest.TestCase): - """ - Test Behaviors - """ - - def test_general_behavior(self): - """ - Tests for behavior registry, getting and creating a behavior. - """ - # Check expected behavior registry. - self.assertEqual( - sorted(behavior_registry.keys()), - [ - "", - "ACTION", - "ADR", - "AVAILABLE", - "BUSYTYPE", - "CALSCALE", - "CATEGORIES", - "CLASS", - "COMMENT", - "COMPLETED", - "CONTACT", - "CREATED", - "DAYLIGHT", - "DESCRIPTION", - "DTEND", - "DTSTAMP", - "DTSTART", - "DUE", - "DURATION", - "EXDATE", - "EXRULE", - "FN", - "FREEBUSY", - "LABEL", - "LAST-MODIFIED", - "LOCATION", - "METHOD", - "N", - "ORG", - "PHOTO", - "PRODID", - "RDATE", - "RECURRENCE-ID", - "RELATED-TO", - "REQUEST-STATUS", - "RESOURCES", - "RRULE", - "STANDARD", - "STATUS", - "SUMMARY", - "TRANSP", - "TRIGGER", - "UID", - "VALARM", - "VAVAILABILITY", - "VCALENDAR", - "VCARD", - "VEVENT", - "VFREEBUSY", - "VJOURNAL", - "VTIMEZONE", - "VTODO", - ], - ) - - # test get_behavior - behavior = base.getBehavior("VCALENDAR") - self.assertEqual(str(behavior), "") - self.assertTrue(behavior.isComponent) - - self.assertEqual(base.getBehavior("invalid_name"), None) - # test for ContentLine (not a component) - non_component_behavior = base.getBehavior("RDATE") - self.assertFalse(non_component_behavior.isComponent) - - def test_MultiDateBehavior(self): - """ - Test MultiDateBehavior - """ - parseRDate = MultiDateBehavior.transformToNative - self.assertEqual( - str(parseRDate(textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904"))), - "", - ) - self.assertEqual( - str( - parseRDate( - textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H") - ) - ), - "", - ) - - def test_periodBehavior(self): - """ - Test PeriodBehavior - """ - line = ContentLine("test", [], "", isNative=True) - line.behavior = PeriodBehavior - line.value = [(datetime.datetime(2006, 2, 16, 10), two_hours)] - - self.assertEqual(line.transformFromNative().value, "20060216T100000/PT2H") - self.assertEqual( - line.transformToNative().value, [(datetime.datetime(2006, 2, 16, 10, 0), datetime.timedelta(0, 7200))] - ) - - line.value.append((datetime.datetime(2006, 5, 16, 10), two_hours)) - - self.assertEqual(line.serialize().strip(), "TEST:20060216T100000/PT2H,20060516T100000/PT2H") - - -class TestVTodo(unittest.TestCase): - """ - VTodo Tests - """ - - def test_vtodo(self): - """ - Test VTodo - """ - vtodo = get_test_file("vtodo.ics") - obj = base.readOne(vtodo) - obj.vtodo.add("completed") - obj.vtodo.completed.value = datetime.datetime(2015, 5, 5, 13, 30) - self.assertEqual(obj.vtodo.completed.serialize()[:23], "COMPLETED:20150505T1330") - obj = base.readOne(obj.serialize()) - self.assertEqual(obj.vtodo.completed.value, datetime.datetime(2015, 5, 5, 13, 30)) - - -class TestVobject(unittest.TestCase): - """ - VObject Tests - """ - - max_diff = None - - @classmethod - def setUpClass(cls): - """ - Method for setting up class fixture before running tests in the class. - Fetches test file. - """ - cls.simple_test_cal = get_test_file("simple_test.ics") - - def test_readComponents(self): - """ - Test if reading components correctly - """ - cal = next(readComponents(self.simple_test_cal)) - - self.assertEqual(str(cal), "]>]>") - self.assertEqual(str(cal.vevent.summary), "") - - def test_parseLine(self): - """ - Test line parsing - """ - self.assertEqual(parseLine("BLAH:"), ("BLAH", [], "", None)) - self.assertEqual( - parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904"), - ("RDATE", [], "VALUE=DATE:19970304,19970504,19970704,19970904", None), - ) - self.assertEqual( - parseLine( - 'DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA' - ), - ( - "DESCRIPTION", - [["ALTREP", "http://www.wiz.org"]], - "The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA", - None, - ), - ) - self.assertEqual( - parseLine("EMAIL;PREF;INTERNET:john@nowhere.com"), - ("EMAIL", [["PREF"], ["INTERNET"]], "john@nowhere.com", None), - ) - self.assertEqual( - parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com'), - ("EMAIL", [["TYPE", "blah", "hah"], ["INTERNET", "DIGI", "DERIDOO"]], "john@nowhere.com", None), - ) - self.assertEqual( - parseLine("item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;"), - ("ADR", [["type", "HOME"], ["type", "pref"]], ";;Reeperbahn 116;Hamburg;;20359;", "item1"), - ) - self.assertRaises(ParseError, parseLine, ":") - - -class TestGeneralFileParsing(unittest.TestCase): - """ - General tests for parsing ics files. - """ - - def test_readOne(self): - """ - Test reading first component of ics - """ - cal = get_test_file("silly_test.ics") - silly = base.readOne(cal) - self.assertEqual( - str(silly), - ", , ]>", - ) - self.assertEqual(str(silly.stuff), "") - - def test_importing(self): - """ - Test importing ics - """ - cal = get_test_file("standard_test.ics") - c = base.readOne(cal, validate=True) - self.assertEqual(str(c.vevent.valarm.trigger), "") - - self.assertEqual(str(c.vevent.dtstart.value), "2002-10-28 14:00:00-08:00") - self.assertTrue(isinstance(c.vevent.dtstart.value, datetime.datetime)) - self.assertEqual(str(c.vevent.dtend.value), "2002-10-28 15:00:00-08:00") - self.assertTrue(isinstance(c.vevent.dtend.value, datetime.datetime)) - self.assertEqual(c.vevent.dtstamp.value, datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc())) - - vevent = c.vevent.transformFromNative() - self.assertEqual(str(vevent.rrule), "") - - def test_bad_stream(self): - """ - Test bad ics stream - """ - cal = get_test_file("badstream.ics") - self.assertRaises(ParseError, base.readOne, cal) - - def test_bad_line(self): - """ - Test bad line in ics file - """ - cal = get_test_file("badline.ics") - self.assertRaises(ParseError, base.readOne, cal) - - newcal = base.readOne(cal, ignoreUnreadable=True) - self.assertEqual(str(newcal.vevent.x_bad_underscore), "") - - def test_parseParams(self): - """ - Test parsing parameters - """ - self.assertEqual(base.parseParams(';ALTREP="http://www.wiz.org"'), [["ALTREP", "http://www.wiz.org"]]) - self.assertEqual( - base.parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR'), - [["ALTREP", "http://www.wiz.org;;", "Blah", "Foo"], ["NEXT", "Nope"], ["BAR"]], - ) - - -class TestVcards(unittest.TestCase): - """ - Test VCards - """ - - @classmethod - def setUpClass(cls): - """ - Method for setting up class fixture before running tests in the class. - Fetches test file. - """ - cls.test_file = get_test_file("vcard_with_groups.ics") - cls.card = base.readOne(cls.test_file) - - def test_vcard_creation(self): - """ - Test creating a vCard - """ - vcard = base.newFromBehavior("vcard", "3.0") - self.assertEqual(str(vcard), "") - - def test_default_behavior(self): - """ - Default behavior test. - """ - card = self.card - self.assertEqual(base.getBehavior("note"), None) - self.assertEqual( - str(card.note.value), "The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line." - ) - - def test_with_groups(self): - """ - vCard groups test - """ - card = self.card - self.assertEqual(str(card.group), "home") - self.assertEqual(str(card.tel.group), "home") - - card.group = card.tel.group = "new" - self.assertEqual(str(card.tel.serialize().strip()), "new.TEL;TYPE=fax,voice,msg:+49 3581 123456") - self.assertEqual(str(card.serialize().splitlines()[0]), "new.BEGIN:VCARD") - - def test_vcard_3_parsing(self): - """ - VCARD 3.0 parse test - """ - test_file = get_test_file("simple_3_0_test.ics") - card = base.readOne(test_file) - # value not rendering correctly? - # self.assertEqual( - # card.adr.value, - # "" - # ) - self.assertEqual(card.org.value, ["University of Novosibirsk", "Department of Octopus Parthenogenesis"]) - - for _ in range(3): - new_card = base.readOne(card.serialize()) - self.assertEqual(new_card.org.value, card.org.value) - card = new_card - - -class TestIcalendar(unittest.TestCase): - """ - Tests for icalendar.py - """ - - max_diff = None - - def test_parseDTStart(self): - """ - Should take a content line and return a datetime object. - """ - self.assertEqual( - parseDtstart(textLineToContentLine("DTSTART:20060509T000000")), datetime.datetime(2006, 5, 9, 0, 0) - ) - - def test_regexes(self): - """ - Test regex patterns - """ - self.assertEqual(re.findall(base.patterns["name"], "12foo-bar:yay"), ["12foo-bar", "yay"]) - self.assertEqual(re.findall(base.patterns["safe_char"], 'a;b"*,cd'), ["a", "b", "*", "c", "d"]) - self.assertEqual(re.findall(base.patterns["qsafe_char"], 'a;b"*,cd'), ["a", ";", "b", "*", ",", "c", "d"]) - self.assertEqual( - re.findall(base.patterns["param_value"], '"quoted";not-quoted;start"after-illegal-quote', re.VERBOSE), - ['"quoted"', "", "not-quoted", "", "start", "", "after-illegal-quote", ""], - ) - match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"') - self.assertEqual(match.group("value"), 'value:;"') - self.assertEqual(match.group("name"), "TEST") - self.assertEqual(match.group("params"), ';ALTREP="http://www.wiz.org"') - - def test_stringToTextValues(self): - """ - Test string lists - """ - self.assertEqual(stringToTextValues(""), [""]) - self.assertEqual(stringToTextValues("abcd,efgh"), ["abcd", "efgh"]) - - def test_stringToPeriod(self): - """ - Test datetime strings - """ - self.assertEqual( - stringToPeriod("19970101T180000Z/19970102T070000Z"), - (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc())), - ) - self.assertEqual( - stringToPeriod("19970101T180000Z/PT1H"), - (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.timedelta(0, 3600)), - ) - - def test_timedeltaToString(self): - """ - Test timedelta strings - """ - self.assertEqual(timedeltaToString(two_hours), "PT2H") - self.assertEqual(timedeltaToString(datetime.timedelta(minutes=20)), "PT20M") - - def test_delta_to_offset(self): - """Test deltaToOffset() function.""" - - # Sydney - delta = datetime.timedelta(hours=10) - self.assertEqual(icalendar.deltaToOffset(delta), "+1000") - - # New York - delta = datetime.timedelta(hours=-5) - self.assertEqual(icalendar.deltaToOffset(delta), "-0500") - - # Adelaide (see https://github.com/py-vobject/vobject/pull/12) - delta = datetime.timedelta(hours=9, minutes=30) - self.assertEqual(icalendar.deltaToOffset(delta), "+0930") - - def test_vtimezone_creation(self): - """ - Test timezones - """ - tzs = dateutil.tz.tzical("test_files/timezones.ics") - pacific = icalendar.TimezoneComponent(tzs.get("US/Pacific")) - self.assertEqual(str(pacific), ">") - santiago = icalendar.TimezoneComponent(tzs.get("Santiago")) - self.assertEqual(str(santiago), ">") - for year in range(2001, 2010): - for month in (2, 9): - dt = datetime.datetime(year, month, 15, tzinfo=tzs.get("Santiago")) - self.assertTrue(dt.replace(tzinfo=tzs.get("Santiago")), dt) - - @staticmethod - def test_timezone_serializing(): - """ - Serializing with timezones test - """ - tzs = dateutil.tz.tzical("test_files/timezones.ics") - pacific = tzs.get("US/Pacific") - cal = base.Component("VCALENDAR") - cal.setBehavior(icalendar.VCalendar2_0) - ev = cal.add("vevent") - ev.add("dtstart").value = datetime.datetime(2005, 10, 12, 9, tzinfo=pacific) - evruleset = rruleset() - evruleset.rrule(rrule(WEEKLY, interval=2, byweekday=[2, 4], until=datetime.datetime(2005, 12, 15, 9))) - evruleset.rrule(rrule(MONTHLY, bymonthday=[-1, -5])) - evruleset.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo=pacific)) - ev.rruleset = evruleset - ev.add("duration").value = datetime.timedelta(hours=1) - - apple = tzs.get("America/Montreal") - ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple) - - def test_pytz_timezone_serializing(self): - """ - Serializing with timezones from pytz test - """ - try: - import pytz - except ImportError: - return self.skipTest("pytz not installed") # NOQA - - # Avoid conflicting cached tzinfo from other tests - def unregister_tzid(tzid): - """Clear tzid from icalendar TZID registry""" - if icalendar.getTzid(tzid, False): - icalendar.registerTzid(tzid, None) - - unregister_tzid("US/Eastern") - eastern = pytz.timezone("US/Eastern") - cal = base.Component("VCALENDAR") - cal.setBehavior(icalendar.VCalendar2_0) - ev = cal.add("vevent") - ev.add("dtstart").value = eastern.localize(datetime.datetime(2008, 10, 12, 9)) - serialized = cal.serialize() - - expected_vtimezone = get_test_file("tz_us_eastern.ics") - self.assertIn(expected_vtimezone.replace("\r\n", "\n"), serialized.replace("\r\n", "\n")) - - # Exhaustively test all zones (just looking for no errors) - for tzname in pytz.all_timezones: - unregister_tzid(tzname) - tz = icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname)) - tz.serialize() - - def test_freeBusy(self): - """ - Test freebusy components - """ - test_cal = get_test_file("freebusy.ics") - - vfb = base.newFromBehavior("VFREEBUSY") - vfb.add("uid").value = "test" - vfb.add("dtstamp").value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) - vfb.add("dtstart").value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) - vfb.add("dtend").value = vfb.dtstart.value + two_hours - vfb.add("freebusy").value = [(vfb.dtstart.value, two_hours / 2)] - vfb.add("freebusy").value = [(vfb.dtstart.value, vfb.dtend.value)] - - self.assertEqual(vfb.serialize().replace("\r\n", "\n"), test_cal.replace("\r\n", "\n")) - - def test_availablity(self): - """ - Test availability components - """ - test_cal = get_test_file("availablity.ics") - - vcal = base.newFromBehavior("VAVAILABILITY") - vcal.add("uid").value = "test" - vcal.add("dtstamp").value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) - vcal.add("dtstart").value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc) - vcal.add("dtend").value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc) - vcal.add("busytype").value = "BUSY" - - av = base.newFromBehavior("AVAILABLE") - av.add("uid").value = "test1" - av.add("dtstamp").value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) - av.add("dtstart").value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc) - av.add("dtend").value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc) - av.add("summary").value = "Available in the morning" - - vcal.add(av) - - self.assertEqual(vcal.serialize().replace("\r\n", "\n"), test_cal.replace("\r\n", "\n")) - - def test_recurrence(self): - """ - Ensure date valued UNTILs in rrules are in a reasonable timezone, - and include that day (12/28 in this test) - """ - test_file = get_test_file("recurrence.ics") - cal = base.readOne(test_file) - dates = list(cal.vevent.getrruleset()) - self.assertEqual(dates[0], datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc())) - self.assertEqual(dates[1], datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc())) - self.assertEqual(dates[-1], datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc())) - - def test_recurring_component(self): - """ - Test recurring events - """ - vevent = RecurringComponent(name="VEVENT") - - # init - self.assertTrue(vevent.isNative) - - # rruleset should be None at this point. - # No rules have been passed or created. - self.assertEqual(vevent.rruleset, None) - - # Now add start and rule for recurring event - vevent.add("dtstart").value = datetime.datetime(2005, 1, 19, 9) - vevent.add("rrule").value = "FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH" - self.assertEqual( - list(vevent.rruleset), [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)] - ) - self.assertEqual( - list(vevent.getrruleset(addRDate=True)), - [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)], - ) - - # Also note that dateutil will expand all-day events (datetime.date values) - # to datetime.datetime value with time 0 and no timezone. - vevent.dtstart.value = datetime.date(2005, 3, 18) - self.assertEqual( - list(vevent.rruleset), [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)] - ) - self.assertEqual( - list(vevent.getrruleset(True)), [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)] - ) - - def test_recurrence_without_tz(self): - """ - Test recurring vevent missing any time zone definitions. - """ - test_file = get_test_file("recurrence-without-tz.ics") - cal = base.readOne(test_file) - dates = list(cal.vevent.getrruleset()) - self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) - self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) - self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) - - def test_recurrence_offset_naive(self): - """ - Ensure recurring vevent missing some time zone definitions is - parsing. See isseu #75. - """ - test_file = get_test_file("recurrence-offset-naive.ics") - cal = base.readOne(test_file) - dates = list(cal.vevent.getrruleset()) - self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) - self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) - self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) - - -class TestChangeTZ(unittest.TestCase): - """ - Tests for change_tz.change_tz - """ - - class StubCal(object): - class StubEvent(object): - class Node(object): - def __init__(self, value): - self.value = value - - def __init__(self, dtstart, dtend): - self.dtstart = self.Node(dtstart) - self.dtend = self.Node(dtend) - - def __init__(self, dates): - """ - dates is a list of tuples (dtstart, dtend) - """ - self.vevent_list = [self.StubEvent(*d) for d in dates] - - def test_change_tz(self): - """ - Change the timezones of events in a component to a different - timezone - """ - - # Setup - create a stub vevent list - old_tz = dateutil.tz.gettz("UTC") # 0:00 - new_tz = dateutil.tz.gettz("America/Chicago") # -5:00 - - dates = [ - ( - datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), - datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=old_tz), - ), - ( - datetime.datetime(2010, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), - datetime.datetime(2011, 1, 2, 3, 0, 0, 0, tzinfo=old_tz), - ), - ] - - cal = self.StubCal(dates) - - # Exercise - change the timezone - change_tz(cal, new_tz, dateutil.tz.gettz("UTC")) - - # Test - that the tzs were converted correctly - expected_new_dates = [ - ( - datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), - datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz), - ), - ( - datetime.datetime(2010, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), - datetime.datetime(2011, 1, 1, 21, 0, 0, 0, tzinfo=new_tz), - ), - ] - - for vevent, expected_datepair in zip(cal.vevent_list, expected_new_dates): - self.assertEqual(vevent.dtstart.value, expected_datepair[0]) - self.assertEqual(vevent.dtend.value, expected_datepair[1]) - - def test_change_tz_utc_only(self): - """ - Change any UTC timezones of events in a component to a different - timezone - """ - - # Setup - create a stub vevent list - utc_tz = dateutil.tz.gettz("UTC") # 0:00 - non_utc_tz = dateutil.tz.gettz("America/Santiago") # -4:00 - new_tz = dateutil.tz.gettz("America/Chicago") # -5:00 - - dates = [ - ( - datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=utc_tz), - datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=non_utc_tz), - ) - ] - - cal = self.StubCal(dates) - - # Exercise - change the timezone passing utc_only=True - change_tz(cal, new_tz, dateutil.tz.gettz("UTC"), utc_only=True) - - # Test - that only the utc item has changed - expected_new_dates = [(datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), dates[0][1])] - - for vevent, expected_datepair in zip(cal.vevent_list, expected_new_dates): - self.assertEqual(vevent.dtstart.value, expected_datepair[0]) - self.assertEqual(vevent.dtend.value, expected_datepair[1]) - - def test_change_tz_default(self): - """ - Change the timezones of events in a component to a different - timezone, passing a default timezone that is assumed when the events - don't have one - """ - - # Setup - create a stub vevent list - new_tz = dateutil.tz.gettz("America/Chicago") # -5:00 - - dates = [ - ( - datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=None), - datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=None), - ) - ] - - cal = self.StubCal(dates) - - # Exercise - change the timezone - change_tz(cal, new_tz, dateutil.tz.gettz("UTC")) - - # Test - that the tzs were converted correctly - expected_new_dates = [ - ( - datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), - datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz), - ) - ] - - for vevent, expected_datepair in zip(cal.vevent_list, expected_new_dates): - self.assertEqual(vevent.dtstart.value, expected_datepair[0]) - self.assertEqual(vevent.dtend.value, expected_datepair[1]) - - -class TestCompatibility(unittest.TestCase): - - def test_radicale_0816(self): - ics_str = get_test_file("radicale-0816.ics") - vobjs = base.readComponents(ics_str, allowQP=True) - for vo in vobjs: - self.assertIsNotNone(vo) - return - - def test_radicale_0827(self): - ics_str = get_test_file("radicale-0827.ics") - vobjs = base.readComponents(ics_str, allowQP=True) - for vo in vobjs: - self.assertIsNotNone(vo) - return - - -if __name__ == "__main__": - unittest.main() +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import datetime +import json +import re +import sys +import unittest + +import dateutil +from dateutil.rrule import MONTHLY, WEEKLY, rrule, rruleset +from dateutil.tz import tzutc + +from vobject import base, iCalendar, icalendar +from vobject.base import ContentLine, ParseError +from vobject.base import __behaviorRegistry as BehaviorRegistry +from vobject.base import parseLine, readComponents, textLineToContentLine +from vobject.change_tz import change_tz +from vobject.icalendar import ( + MultiDateBehavior, + PeriodBehavior, + RecurringComponent, + parseDtstart, + stringToPeriod, + stringToTextValues, + timedeltaToString, + utc, +) + +behavior_registry = BehaviorRegistry +two_hours = datetime.timedelta(hours=2) + + +def get_test_file(path): + """ + Helper function to open and read test files. + """ + filepath = f"test_files/{path}" + if sys.version_info[0] < 3: + # On python 2, this library operates on bytes. + f = open(filepath, "r") + else: + # On python 3, it operates on unicode. We need to specify an encoding + # for systems for which the preferred encoding isn't utf-8 (e.g windows) + f = open(filepath, "r", encoding="utf-8") + text = f.read() + f.close() + return text + + +class TestCalendarSerializing(unittest.TestCase): + """ + Test creating an iCalendar file + """ + + max_diff = None + + def test_scratchbuild(self): + """ + CreateCalendar 2.0 format from scratch + """ + test_cal = get_test_file("simple_2_0_test.ics") + cal = base.newFromBehavior("vcalendar", "2.0") + cal.add("vevent") + cal.vevent.add("dtstart").value = datetime.datetime(2006, 5, 9) + cal.vevent.add("description").value = "Test event" + cal.vevent.add("created").value = datetime.datetime( + 2006, 1, 1, 10, tzinfo=dateutil.tz.tzical("test_files/timezones.ics").get("US/Pacific") + ) + cal.vevent.add("uid").value = "Not very random UID" + cal.vevent.add("dtstamp").value = datetime.datetime(2017, 6, 26, 0, tzinfo=tzutc()) + + cal.vevent.add("attendee").value = "mailto:froelich@example.com" + cal.vevent.attendee.params["CN"] = ["Fröhlich"] + + # Note we're normalizing line endings, because no one got time for that. + self.assertEqual(cal.serialize().replace("\r\n", "\n"), test_cal.replace("\r\n", "\n")) + + def test_unicode(self): + """ + Test unicode characters + """ + test_cal = get_test_file("utf8_test.ics") + vevent = base.readOne(test_cal).vevent + vevent2 = base.readOne(vevent.serialize()) + self.assertEqual(str(vevent), str(vevent2)) + + self.assertEqual(vevent.summary.value, "The title こんにちはキティ") + + if sys.version_info[0] < 3: + test_cal = test_cal.decode("utf-8") + vevent = base.readOne(test_cal).vevent + vevent2 = base.readOne(vevent.serialize()) + self.assertEqual(str(vevent), str(vevent2)) + self.assertEqual(vevent.summary.value, "The title こんにちはキティ") + + def test_wrapping(self): + """ + Should support input file with a long text field covering multiple lines + """ + test_journal = get_test_file("journal.ics") + vobj = base.readOne(test_journal) + vjournal = base.readOne(vobj.serialize()) + self.assertTrue("Joe, Lisa, and Bob" in vjournal.description.value) + self.assertTrue("Tuesday.\n2." in vjournal.description.value) + + def test_multiline(self): + """ + Multi-text serialization test + """ + category = base.newFromBehavior("categories") + category.value = ["Random category"] + self.assertEqual(category.serialize().strip(), "CATEGORIES:Random category") + + category.value.append("Other category") + self.assertEqual(category.serialize().strip(), "CATEGORIES:Random category,Other category") + + def test_semicolon_separated(self): + """ + Semi-colon separated multi-text serialization test + """ + request_status = base.newFromBehavior("request-status") + request_status.value = ["5.1", "Service unavailable"] + self.assertEqual(request_status.serialize().strip(), "REQUEST-STATUS:5.1;Service unavailable") + + @staticmethod + def test_unicode_multiline(): + """ + Test multiline unicode characters + """ + cal = iCalendar() + cal.add("method").value = "REQUEST" + cal.add("vevent") + cal.vevent.add("created").value = datetime.datetime.now() + cal.vevent.add("summary").value = "Классное событие" + cal.vevent.add("description").value = ( + "Классное событие Классное событие Классное событие Классное событие " + "Классное событие Классsdssdное событие" + ) + + # json tries to encode as utf-8 and it would break if some chars could not be encoded + json.dumps(cal.serialize()) + + @staticmethod + def test_ical_to_hcal(): + """ + Serializing iCalendar to hCalendar. + + Since Hcalendar is experimental and the behavior doesn't seem to want to load, + This test will have to wait. + + + tzs = dateutil.tz.tzical("test_files/timezones.ics") + cal = base.newFromBehavior('hcalendar') + self.assertEqual( + str(cal.behavior), + "" + ) + cal.add('vevent') + cal.vevent.add('summary').value = "this is a note" + cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator" + cal.vevent.add('dtstart').value = datetime.date(2006,2,27) + cal.vevent.add('location').value = "a place" + cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2) + + event2 = cal.add('vevent') + event2.add('summary').value = "Another one" + event2.add('description').value = "The greatest thing ever!" + event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = tzs.get('US/Pacific')) + event2.add('location').value = "somewhere else" + event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6) + hcal = cal.serialize() + """ + # self.assertEqual( + # str(hcal), + # """ + # + # this is a note: + # Monday, February 27 + # - Tuesday, February 28 + # at a place + # + # + # + # Another one: + # Thursday, December 17, 16:42 + # - Wednesday, December 23, 16:42 + # at somewhere else + #
The greatest thing ever!
+ #
+ # """ + # ) + + +class TestBehaviors(unittest.TestCase): + """ + Test Behaviors + """ + + def test_general_behavior(self): + """ + Tests for behavior registry, getting and creating a behavior. + """ + # Check expected behavior registry. + self.assertEqual( + sorted(behavior_registry.keys()), + [ + "", + "ACTION", + "ADR", + "AVAILABLE", + "BUSYTYPE", + "CALSCALE", + "CATEGORIES", + "CLASS", + "COMMENT", + "COMPLETED", + "CONTACT", + "CREATED", + "DAYLIGHT", + "DESCRIPTION", + "DTEND", + "DTSTAMP", + "DTSTART", + "DUE", + "DURATION", + "EXDATE", + "EXRULE", + "FN", + "FREEBUSY", + "LABEL", + "LAST-MODIFIED", + "LOCATION", + "METHOD", + "N", + "ORG", + "PHOTO", + "PRODID", + "RDATE", + "RECURRENCE-ID", + "RELATED-TO", + "REQUEST-STATUS", + "RESOURCES", + "RRULE", + "STANDARD", + "STATUS", + "SUMMARY", + "TRANSP", + "TRIGGER", + "UID", + "VALARM", + "VAVAILABILITY", + "VCALENDAR", + "VCARD", + "VEVENT", + "VFREEBUSY", + "VJOURNAL", + "VTIMEZONE", + "VTODO", + ], + ) + + # test get_behavior + behavior = base.getBehavior("VCALENDAR") + self.assertEqual(str(behavior), "") + self.assertTrue(behavior.isComponent) + + self.assertEqual(base.getBehavior("invalid_name"), None) + # test for ContentLine (not a component) + non_component_behavior = base.getBehavior("RDATE") + self.assertFalse(non_component_behavior.isComponent) + + def test_MultiDateBehavior(self): + """ + Test MultiDateBehavior + """ + parseRDate = MultiDateBehavior.transformToNative + self.assertEqual( + str(parseRDate(textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904"))), + "", + ) + self.assertEqual( + str( + parseRDate( + textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H") + ) + ), + "", + ) + + def test_periodBehavior(self): + """ + Test PeriodBehavior + """ + line = ContentLine("test", [], "", isNative=True) + line.behavior = PeriodBehavior + line.value = [(datetime.datetime(2006, 2, 16, 10), two_hours)] + + self.assertEqual(line.transformFromNative().value, "20060216T100000/PT2H") + self.assertEqual( + line.transformToNative().value, [(datetime.datetime(2006, 2, 16, 10, 0), datetime.timedelta(0, 7200))] + ) + + line.value.append((datetime.datetime(2006, 5, 16, 10), two_hours)) + + self.assertEqual(line.serialize().strip(), "TEST:20060216T100000/PT2H,20060516T100000/PT2H") + + +class TestVTodo(unittest.TestCase): + """ + VTodo Tests + """ + + def test_vtodo(self): + """ + Test VTodo + """ + vtodo = get_test_file("vtodo.ics") + obj = base.readOne(vtodo) + obj.vtodo.add("completed") + obj.vtodo.completed.value = datetime.datetime(2015, 5, 5, 13, 30) + self.assertEqual(obj.vtodo.completed.serialize()[:23], "COMPLETED:20150505T1330") + obj = base.readOne(obj.serialize()) + self.assertEqual(obj.vtodo.completed.value, datetime.datetime(2015, 5, 5, 13, 30)) + + +class TestVobject(unittest.TestCase): + """ + VObject Tests + """ + + max_diff = None + + @classmethod + def setUpClass(cls): + """ + Method for setting up class fixture before running tests in the class. + Fetches test file. + """ + cls.simple_test_cal = get_test_file("simple_test.ics") + + def test_readComponents(self): + """ + Test if reading components correctly + """ + cal = next(readComponents(self.simple_test_cal)) + + self.assertEqual(str(cal), "]>]>") + self.assertEqual(str(cal.vevent.summary), "") + + def test_parseLine(self): + """ + Test line parsing + """ + self.assertEqual(parseLine("BLAH:"), ("BLAH", [], "", None)) + self.assertEqual( + parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904"), + ("RDATE", [], "VALUE=DATE:19970304,19970504,19970704,19970904", None), + ) + self.assertEqual( + parseLine( + 'DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA' + ), + ( + "DESCRIPTION", + [["ALTREP", "http://www.wiz.org"]], + "The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA", + None, + ), + ) + self.assertEqual( + parseLine("EMAIL;PREF;INTERNET:john@nowhere.com"), + ("EMAIL", [["PREF"], ["INTERNET"]], "john@nowhere.com", None), + ) + self.assertEqual( + parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com'), + ("EMAIL", [["TYPE", "blah", "hah"], ["INTERNET", "DIGI", "DERIDOO"]], "john@nowhere.com", None), + ) + self.assertEqual( + parseLine("item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;"), + ("ADR", [["type", "HOME"], ["type", "pref"]], ";;Reeperbahn 116;Hamburg;;20359;", "item1"), + ) + self.assertRaises(ParseError, parseLine, ":") + + +class TestGeneralFileParsing(unittest.TestCase): + """ + General tests for parsing ics files. + """ + + def test_readOne(self): + """ + Test reading first component of ics + """ + cal = get_test_file("silly_test.ics") + silly = base.readOne(cal) + self.assertEqual( + str(silly), + ", , ]>", + ) + self.assertEqual(str(silly.stuff), "") + + def test_importing(self): + """ + Test importing ics + """ + cal = get_test_file("standard_test.ics") + c = base.readOne(cal, validate=True) + self.assertEqual(str(c.vevent.valarm.trigger), "") + + self.assertEqual(str(c.vevent.dtstart.value), "2002-10-28 14:00:00-08:00") + self.assertTrue(isinstance(c.vevent.dtstart.value, datetime.datetime)) + self.assertEqual(str(c.vevent.dtend.value), "2002-10-28 15:00:00-08:00") + self.assertTrue(isinstance(c.vevent.dtend.value, datetime.datetime)) + self.assertEqual(c.vevent.dtstamp.value, datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc())) + + vevent = c.vevent.transformFromNative() + self.assertEqual(str(vevent.rrule), "") + + def test_bad_stream(self): + """ + Test bad ics stream + """ + cal = get_test_file("badstream.ics") + self.assertRaises(ParseError, base.readOne, cal) + + def test_bad_line(self): + """ + Test bad line in ics file + """ + cal = get_test_file("badline.ics") + self.assertRaises(ParseError, base.readOne, cal) + + newcal = base.readOne(cal, ignoreUnreadable=True) + self.assertEqual(str(newcal.vevent.x_bad_underscore), "") + + def test_parseParams(self): + """ + Test parsing parameters + """ + self.assertEqual(base.parseParams(';ALTREP="http://www.wiz.org"'), [["ALTREP", "http://www.wiz.org"]]) + self.assertEqual( + base.parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR'), + [["ALTREP", "http://www.wiz.org;;", "Blah", "Foo"], ["NEXT", "Nope"], ["BAR"]], + ) + + +class TestVcards(unittest.TestCase): + """ + Test VCards + """ + + @classmethod + def setUpClass(cls): + """ + Method for setting up class fixture before running tests in the class. + Fetches test file. + """ + cls.test_file = get_test_file("vcard_with_groups.ics") + cls.card = base.readOne(cls.test_file) + + def test_vcard_creation(self): + """ + Test creating a vCard + """ + vcard = base.newFromBehavior("vcard", "3.0") + self.assertEqual(str(vcard), "") + + def test_default_behavior(self): + """ + Default behavior test. + """ + card = self.card + self.assertEqual(base.getBehavior("note"), None) + self.assertEqual( + str(card.note.value), "The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line." + ) + + def test_with_groups(self): + """ + vCard groups test + """ + card = self.card + self.assertEqual(str(card.group), "home") + self.assertEqual(str(card.tel.group), "home") + + card.group = card.tel.group = "new" + self.assertEqual(str(card.tel.serialize().strip()), "new.TEL;TYPE=fax,voice,msg:+49 3581 123456") + self.assertEqual(str(card.serialize().splitlines()[0]), "new.BEGIN:VCARD") + + def test_vcard_3_parsing(self): + """ + VCARD 3.0 parse test + """ + test_file = get_test_file("simple_3_0_test.ics") + card = base.readOne(test_file) + # value not rendering correctly? + # self.assertEqual( + # card.adr.value, + # "" + # ) + self.assertEqual(card.org.value, ["University of Novosibirsk", "Department of Octopus Parthenogenesis"]) + + for _ in range(3): + new_card = base.readOne(card.serialize()) + self.assertEqual(new_card.org.value, card.org.value) + card = new_card + + +class TestIcalendar(unittest.TestCase): + """ + Tests for icalendar.py + """ + + max_diff = None + + def test_parseDTStart(self): + """ + Should take a content line and return a datetime object. + """ + self.assertEqual( + parseDtstart(textLineToContentLine("DTSTART:20060509T000000")), datetime.datetime(2006, 5, 9, 0, 0) + ) + + def test_regexes(self): + """ + Test regex patterns + """ + self.assertEqual(re.findall(base.patterns["name"], "12foo-bar:yay"), ["12foo-bar", "yay"]) + self.assertEqual(re.findall(base.patterns["safe_char"], 'a;b"*,cd'), ["a", "b", "*", "c", "d"]) + self.assertEqual(re.findall(base.patterns["qsafe_char"], 'a;b"*,cd'), ["a", ";", "b", "*", ",", "c", "d"]) + self.assertEqual( + re.findall(base.patterns["param_value"], '"quoted";not-quoted;start"after-illegal-quote', re.VERBOSE), + ['"quoted"', "", "not-quoted", "", "start", "", "after-illegal-quote", ""], + ) + match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"') + self.assertEqual(match.group("value"), 'value:;"') + self.assertEqual(match.group("name"), "TEST") + self.assertEqual(match.group("params"), ';ALTREP="http://www.wiz.org"') + + def test_stringToTextValues(self): + """ + Test string lists + """ + self.assertEqual(stringToTextValues(""), [""]) + self.assertEqual(stringToTextValues("abcd,efgh"), ["abcd", "efgh"]) + + def test_stringToPeriod(self): + """ + Test datetime strings + """ + self.assertEqual( + stringToPeriod("19970101T180000Z/19970102T070000Z"), + (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc())), + ) + self.assertEqual( + stringToPeriod("19970101T180000Z/PT1H"), + (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.timedelta(0, 3600)), + ) + + def test_timedeltaToString(self): + """ + Test timedelta strings + """ + self.assertEqual(timedeltaToString(two_hours), "PT2H") + self.assertEqual(timedeltaToString(datetime.timedelta(minutes=20)), "PT20M") + + def test_delta_to_offset(self): + """Test deltaToOffset() function.""" + + # Sydney + delta = datetime.timedelta(hours=10) + self.assertEqual(icalendar.deltaToOffset(delta), "+1000") + + # New York + delta = datetime.timedelta(hours=-5) + self.assertEqual(icalendar.deltaToOffset(delta), "-0500") + + # Adelaide (see https://github.com/py-vobject/vobject/pull/12) + delta = datetime.timedelta(hours=9, minutes=30) + self.assertEqual(icalendar.deltaToOffset(delta), "+0930") + + def test_vtimezone_creation(self): + """ + Test timezones + """ + tzs = dateutil.tz.tzical("test_files/timezones.ics") + pacific = icalendar.TimezoneComponent(tzs.get("US/Pacific")) + self.assertEqual(str(pacific), ">") + santiago = icalendar.TimezoneComponent(tzs.get("Santiago")) + self.assertEqual(str(santiago), ">") + for year in range(2001, 2010): + for month in (2, 9): + dt = datetime.datetime(year, month, 15, tzinfo=tzs.get("Santiago")) + self.assertTrue(dt.replace(tzinfo=tzs.get("Santiago")), dt) + + @staticmethod + def test_timezone_serializing(): + """ + Serializing with timezones test + """ + tzs = dateutil.tz.tzical("test_files/timezones.ics") + pacific = tzs.get("US/Pacific") + cal = base.Component("VCALENDAR") + cal.setBehavior(icalendar.VCalendar2_0) + ev = cal.add("vevent") + ev.add("dtstart").value = datetime.datetime(2005, 10, 12, 9, tzinfo=pacific) + evruleset = rruleset() + evruleset.rrule(rrule(WEEKLY, interval=2, byweekday=[2, 4], until=datetime.datetime(2005, 12, 15, 9))) + evruleset.rrule(rrule(MONTHLY, bymonthday=[-1, -5])) + evruleset.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo=pacific)) + ev.rruleset = evruleset + ev.add("duration").value = datetime.timedelta(hours=1) + + apple = tzs.get("America/Montreal") + ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple) + + def test_pytz_timezone_serializing(self): + """ + Serializing with timezones from pytz test + """ + try: + import pytz + except ImportError: + return self.skipTest("pytz not installed") # NOQA + + # Avoid conflicting cached tzinfo from other tests + def unregister_tzid(tzid): + """Clear tzid from icalendar TZID registry""" + if icalendar.getTzid(tzid, False): + icalendar.registerTzid(tzid, None) + + unregister_tzid("US/Eastern") + eastern = pytz.timezone("US/Eastern") + cal = base.Component("VCALENDAR") + cal.setBehavior(icalendar.VCalendar2_0) + ev = cal.add("vevent") + ev.add("dtstart").value = eastern.localize(datetime.datetime(2008, 10, 12, 9)) + serialized = cal.serialize() + + expected_vtimezone = get_test_file("tz_us_eastern.ics") + self.assertIn(expected_vtimezone.replace("\r\n", "\n"), serialized.replace("\r\n", "\n")) + + # Exhaustively test all zones (just looking for no errors) + for tzname in pytz.all_timezones: + unregister_tzid(tzname) + tz = icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname)) + tz.serialize() + + def test_freeBusy(self): + """ + Test freebusy components + """ + test_cal = get_test_file("freebusy.ics") + + vfb = base.newFromBehavior("VFREEBUSY") + vfb.add("uid").value = "test" + vfb.add("dtstamp").value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + vfb.add("dtstart").value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) + vfb.add("dtend").value = vfb.dtstart.value + two_hours + vfb.add("freebusy").value = [(vfb.dtstart.value, two_hours / 2)] + vfb.add("freebusy").value = [(vfb.dtstart.value, vfb.dtend.value)] + + self.assertEqual(vfb.serialize().replace("\r\n", "\n"), test_cal.replace("\r\n", "\n")) + + def test_availablity(self): + """ + Test availability components + """ + test_cal = get_test_file("availablity.ics") + + vcal = base.newFromBehavior("VAVAILABILITY") + vcal.add("uid").value = "test" + vcal.add("dtstamp").value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + vcal.add("dtstart").value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc) + vcal.add("dtend").value = datetime.datetime(2006, 2, 17, 0, tzinfo=utc) + vcal.add("busytype").value = "BUSY" + + av = base.newFromBehavior("AVAILABLE") + av.add("uid").value = "test1" + av.add("dtstamp").value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc) + av.add("dtstart").value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc) + av.add("dtend").value = datetime.datetime(2006, 2, 16, 12, tzinfo=utc) + av.add("summary").value = "Available in the morning" + + vcal.add(av) + + self.assertEqual(vcal.serialize().replace("\r\n", "\n"), test_cal.replace("\r\n", "\n")) + + def test_recurrence(self): + """ + Ensure date valued UNTILs in rrules are in a reasonable timezone, + and include that day (12/28 in this test) + """ + test_file = get_test_file("recurrence.ics") + cal = base.readOne(test_file) + dates = list(cal.vevent.getrruleset()) + self.assertEqual(dates[0], datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc())) + self.assertEqual(dates[1], datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc())) + self.assertEqual(dates[-1], datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc())) + + def test_recurring_component(self): + """ + Test recurring events + """ + vevent = RecurringComponent(name="VEVENT") + + # init + self.assertTrue(vevent.isNative) + + # rruleset should be None at this point. + # No rules have been passed or created. + self.assertEqual(vevent.rruleset, None) + + # Now add start and rule for recurring event + vevent.add("dtstart").value = datetime.datetime(2005, 1, 19, 9) + vevent.add("rrule").value = "FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH" + self.assertEqual( + list(vevent.rruleset), [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)] + ) + self.assertEqual( + list(vevent.getrruleset(addRDate=True)), + [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)], + ) + + # Also note that dateutil will expand all-day events (datetime.date values) + # to datetime.datetime value with time 0 and no timezone. + vevent.dtstart.value = datetime.date(2005, 3, 18) + self.assertEqual( + list(vevent.rruleset), [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)] + ) + self.assertEqual( + list(vevent.getrruleset(True)), [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)] + ) + + def test_recurrence_without_tz(self): + """ + Test recurring vevent missing any time zone definitions. + """ + test_file = get_test_file("recurrence-without-tz.ics") + cal = base.readOne(test_file) + dates = list(cal.vevent.getrruleset()) + self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) + self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) + self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) + + def test_recurrence_offset_naive(self): + """ + Ensure recurring vevent missing some time zone definitions is + parsing. See isseu #75. + """ + test_file = get_test_file("recurrence-offset-naive.ics") + cal = base.readOne(test_file) + dates = list(cal.vevent.getrruleset()) + self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0)) + self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0)) + self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0)) + + +class TestChangeTZ(unittest.TestCase): + """ + Tests for change_tz.change_tz + """ + + class StubCal(object): + class StubEvent(object): + class Node(object): + def __init__(self, value): + self.value = value + + def __init__(self, dtstart, dtend): + self.dtstart = self.Node(dtstart) + self.dtend = self.Node(dtend) + + def __init__(self, dates): + """ + dates is a list of tuples (dtstart, dtend) + """ + self.vevent_list = [self.StubEvent(*d) for d in dates] + + def test_change_tz(self): + """ + Change the timezones of events in a component to a different + timezone + """ + + # Setup - create a stub vevent list + old_tz = dateutil.tz.gettz("UTC") # 0:00 + new_tz = dateutil.tz.gettz("America/Chicago") # -5:00 + + dates = [ + ( + datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), + datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=old_tz), + ), + ( + datetime.datetime(2010, 12, 31, 23, 59, 59, 0, tzinfo=old_tz), + datetime.datetime(2011, 1, 2, 3, 0, 0, 0, tzinfo=old_tz), + ), + ] + + cal = self.StubCal(dates) + + # Exercise - change the timezone + change_tz(cal, new_tz, dateutil.tz.gettz("UTC")) + + # Test - that the tzs were converted correctly + expected_new_dates = [ + ( + datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz), + ), + ( + datetime.datetime(2010, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + datetime.datetime(2011, 1, 1, 21, 0, 0, 0, tzinfo=new_tz), + ), + ] + + for vevent, expected_datepair in zip(cal.vevent_list, expected_new_dates): + self.assertEqual(vevent.dtstart.value, expected_datepair[0]) + self.assertEqual(vevent.dtend.value, expected_datepair[1]) + + def test_change_tz_utc_only(self): + """ + Change any UTC timezones of events in a component to a different + timezone + """ + + # Setup - create a stub vevent list + utc_tz = dateutil.tz.gettz("UTC") # 0:00 + non_utc_tz = dateutil.tz.gettz("America/Santiago") # -4:00 + new_tz = dateutil.tz.gettz("America/Chicago") # -5:00 + + dates = [ + ( + datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=utc_tz), + datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=non_utc_tz), + ) + ] + + cal = self.StubCal(dates) + + # Exercise - change the timezone passing utc_only=True + change_tz(cal, new_tz, dateutil.tz.gettz("UTC"), utc_only=True) + + # Test - that only the utc item has changed + expected_new_dates = [(datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), dates[0][1])] + + for vevent, expected_datepair in zip(cal.vevent_list, expected_new_dates): + self.assertEqual(vevent.dtstart.value, expected_datepair[0]) + self.assertEqual(vevent.dtend.value, expected_datepair[1]) + + def test_change_tz_default(self): + """ + Change the timezones of events in a component to a different + timezone, passing a default timezone that is assumed when the events + don't have one + """ + + # Setup - create a stub vevent list + new_tz = dateutil.tz.gettz("America/Chicago") # -5:00 + + dates = [ + ( + datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=None), + datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=None), + ) + ] + + cal = self.StubCal(dates) + + # Exercise - change the timezone + change_tz(cal, new_tz, dateutil.tz.gettz("UTC")) + + # Test - that the tzs were converted correctly + expected_new_dates = [ + ( + datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz), + datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz), + ) + ] + + for vevent, expected_datepair in zip(cal.vevent_list, expected_new_dates): + self.assertEqual(vevent.dtstart.value, expected_datepair[0]) + self.assertEqual(vevent.dtend.value, expected_datepair[1]) + + +class TestCompatibility(unittest.TestCase): + + def test_radicale_0816(self): + ics_str = get_test_file("radicale-0816.ics") + vobjs = base.readComponents(ics_str, allowQP=True) + for vo in vobjs: + self.assertIsNotNone(vo) + return + + def test_radicale_0827(self): + ics_str = get_test_file("radicale-0827.ics") + vobjs = base.readComponents(ics_str, allowQP=True) + for vo in vobjs: + self.assertIsNotNone(vo) + return + + +if __name__ == "__main__": + unittest.main() diff --git a/vobject/hcalendar.py b/vobject/hcalendar.py index 88abc11..67ad83f 100644 --- a/vobject/hcalendar.py +++ b/vobject/hcalendar.py @@ -1,4 +1,4 @@ -""" +r""" hCalendar: A microformat for serializing iCalendar data (http://microformats.org/wiki/hcalendar) @@ -12,7 +12,7 @@ DTSTART:20051005 DTEND:20051008 SUMMARY:Web 2.0 Conference -LOCATION:Argent Hotel, San Francisco, CA +LOCATION:Argent Hotel\, San Francisco\, CA END:VEVENT END:VCALENDAR