diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9cf5b20 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] " +labels: '' +assignees: '' + +--- + + +## Describe the bug + + +## To Reproduce + + +``` +import icalendar +... +``` +Output: + +``` + +``` + +## Expected behavior** + + +## Environment + + - [ ] OS: + - [ ] Python version: + - [ ] `icalendar` version: + +## Additional context + +- [ ] I tested it with the latest version `pip3 install https://github.com/collective/icalendar.git` +- [ ] I attached the ICS source file or there is no ICS source file diff --git a/.github/ISSUE_TEMPLATE/empty-issue.md b/.github/ISSUE_TEMPLATE/empty-issue.md new file mode 100644 index 0000000..abe2217 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/empty-issue.md @@ -0,0 +1,18 @@ +--- +name: empty issue +about: This is an unstructured issue template to use with issues that are not described, + yet. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 83bb814..0190cb8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,14 +2,17 @@ name: tests on: push: - branches: [ main ] + branches: + - main + tags: + - v* pull_request: schedule: - cron: '14 7 * * 0' # run once a week on Sunday workflow_dispatch: jobs: - build: + run-tests: strategy: matrix: config: @@ -20,6 +23,7 @@ jobs: - ["3.10", "py310"] - ["pypy-3.9", "pypy3"] - ["3.10", "docs"] + - ["3.10", "plone"] - ["3.11.0-rc.1", "py311"] runs-on: ubuntu-latest @@ -50,3 +54,61 @@ jobs: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + deploy-tag-to-pypi: + # only deploy on tags, see https://stackoverflow.com/a/58478262/1320237 + if: startsWith(github.ref, 'refs/tags/v') + needs: + - run-tests + runs-on: ubuntu-latest + # This environment stores the TWINE_USERNAME and TWINE_PASSWORD + # see https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment + environment: + name: PyPI + url: https://pypi.org/project/icalendar/ + # after using the environment, we need to make the secrets available + # see https://docs.github.com/en/actions/security-guides/encrypted-secrets#example-using-bash + env: + TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.9" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install wheel twine + - name: Check the tag + run: | + PACKAGE_VERSION=`python setup.py --version` + TAG_NAME=v$PACKAGE_VERSION + echo "Package version $PACKAGE_VERSION with possible tag name $TAG_NAME on $GITHUB_REF_NAME" + # test that the tag represents the version + # see https://docs.github.com/en/actions/learn-github-actions/environment-variables + if [ "$TAG_NAME" != "$GITHUB_REF_NAME" ]; then + echo "ERROR: This tag is for the wrong version. Got \"$GITHUB_REF_NAME\" expected \"$TAG_NAME\"." + exit 1 + fi + - name: remove old files + run: rm -rf dist/* + - name: build distribution files + run: python setup.py bdist_wheel sdist + - name: deploy to pypi + run: | + # You will have to set the variables TWINE_USERNAME and TWINE_PASSWORD + # You can use a token specific to your project by setting the user name to + # __token__ and the password to the token given to you by the PyPI project. + # sources: + # - https://shambu2k.hashnode.dev/gitlab-to-pypi + # - http://blog.octomy.org/2020/11/deploying-python-pacakges-to-pypi-using.html?m=1 + if [ -z "$TWINE_USERNAME" ]; then + echo "WARNING: TWINE_USERNAME not set!" + fi + if [ -z "$TWINE_PASSWORD" ]; then + echo "WARNING: TWINE_PASSWORD not set!" + fi + twine check dist/* + twine upload dist/* diff --git a/CHANGES.rst b/CHANGES.rst index 697a7f8..cb8b181 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,21 +1,42 @@ Changelog ========= +5.0.1 (unreleased) +------------------ -5.0.0a2 (unreleased) --------------------- +Minor changes: + +- fixed setuptools deprecation warnings [mgorny] + +Breaking changes: + +- ... + +New features: + +- ... + +Bug fixes: + +- ... + +5.0.0 (2022-10-17) +------------------ Minor changes: - removed deprecated test checks [tuergeist] - Fix: cli does not support DURATION #354 [mamico] - Add changelog and contributing to readthedocs documentation #428 [peleccom] +- fixed small typos #323 [rohnsha0] +- unittest to parametrized pytest refactoring [jacadzaca] Breaking changes: - Require Python 3.7 as minimum Python version. [maurits] [niccokunzmann] - icalenar now takes a ics file directly as an input - icalendar's output is different +- Drop Support for Python 3.6. Versions 3.7 - 3.11 are supported and tested. New features: @@ -36,6 +57,8 @@ Bug fixes: Ref: #338 Fixes: #335 [tobixen] +- add ``__eq__`` to ``icalendar.prop.vDDDTypes`` #391 [jacadzaca] +- Refactor deprecated unittest aliases for Python 3.11 compatibility #330 [tirkarthi] 5.0.0a1 (2022-07-11) -------------------- diff --git a/docs/credits.rst b/docs/credits.rst index 9722fde..22952c4 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -63,6 +63,7 @@ icalendar contributors - jacadzaca - Mauro Amico - Alexander Pitkin +- Michał Górny Find out who contributed:: diff --git a/docs/maintenance.rst b/docs/maintenance.rst index 6697427..6fb7460 100644 --- a/docs/maintenance.rst +++ b/docs/maintenance.rst @@ -25,6 +25,9 @@ Maintainers need this: - ``Maintainer`` access to the `Read The Docs project `_. This can be given by existing maintainers listed on the project's page. TODO: link to the settings +- ``PyPI environment access for GitHub Actions`` grant new releases from tags. + This access can be granted in `Settings → Environments → PyPI `__ + by adding the GitHub username to the list of "Required Reviewers". Contributors @@ -42,37 +45,79 @@ Nobody should merge their own pull requests. If you like to review or merge pull requests of other people and you have shown that you know how to contribute, you can ask for becoming a contributor or a maintainer asks you if you would like to become one. - New Releases ------------ -This explains how to create a new release on PyPI. -You will need write access to the `PyPI project`_. +This explains how to create a new release on `PyPI `_. -1. Check that the ``CHANGES.rst`` is up to date with the latest merges and the version you want to release is correctly named. +Since contributors and maintainers have write access the the repository, they can start the release process. +However, only people with ``PyPI environment access for GitHub Actions`` can approve an automated release to PyPI. + + +1. Check that the ``CHANGES.rst`` is up to date with the `latest merged pull requests `__ + and the version you want to release is correctly named. 2. Change the ``__version__`` variable in - the ``src/icalendar/__init__.py`` file and - in the ``docs/usage.rst`` file (look for ``icalendar.__version__``) -3. Create a commit on the ``master`` branch to release this version. +3. Create a commit on the ``release-5.0.0`` branch (or equivalent) to release this version. - .. code-block:: bash + .. code-block:: bash - git checkout master - git commit -am"version 5.0.0a2" -4. Push the commit and see if the `CI-tests `__ are running on it. + git checkout master + git pull + git checkout -b release master + git add CHANGES.rst src/icalendar/__init__.py + git commit -m"version 5.0.0" - .. code-block:: bash +4. Push the commit and `create a pull request `__ + Here is an `example pull request #457 `__. - git push -5. Create a tag for the release and see if the `CI-tests `__ are running. + .. code-block:: bash - .. code-block:: bash + git push -u origin release-5.0.0 - git tag v5.0.0a2 - git push origin v5.0.0a2 -6. TODO: how to release new version to PyPI. +5. See if the `CI-tests `_ are running on the pull request. + If they are not running, no new release can be issued. + If the tests are running, merge the pull request. +6. Create a tag for the release and see if the `CI-tests`_ are running. + .. code-block:: bash + + git checkout master + git pull + git tag v5.0.0 + git push upstream v5.0.0 # could be origin or whatever reference + +7. Once the tag is pushed and its `CI-tests`_ are passing, maintainers will get an e-mail:: + + Subject: Deployment review in collective/icalendar + + tests: PyPI is waiting for your review + +8. If the release is approved by a maintainer. It will be pushed to `PyPI`_. + If that happens, notify the issues that were fixed about this release. +9. Copy this to the start of ``CHANGES.rst`` and create a new commit with this on the ``master`` branch:: + + 5.0.1 (unreleased) + ------------------ + + Minor changes: + + - ... + + Breaking changes: + + - ... + + New features: + + - ... + + Bug fixes: + + - ... + Links ----- @@ -82,6 +127,7 @@ This section contains useful links for maintainers and contributors: - `Future of icalendar, looking for maintainer #360 `__ - `Team icalendar-admin `__ - `Team icalendar-contributor `__ +- `Comment on the Plone tests running with icalendar `__ diff --git a/setup.cfg b/setup.cfg index 01fe2a4..a19032d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ create-wheel = yes universal = 1 [metadata] -license_file = LICENSE.rst +license_files = LICENSE.rst [tool:pytest] norecursedirs = .* env* docs *.egg src/icalendar/tests/hypothesis diff --git a/setup.py b/setup.py index 93b3778..26f44fa 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ setuptools.setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], @@ -51,7 +52,7 @@ setuptools.setup( author_email='plone-developers@lists.sourceforge.net', url='https://github.com/collective/icalendar', license='BSD', - packages=setuptools.find_packages('src'), + packages=setuptools.find_namespace_packages('src'), package_dir={'': 'src'}, include_package_data=True, zip_safe=False, diff --git a/src/icalendar/__init__.py b/src/icalendar/__init__.py index 2c5f2fc..5bc8daf 100644 --- a/src/icalendar/__init__.py +++ b/src/icalendar/__init__.py @@ -1,4 +1,4 @@ -__version__ = '5.0.0a2.dev0' +__version__ = '5.0.0' from icalendar.cal import ( Calendar, diff --git a/src/icalendar/tests/calendars/broken_ical.ics b/src/icalendar/tests/calendars/broken_ical.ics new file mode 100644 index 0000000..71604a2 --- /dev/null +++ b/src/icalendar/tests/calendars/broken_ical.ics @@ -0,0 +1,7 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +SUMMARY:An Event with too many semicolons +DTSTART;;VALUE=DATE-TIME:20140409T093000 +UID:abc +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/src/icalendar/tests/calendars/issue_156_RDATE_with_PERIOD_TZID_khal.ics b/src/icalendar/tests/calendars/issue_156_RDATE_with_PERIOD_TZID_khal.ics new file mode 100644 index 0000000..9d62893 --- /dev/null +++ b/src/icalendar/tests/calendars/issue_156_RDATE_with_PERIOD_TZID_khal.ics @@ -0,0 +1,25 @@ +BEGIN:VCALENDAR +X-SOURCE-URL:https://github.com/pimutils/khal/issues/152#issuecomment-387410353 +VERSION:2.0 +PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN +BEGIN:VEVENT +SUMMARY:Event +DTSTART;TZID=America/Chicago;VALUE=DATE-TIME:20180327T080000 +DTEND;TZID=America/Chicago;VALUE=DATE-TIME:20180327T090000 +DTSTAMP:20180323T200333Z +RECURRENCE-ID;RANGE=THISANDFUTURE:20180327T130000Z +SEQUENCE:10 +RDATE;TZID="Central Standard Time";VALUE=PERIOD:20180327T080000/20180327T0 + 90000,20180403T080000/20180403T090000,20180410T080000/20180410T090000,2018 + 0417T080000/20180417T090000,20180424T080000/20180424T090000,20180501T08000 + 0/20180501T090000,20180508T080000/20180508T090000,20180515T080000/20180515 + T090000,20180522T080000/20180522T090000,20180529T080000/20180529T090000,20 + 180605T080000/20180605T090000,20180612T080000/20180612T090000,20180619T080 + 000/20180619T090000,20180626T080000/20180626T090000,20180703T080000/201807 + 03T090000,20180710T080000/20180710T090000,20180717T080000/20180717T090000, + 20180724T080000/20180724T090000,20180731T080000/20180731T090000 +ATTENDEE;CN="XYZ";PARTSTAT=ACCEPTED;ROLE=CHAIR;RSVP= + FALSE:mailto:xyz@xyz.com +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/issue_156_RDATE_with_PERIOD_TZID_khal_2.ics b/src/icalendar/tests/calendars/issue_156_RDATE_with_PERIOD_TZID_khal_2.ics new file mode 100644 index 0000000..9c5288a --- /dev/null +++ b/src/icalendar/tests/calendars/issue_156_RDATE_with_PERIOD_TZID_khal_2.ics @@ -0,0 +1,55 @@ +BEGIN:VCALENDAR +X-SOURCE-URL:https://github.com/pimutils/khal/issues/152#issuecomment-933635248 +VERSION:2.0 +PRODID:-//PIMUTILS.ORG//NONSGML khal / icalendar //EN +BEGIN:VTIMEZONE +TZID:Western/Central Europe +BEGIN:STANDARD +DTSTART:19501029T020000 +RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19500326T020000 +RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +SUMMARY:(omitted) +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 +DTSTAMP:20211004T150245Z +UID:BF5109494E67AAE20025875100566D31-Lotus_Notes_Generated +RECURRENCE-ID;RANGE=THISANDFUTURE:20211101T150000Z +SEQUENCE:0 +RDATE;TZID="Western/Central Europe";VALUE=PERIOD:20211101T160000/20211101T + 163000,20211206T160000/20211206T163000,20220103T160000/20220103T163000,202 + 20207T160000/20220207T163000 +ATTENDEE;CN="(omitted)";PARTSTAT=ACCEPTED;ROLE=CHAIR;RSVP=FAL + SE:mailto:omitted@example.com +CLASS:PUBLIC +TRANSP:OPAQUE +X-LOTUS-APPTTYPE:3 +X-LOTUS-AUDIOVIDEOFLAGS:0 +X-LOTUS-BROADCAST:FALSE +X-LOTUS-CHANGE-INST-DATES:20211101T150000Z\,20211206T150000Z\,20220103T150 + 000Z\,20220207T150000Z +X-LOTUS-CHILD-UID:567EFBAF6CBD07FC0025875100566D3B +X-LOTUS-INITIAL-RDATES:20211101T150000Z\,20211206T150000Z\,20220103T150000 + Z\,20220207T150000Z +X-LOTUS-LASTALL-RDATES;TZID="Western/Central Europe":20211101T160000\,2021 + 1206T160000\,20220103T160000\,20220207T160000 +X-LOTUS-NOTESVERSION:2 +X-LOTUS-NOTICETYPE:I +X-LOTUS-RECURID;RANGE=THISANDFUTURE:20211101T150000Z +X-LOTUS-UPDATE-SEQ:2 +X-LOTUS-UPDATE-WISL:$W:1\;$O:1\;$M:1\;RequiredAttendees:1\;INetRequiredNam + es:1\;AltRequiredNames:1\;StorageRequiredNames:1\;OptionalAttendees:1\;INe + tOptionalNames:1\;AltOptionalNames:1\;StorageOptionalNames:1\;ApptUNIDURL: + 1\;STUnyteConferenceURL:1\;STUnyteConferenceID:1\;SametimeType:1\;WhiteBoa + rdContent:1\;STRoomName:1\;$S:2\;$B:2\;$L:2\;$E:2\;$R:2 +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/issue_237_fail_to_parse_timezone_with_non_ascii_tzid.ics b/src/icalendar/tests/calendars/issue_237_fail_to_parse_timezone_with_non_ascii_tzid.ics new file mode 100644 index 0000000..9b2e8f6 --- /dev/null +++ b/src/icalendar/tests/calendars/issue_237_fail_to_parse_timezone_with_non_ascii_tzid.ics @@ -0,0 +1,23 @@ +BEGIN:VCALENDAR +BEGIN:VTIMEZONE +TZID:(UTC-03:00) Brasília +BEGIN:STANDARD +TZNAME:Brasília standard +DTSTART:16010101T235959 +TZOFFSETFROM:-0200 +TZOFFSETTO:-0300 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=3SA;BYMONTH=2 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:Brasília daylight +DTSTART:16010101T235959 +TZOFFSETFROM:-0300 +TZOFFSETTO:-0200 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SA;BYMONTH=10 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID="(UTC-03:00) Brasília":20170511T133000 +DTEND;TZID="(UTC-03:00) Brasília":20170511T140000 +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/multiple.ics b/src/icalendar/tests/calendars/multiple_calendar_components.ics similarity index 100% rename from src/icalendar/tests/multiple.ics rename to src/icalendar/tests/calendars/multiple_calendar_components.ics diff --git a/src/icalendar/tests/x_location.ics b/src/icalendar/tests/calendars/x_location.ics similarity index 100% rename from src/icalendar/tests/x_location.ics rename to src/icalendar/tests/calendars/x_location.ics diff --git a/src/icalendar/tests/conftest.py b/src/icalendar/tests/conftest.py index efc0b07..0b1296b 100644 --- a/src/icalendar/tests/conftest.py +++ b/src/icalendar/tests/conftest.py @@ -1,5 +1,4 @@ import os -import logging import pytest import icalendar import pytz @@ -10,7 +9,6 @@ try: except ModuleNotFoundError: from backports import zoneinfo - class DataSource: '''A collection of parsed ICS elements (e.g calendars, timezones, events)''' def __init__(self, data_source_folder, parser): @@ -24,7 +22,8 @@ class DataSource: with open(source_path, 'rb') as f: raw_ics = f.read() source = self._parser(raw_ics) - source.raw_ics = raw_ics + if not isinstance(source, list): + source.raw_ics = raw_ics self.__dict__[attribute] = source return source @@ -34,10 +33,19 @@ class DataSource: def __repr__(self): return repr(self.__dict__) + @property + def multiple(self): + """Return a list of all components parsed.""" + return self.__class__(self._data_source_folder, lambda data: self._parser(data, multiple=True)) + HERE = os.path.dirname(__file__) +CALENDARS_FOLDER = os.path.join(HERE, 'calendars') TIMEZONES_FOLDER = os.path.join(HERE, 'timezones') EVENTS_FOLDER = os.path.join(HERE, 'events') -CALENDARS_FOLDER = os.path.join(HERE, 'calendars') + +@pytest.fixture +def calendars(): + return DataSource(CALENDARS_FOLDER, icalendar.Calendar.from_ical) @pytest.fixture def timezones(): @@ -56,6 +64,10 @@ def events(): def utc(request): return request.param -@pytest.fixture -def calendars(): - return DataSource(CALENDARS_FOLDER, icalendar.Calendar.from_ical) +@pytest.fixture(params=[ + lambda dt, tzname: pytz.timezone(tzname).localize(dt), + lambda dt, tzname: dt.replace(tzinfo=tz.gettz(tzname)), + lambda dt, tzname: dt.replace(tzinfo=zoneinfo.ZoneInfo(tzname)) +]) +def in_timezone(request): + return request.param diff --git a/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD.ics b/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD.ics new file mode 100644 index 0000000..398859c --- /dev/null +++ b/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD.ics @@ -0,0 +1,7 @@ +BEGIN:VEVENT +SUMMARY:RDATE period +DTSTART:19961230T020000Z +DTEND:19961230T060000Z +UID:rdate_period +RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z +END:VEVENT diff --git a/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD_list.ics b/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD_list.ics new file mode 100644 index 0000000..3202e85 --- /dev/null +++ b/src/icalendar/tests/events/issue_156_RDATE_with_PERIOD_list.ics @@ -0,0 +1,8 @@ +BEGIN:VEVENT +SUMMARY:RDATE period +DTSTART:19961230T020000Z +DTEND:19961230T060000Z +UID:rdate_period +RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,19970109T180000Z/PT5H + 30M +END:VEVENT diff --git a/src/icalendar/tests/issue_53_parsing_failure.ics b/src/icalendar/tests/issue_53_parsing_failure.ics deleted file mode 100644 index 1275db6..0000000 --- a/src/icalendar/tests/issue_53_parsing_failure.ics +++ /dev/null @@ -1,78 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Meetup//RemoteApi//EN -CALSCALE:GREGORIAN -METHOD:PUBLISH -X-ORIGINAL-URL:http://www.meetup.com/DevOpsDC/events/ical/DevOpsDC/ -X-WR-CALNAME:Events - DevOpsDC -BEGIN:VTIMEZONE -TZID:America/New_York -TZURL:http://tzurl.org/zoneinfo-outlook/America/New_York -X-LIC-LOCATION:America/New_York -BEGIN:DAYLIGHT -TZOFFSETFROM:-0500 -TZOFFSETTO:-0400 -TZNAME:EDT -DTSTART:19700308T020000 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU -END:DAYLIGHT -BEGIN:STANDARD -TZOFFSETFROM:-0400 -TZOFFSETTO:-0500 -TZNAME:EST -DTSTART:19701101T020000 -RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU -END:STANDARD -END:VTIMEZONE -BEGIN:VEVENT -DTSTAMP:20120605T003759Z -DTSTART;TZID=America/New_York:20120712T183000 -DTEND;TZID=America/New_York:20120712T213000 -STATUS:CONFIRMED -SUMMARY:DevOps DC Meetup -DESCRIPTION:DevOpsDC\nThursday\, July 12 at 6:30 PM\n\nThis will be a joi - nt meetup / hack night with the DC jQuery Users Group. The idea behind - the hack night: Small teams consisting of at least 1 member...\n\nDeta - ils: http://www.meetup.com/DevOpsDC/events/47635522/ -CLASS:PUBLIC -CREATED:20120111T120339Z -GEO:38.90;-77.01 -LOCATION:Fathom Creative\, Inc. (1333 14th Street Northwest\, Washington - D.C.\, DC 20005) -URL:http://www.meetup.com/DevOpsDC/events/47635522/ -LAST-MODIFIED:20120522T174406Z -UID:event_qtkfrcyqkbnb@meetup.com -END:VEVENT -BEGIN:VEVENT -DTSTAMP:20120605T003759Z -DTSTART;TZID=America/New_York:20120911T183000 -DTEND;TZID=America/New_York:20120911T213000 -STATUS:CONFIRMED -SUMMARY:DevOps DC Meetup -DESCRIPTION:DevOpsDC\nTuesday\, September 11 at 6:30 PM\n\n \n\nDetails: - http://www.meetup.com/DevOpsDC/events/47635532/ -CLASS:PUBLIC -CREATED:20120111T120352Z -GEO:38.90;-77.01 -LOCATION:CustomInk\, LLC (7902 Westpark Drive\, McLean\, VA 22102) -URL:http://www.meetup.com/DevOpsDC/events/47635532/ -LAST-MODIFIED:20120316T202210Z -UID:event_qtkfrcyqmbpb@meetup.com -END:VEVENT -BEGIN:VEVENT -DTSTAMP:20120605T003759Z -DTSTART;TZID=America/New_York:20121113T183000 -DTEND;TZID=America/New_York:20121113T213000 -STATUS:CONFIRMED -SUMMARY:DevOps DC Meetup -DESCRIPTION:DevOpsDC\nTuesday\, November 13 at 6:30 PM\n\n \n\nDetails: h - ttp://www.meetup.com/DevOpsDC/events/47635552/ -CLASS:PUBLIC -CREATED:20120111T120402Z -GEO:38.90;-77.01 -LOCATION:CustomInk\, LLC (7902 Westpark Drive\, McLean\, VA 22102) -URL:http://www.meetup.com/DevOpsDC/events/47635552/ -LAST-MODIFIED:20120316T202210Z -UID:event_qtkfrcyqpbrb@meetup.com -END:VEVENT -END:VCALENDAR diff --git a/src/icalendar/tests/test_encoding.py b/src/icalendar/tests/test_encoding.py index 7822b5e..18a3900 100644 --- a/src/icalendar/tests/test_encoding.py +++ b/src/icalendar/tests/test_encoding.py @@ -1,4 +1,5 @@ import pytest +import datetime @pytest.mark.parametrize('field, expected_value', [ ('PRODID', '-//Plönë.org//NONSGML plone.app.event//EN'), @@ -27,3 +28,41 @@ def test_events_parameter_unicoded(events): https://github.com/collective/icalendar/issues/101 ''' assert events.issue_101_icalendar_chokes_on_umlauts_in_organizer['ORGANIZER'].params['CN'] == 'acme, ädmin' + +def test_parses_event_with_non_ascii_tzid_issue_237(calendars, in_timezone): + """Issue #237 - Fail to parse timezone with non-ascii TZID + see https://github.com/collective/icalendar/issues/237 + """ + start = calendars.issue_237_fail_to_parse_timezone_with_non_ascii_tzid.walk('VEVENT')[0].decoded('DTSTART') + expected = in_timezone(datetime.datetime(2017, 5, 11, 13, 30), 'America/Sao_Paulo') + assert not calendars.issue_237_fail_to_parse_timezone_with_non_ascii_tzid.errors + assert start == expected + +def test_parses_timezone_with_non_ascii_tzid_issue_237(timezones): + """Issue #237 - Fail to parse timezone with non-ascii TZID + see https://github.com/collective/icalendar/issues/237 + """ + assert timezones.issue_237_brazilia_standard['tzid'] == '(UTC-03:00) Brasília' + +@pytest.mark.parametrize('timezone_name', ['standard', 'daylight']) +def test_parses_timezone_with_non_ascii_tzname_issue_273(timezones, timezone_name): + """Issue #237 - Fail to parse timezone with non-ascii TZID + see https://github.com/collective/icalendar/issues/237 + """ + assert timezones.issue_237_brazilia_standard.walk(timezone_name)[0]['TZNAME'] == f'Brasília {timezone_name}' + +def test_broken_property(calendars): + """ + Test if error messages are encoded properly. + """ + for event in calendars.broken_ical.walk('vevent'): + assert len(event.errors) == 1, 'Not the right amount of errors.' + error = event.errors[0][1] + assert error.startswith('Content line could not be parsed into parts') + +def test_apple_xlocation(calendars): + """ + Test if we support base64 encoded binary data in parameter values. + """ + for event in calendars.x_location.walk('vevent'): + assert len(event.errors) == 0, 'Got too many errors' diff --git a/src/icalendar/tests/test_fixed_issues.py b/src/icalendar/tests/test_fixed_issues.py index cd66999..af84ce0 100644 --- a/src/icalendar/tests/test_fixed_issues.py +++ b/src/icalendar/tests/test_fixed_issues.py @@ -42,47 +42,3 @@ class TestIssues(unittest.TestCase): event.to_ical(), icalendar.Event.from_ical(event.to_ical()).to_ical() ) - - def test_issue_237(self): - """Issue #237 - Fail to parse timezone with non-ascii TZID""" - - ical_str = ['BEGIN:VCALENDAR', - 'BEGIN:VTIMEZONE', - 'TZID:(UTC-03:00) Brasília', - 'BEGIN:STANDARD', - 'TZNAME:Brasília standard', - 'DTSTART:16010101T235959', - 'TZOFFSETFROM:-0200', - 'TZOFFSETTO:-0300', - 'RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=3SA;BYMONTH=2', - 'END:STANDARD', - 'BEGIN:DAYLIGHT', - 'TZNAME:Brasília daylight', - 'DTSTART:16010101T235959', - 'TZOFFSETFROM:-0300', - 'TZOFFSETTO:-0200', - 'RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SA;BYMONTH=10', - 'END:DAYLIGHT', - 'END:VTIMEZONE', - 'BEGIN:VEVENT', - 'DTSTART;TZID=\"(UTC-03:00) Brasília\":20170511T133000', - 'DTEND;TZID=\"(UTC-03:00) Brasília\":20170511T140000', - 'END:VEVENT', - 'END:VCALENDAR', - ] - - cal = icalendar.Calendar.from_ical('\r\n'.join(ical_str)) - self.assertEqual(cal.errors, []) - - dtstart = cal.walk(name='VEVENT')[0].decoded("DTSTART") - expected = pytz.timezone('America/Sao_Paulo').localize(datetime.datetime(2017, 5, 11, 13, 30)) - self.assertEqual(dtstart, expected) - - try: - expected_zone = '(UTC-03:00) Brasília' - expected_tzname = 'Brasília standard' - except UnicodeEncodeError: - expected_zone = '(UTC-03:00) Brasília'.encode('ascii', 'replace') - expected_tzname = 'Brasília standard'.encode('ascii', 'replace') - self.assertEqual(dtstart.tzinfo.zone, expected_zone) - self.assertEqual(dtstart.tzname(), expected_tzname) diff --git a/src/icalendar/tests/test_icalendar.py b/src/icalendar/tests/test_icalendar.py index a23f13e..9bc2ef8 100644 --- a/src/icalendar/tests/test_icalendar.py +++ b/src/icalendar/tests/test_icalendar.py @@ -291,34 +291,3 @@ class IcalendarTestCase (unittest.TestCase): 'Max,Moller,"Rasmussen, Max"') -class TestEncoding(unittest.TestCase): - - def test_broken_property(self): - """ - Test if error messages are encode properly. - """ - broken_ical = textwrap.dedent(""" - BEGIN:VCALENDAR - BEGIN:VEVENT - SUMMARY:An Event with too many semicolons - DTSTART;;VALUE=DATE-TIME:20140409T093000 - UID:abc - END:VEVENT - END:VCALENDAR - """) - cal = icalendar.Calendar.from_ical(broken_ical) - for event in cal.walk('vevent'): - self.assertEqual(len(event.errors), 1, 'Not the right amount of errors.') - error = event.errors[0][1] - self.assertTrue(error.startswith('Content line could not be parsed into parts')) - - def test_apple_xlocation(self): - """ - Test if we support base64 encoded binary data in parameter values. - """ - directory = os.path.dirname(__file__) - with open(os.path.join(directory, 'x_location.ics'), 'rb') as fp: - data = fp.read() - cal = icalendar.Calendar.from_ical(data) - for event in cal.walk('vevent'): - self.assertEqual(len(event.errors), 0, 'Got too many errors') diff --git a/src/icalendar/tests/test_multiple.py b/src/icalendar/tests/test_multiple.py index a6fd1d6..0fd3336 100644 --- a/src/icalendar/tests/test_multiple.py +++ b/src/icalendar/tests/test_multiple.py @@ -1,27 +1,15 @@ +"""An example with multiple VCALENDAR components""" from icalendar import Calendar from icalendar.prop import vText -import unittest - -import os -class TestMultiple(unittest.TestCase): - """A example with multiple VCALENDAR components""" - def test_multiple(self): +def test_multiple(calendars): + """Check opening multiple calendars.""" - directory = os.path.dirname(__file__) - with open(os.path.join(directory, 'multiple.ics'), 'rb') as fp: - data = fp.read() - cals = Calendar.from_ical(data, multiple=True) + cals = calendars.multiple.multiple_calendar_components - self.assertEqual(len(cals), 2) - self.assertSequenceEqual([comp.name for comp in cals[0].walk()], - ['VCALENDAR', 'VEVENT']) - self.assertSequenceEqual([comp.name for comp in cals[1].walk()], - ['VCALENDAR', 'VEVENT', 'VEVENT']) - - self.assertEqual( - cals[0]['prodid'], - vText('-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN') - ) + assert len(cals) == 2 + assert [comp.name for comp in cals[0].walk()] == ['VCALENDAR', 'VEVENT'] + assert [comp.name for comp in cals[1].walk()] == ['VCALENDAR', 'VEVENT', 'VEVENT'] + assert cals[0]['prodid'] == vText('-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN') diff --git a/src/icalendar/tests/test_parsing.py b/src/icalendar/tests/test_parsing.py index b01c091..9607d61 100644 --- a/src/icalendar/tests/test_parsing.py +++ b/src/icalendar/tests/test_parsing.py @@ -15,9 +15,11 @@ from icalendar.parser import Contentline, Parameters # Nonstandard component inside other components, also has properties 'issue_178_custom_component_inside_other', # Nonstandard component is able to contain other components - 'issue_178_custom_component_contains_other']) + 'issue_178_custom_component_contains_other', +]) def test_calendar_to_ical_is_inverse_of_from_ical(calendars, calendar_name): calendar = getattr(calendars, calendar_name) + assert calendar.to_ical().splitlines() == calendar.raw_ics.splitlines() assert calendar.to_ical() == calendar.raw_ics @pytest.mark.parametrize('raw_content_line, expected_output', [ @@ -74,10 +76,14 @@ def test_issue_157_removes_trailing_semicolon(events): # https://github.com/collective/icalendar/pull/100 ('issue_100_transformed_doctests_into_unittests'), ('issue_184_broken_representation_of_period'), + # PERIOD should be put back into shape + 'issue_156_RDATE_with_PERIOD', + 'issue_156_RDATE_with_PERIOD_list', ]) def test_event_to_ical_is_inverse_of_from_ical(events, event_name): """Make sure that an event's ICS is equal to the ICS it was made from.""" event = events[event_name] + assert event.to_ical().splitlines() == event.raw_ics.splitlines() assert event.to_ical() == event.raw_ics def test_decode_rrule_attribute_error_issue_70(events): diff --git a/src/icalendar/tests/test_period.py b/src/icalendar/tests/test_period.py new file mode 100644 index 0000000..ff64a78 --- /dev/null +++ b/src/icalendar/tests/test_period.py @@ -0,0 +1,48 @@ +"""These tests make sure that we have some coverage on the usage of the PERIOD value type. + +See +- https://github.com/collective/icalendar/issues/156 +- https://github.com/pimutils/khal/issues/152#issuecomment-933635248 +""" +import pytest +import pytz +from icalendar.prop import vDDDTypes + + +@pytest.mark.parametrize("calname,tzname,index,period_string", [ + ("issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 0, "20211101T160000/20211101T163000"), + ("issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 1, "20211206T160000/20211206T163000"), + ("issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 2, "20220103T160000/20220103T163000"), + ("issue_156_RDATE_with_PERIOD_TZID_khal_2", "Europe/Berlin", 3, "20220207T160000/20220207T163000"), +] + [ + ("issue_156_RDATE_with_PERIOD_TZID_khal", "America/Chicago", i, period) + for i, period in enumerate(("20180327T080000/20180327T0" + "90000,20180403T080000/20180403T090000,20180410T080000/20180410T090000,2018" + "0417T080000/20180417T090000,20180424T080000/20180424T090000,20180501T08000" + "0/20180501T090000,20180508T080000/20180508T090000,20180515T080000/20180515" + "T090000,20180522T080000/20180522T090000,20180529T080000/20180529T090000,20" + "180605T080000/20180605T090000,20180612T080000/20180612T090000,20180619T080" + "000/20180619T090000,20180626T080000/20180626T090000,20180703T080000/201807" + "03T090000,20180710T080000/20180710T090000,20180717T080000/20180717T090000," + "20180724T080000/20180724T090000,20180731T080000/20180731T090000").split(",")) +]) +def test_issue_156_period_list_in_rdate(calendars, calname, tzname, index, period_string): + """Check items in a list of period values.""" + calendar = calendars[calname] + rdate = calendar.walk("vevent")[0]["rdate"] + period = rdate.dts[index] + assert period.dt == vDDDTypes.from_ical(period_string, timezone=pytz.timezone(tzname)) + + +def test_duration_properly_parsed(events): + """This checks the duration PT5H30M.""" + start = vDDDTypes.from_ical("19970109T180000Z") + duration = vDDDTypes.from_ical("PT5H30M") + rdate = events.issue_156_RDATE_with_PERIOD_list["RDATE"] + print(rdate) + period = rdate.dts[1].dt + print(dir(duration)) + assert period[0] == start + assert period[1].days == 0 + assert period[1].seconds == (5 * 60 + 30) * 60 + assert period[1] == duration diff --git a/src/icalendar/tests/timezones/issue_237_brazilia_standard.ics b/src/icalendar/tests/timezones/issue_237_brazilia_standard.ics new file mode 100644 index 0000000..462f3fc --- /dev/null +++ b/src/icalendar/tests/timezones/issue_237_brazilia_standard.ics @@ -0,0 +1,17 @@ +BEGIN:VTIMEZONE +TZID:(UTC-03:00) Brasília +BEGIN:STANDARD +TZNAME:Brasília standard +DTSTART:16010101T235959 +TZOFFSETFROM:-0200 +TZOFFSETTO:-0300 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=3SA;BYMONTH=2 +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:Brasília daylight +DTSTART:16010101T235959 +TZOFFSETFROM:-0300 +TZOFFSETTO:-0200 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=2SA;BYMONTH=10 +END:DAYLIGHT +END:VTIMEZONE diff --git a/tox.ini b/tox.ini index c80a2c4..3a13fd5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # to run for a specific environment, use ``tox -e ENVNAME`` [tox] -envlist = py37,py38,py39,py310,pypy3,docs +envlist = py37,py38,py39,py310,pypy3,docs,plone # Note: the 'docs' env creates a 'build' directory which may interfere in strange ways # with the other environments. You might see this when you run the tests in parallel. # See https://github.com/collective/icalendar/pull/359#issuecomment-1214150269 @@ -23,3 +23,17 @@ changedir = docs allowlist_externals = make commands = make html + +[testenv:plone] +usedevelop = False +install = False +python = 3.10 +commands_pre = +# Install Plone and explicitly the single package that uses icalendar, plus the test runner. + pip install Plone plone.app.event[test] zope.testrunner -c https://dist.plone.org/release/6.0-dev/constraints.txt +# Install the dev version of the package, mostly so we can safely point to the path with the tests. + pip install -e "git+https://github.com/plone/plone.app.event.git#egg=plone.app.event" +# icalendar is pinned in the constraints, but we want the current dev version. + pip install -e {toxinidir} +commands = + zope-testrunner --test-path={envdir}/src/plone-app-event {posargs:-vc}