kopia lustrzana https://github.com/collective/icalendar
Merge branch 'master' into issue-318-skip-value-parameter-for-default-date-time
commit
e5ae796862
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- This template is there to guide you and help us. If you can not complete everything here, that is fine. -->
|
||||
## Describe the bug
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
## To Reproduce
|
||||
<!-- Please add the neccesary code here to reproduce the problem on your machine. -->
|
||||
|
||||
```
|
||||
import icalendar
|
||||
...
|
||||
```
|
||||
Output:
|
||||
<!-- If applicable, add logs or error outputs to help explain your problem. -->
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
## Environment
|
||||
<!-- please complete the following information: -->
|
||||
- [ ] OS: <!-- e.g. Ubuntu 22 or Windows 10 -->
|
||||
- [ ] Python version: <!-- e.g. Python 3.10 -->
|
||||
- [ ] `icalendar` version: <!-- python3 -c 'import icalendar; print(icalendar.__version__)' -->
|
||||
|
||||
## Additional context
|
||||
<!-- Add any other context about the problem here, related issues and pull requests. -->
|
||||
- [ ] 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
|
|
@ -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: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- This is an unstructured issue template to use if you do not need guidance.
|
||||
Here are some hints though:
|
||||
- add ICS example files if you can
|
||||
- show code if possible
|
||||
- show error output or output that differs from what you expect
|
||||
- it is nice to connect to the value of this issue
|
||||
Thanks for taking your time to report this!
|
||||
-->
|
|
@ -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:
|
||||
|
@ -51,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/*
|
||||
|
|
48
CHANGES.rst
48
CHANGES.rst
|
@ -1,21 +1,63 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
5.0.2 (unreleased)
|
||||
------------------
|
||||
|
||||
5.0.0a2 (unreleased)
|
||||
--------------------
|
||||
Minor changes:
|
||||
|
||||
- Refactored cal.py, tools.py and completed remaining minimal refactoring in parser.py. Ref: #481 [pronoym99]
|
||||
- Calendar.from_ical no longer throws long errors
|
||||
Ref: #473
|
||||
Fixes: #472
|
||||
[jacadzaca]
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- ...
|
||||
|
||||
New features:
|
||||
|
||||
- source code in documentation is tested using doctest #445 [niccokunzmann]
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- broken properties are not added to the parent component
|
||||
Ref: #471
|
||||
Fixes: #464
|
||||
[jacadzaca]
|
||||
|
||||
5.0.1 (2022-10-22)
|
||||
------------------
|
||||
|
||||
Minor changes:
|
||||
|
||||
- fixed setuptools deprecation warnings [mgorny]
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- a well-known timezone timezone prefixed with a `/` is treated as if the slash wasn't present
|
||||
Ref: #467
|
||||
Fixes: #466
|
||||
[jacadzaca]
|
||||
|
||||
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:
|
||||
|
||||
|
@ -35,6 +77,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)
|
||||
--------------------
|
||||
|
|
|
@ -63,6 +63,8 @@ icalendar contributors
|
|||
- jacadzaca <vitouejj@gmail.com>
|
||||
- Mauro Amico <mauro.amico@gmail.com>
|
||||
- Alexander Pitkin <peleccom@gmail.com>
|
||||
- Michał Górny <mgorny@gentoo.org>
|
||||
- Pronoy <lukex9442@gmail.com>
|
||||
|
||||
Find out who contributed::
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ Try it out:
|
|||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
>>> import icalendar
|
||||
>>> icalendar.__version__
|
||||
'4.0.10.dev0'
|
||||
'5.0.1'
|
||||
|
||||
Building the documentation locally
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -25,6 +25,9 @@ Maintainers need this:
|
|||
- ``Maintainer`` access to the `Read The Docs project <https://readthedocs.org/projects/icalendar/>`_.
|
||||
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 <https://github.com/collective/icalendar/settings/environments/674266024/edit>`__
|
||||
by adding the GitHub username to the list of "Required Reviewers".
|
||||
|
||||
|
||||
Contributors
|
||||
|
@ -42,34 +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 <https://pypi.org/project/icalendar/>`_.
|
||||
|
||||
1. Check that the ``CHANGES.rst`` is up to date with the latest merges and the version you want to release is correctly named.
|
||||
2. Change the ``__version__`` variable in the ``src/icalendar/__init__.py`` file.
|
||||
3. Create a commit on the ``master`` branch to release this version.
|
||||
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.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git checkout master
|
||||
git commit -am"version 5.0.0a2"
|
||||
4. Push the commit and see if the `CI-tests <https://github.com/collective/icalendar/actions?query=branch%3Amaster>`__ are running on it.
|
||||
1. Check that the ``CHANGES.rst`` is up to date with the `latest merged pull requests <https://github.com/collective/icalendar/pulls?q=is%3Apr+is%3Amerged>`__
|
||||
and the version you want to release is correctly named.
|
||||
2. Change the ``__version__`` variable in
|
||||
|
||||
.. code-block:: bash
|
||||
- the ``src/icalendar/__init__.py`` file and
|
||||
- in the ``docs/usage.rst`` file (look for ``icalendar.__version__``)
|
||||
3. Create a commit on the ``release-5.0.0`` branch (or equivalent) to release this version.
|
||||
|
||||
git push
|
||||
5. Create a tag for the release and see if the `CI-tests <https://github.com/collective/icalendar/actions>`__ are running.
|
||||
.. code-block:: bash
|
||||
|
||||
.. code-block:: bash
|
||||
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"
|
||||
|
||||
git tag v5.0.0a2
|
||||
git push origin v5.0.0a2
|
||||
6. TODO: how to release new version to PyPI.
|
||||
4. Push the commit and `create a pull request <https://github.com/collective/icalendar/compare?expand=1>`__
|
||||
Here is an `example pull request #457 <https://github.com/collective/icalendar/pull/457>`__.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git push -u origin release-5.0.0
|
||||
|
||||
5. See if the `CI-tests <https://github.com/collective/icalendar/actions>`_ 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
|
||||
-----
|
||||
|
@ -79,6 +127,7 @@ This section contains useful links for maintainers and contributors:
|
|||
- `Future of icalendar, looking for maintainer #360 <https://github.com/collective/icalendar/discussions/360>`__
|
||||
- `Team icalendar-admin <https://github.com/orgs/collective/teams/icalendar-admin>`__
|
||||
- `Team icalendar-contributor <https://github.com/orgs/collective/teams/icalendar-contributor>`__
|
||||
- `Comment on the Plone tests running with icalendar <https://github.com/collective/icalendar/pull/447#issuecomment-1277643634>`__
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -79,8 +79,8 @@ you do it like this. The calendar is a component::
|
|||
>>> cal['summary'] = 'Python meeting about calendaring'
|
||||
>>> for k,v in cal.items():
|
||||
... k,v
|
||||
(u'DTSTART', '20050404T080000')
|
||||
(u'SUMMARY', 'Python meeting about calendaring')
|
||||
('DTSTART', '20050404T080000')
|
||||
('SUMMARY', 'Python meeting about calendaring')
|
||||
|
||||
NOTE: the recommended way to add components to the calendar is to use
|
||||
create the subcomponent and add it via Calendar.add! The example above adds a
|
||||
|
@ -90,7 +90,7 @@ string, but not a vText component.
|
|||
You can generate a string for a file with the to_ical() method::
|
||||
|
||||
>>> cal.to_ical()
|
||||
'BEGIN:VCALENDAR\r\nDTSTART:20050404T080000\r\nSUMMARY:Python meeting about calendaring\r\nEND:VCALENDAR\r\n'
|
||||
b'BEGIN:VCALENDAR\r\nDTSTART:20050404T080000\r\nSUMMARY:Python meeting about calendaring\r\nEND:VCALENDAR\r\n'
|
||||
|
||||
The rendered view is easier to read::
|
||||
|
||||
|
@ -102,13 +102,13 @@ The rendered view is easier to read::
|
|||
So, let's define a function so we can easily display to_ical() output::
|
||||
|
||||
>>> def display(cal):
|
||||
... return cal.to_ical().decode("utf-8").replace(b'\r\n', b'\n').strip()
|
||||
... return cal.to_ical().decode("utf-8").replace('\r\n', '\n').strip()
|
||||
|
||||
You can set multiple properties like this::
|
||||
|
||||
>>> cal = Calendar()
|
||||
>>> cal['attendee'] = ['MAILTO:maxm@mxm.dk','MAILTO:test@example.com']
|
||||
>>> print display(cal)
|
||||
>>> print(display(cal))
|
||||
BEGIN:VCALENDAR
|
||||
ATTENDEE:MAILTO:maxm@mxm.dk
|
||||
ATTENDEE:MAILTO:test@example.com
|
||||
|
@ -122,7 +122,7 @@ added. Here is an example::
|
|||
>>> cal = Calendar()
|
||||
>>> cal.add('attendee', 'MAILTO:maxm@mxm.dk')
|
||||
>>> cal.add('attendee', 'MAILTO:test@example.com')
|
||||
>>> print display(cal)
|
||||
>>> print(display(cal))
|
||||
BEGIN:VCALENDAR
|
||||
ATTENDEE:MAILTO:maxm@mxm.dk
|
||||
ATTENDEE:MAILTO:test@example.com
|
||||
|
@ -148,7 +148,7 @@ component::
|
|||
And then appending it to a "parent"::
|
||||
|
||||
>>> cal.add_component(event)
|
||||
>>> print display(cal)
|
||||
>>> print(display(cal))
|
||||
BEGIN:VCALENDAR
|
||||
ATTENDEE:MAILTO:maxm@mxm.dk
|
||||
ATTENDEE:MAILTO:test@example.com
|
||||
|
@ -161,7 +161,7 @@ And then appending it to a "parent"::
|
|||
Subcomponents are appended to the subcomponents property on the component::
|
||||
|
||||
>>> cal.subcomponents
|
||||
[VEVENT({'DTSTART': '20050404T080000', 'UID': '42'})]
|
||||
[VEVENT({'UID': '42', 'DTSTART': '20050404T080000'})]
|
||||
|
||||
|
||||
Value types
|
||||
|
@ -184,7 +184,7 @@ type defined in the spec::
|
|||
>>> from datetime import datetime
|
||||
>>> cal.add('dtstart', datetime(2005,4,4,8,0,0))
|
||||
>>> cal['dtstart'].to_ical()
|
||||
'20050404T080000'
|
||||
b'20050404T080000'
|
||||
|
||||
If that doesn't work satisfactorily for some reason, you can also do it
|
||||
manually.
|
||||
|
@ -197,7 +197,7 @@ So if you want to do it manually::
|
|||
>>> from icalendar import vDatetime
|
||||
>>> now = datetime(2005,4,4,8,0,0)
|
||||
>>> vDatetime(now).to_ical()
|
||||
'20050404T080000'
|
||||
b'20050404T080000'
|
||||
|
||||
So the drill is to initialise the object with a python built in type,
|
||||
and then call the "to_ical()" method on the object. That will return an
|
||||
|
@ -220,7 +220,7 @@ value directly::
|
|||
>>> cal = Calendar()
|
||||
>>> cal.add('dtstart', datetime(2005,4,4,8,0,0))
|
||||
>>> cal['dtstart'].to_ical()
|
||||
'20050404T080000'
|
||||
b'20050404T080000'
|
||||
>>> cal.decoded('dtstart')
|
||||
datetime.datetime(2005, 4, 4, 8, 0)
|
||||
|
||||
|
@ -232,12 +232,13 @@ Property parameters are automatically added, depending on the input value. For
|
|||
example, for date/time related properties, the value type and timezone
|
||||
identifier (if applicable) are automatically added here::
|
||||
|
||||
>>> import pytz
|
||||
>>> event = Event()
|
||||
>>> event.add('dtstart', datetime(2010, 10, 10, 10, 0, 0,
|
||||
... tzinfo=pytz.timezone("Europe/Vienna")))
|
||||
|
||||
>>> lines = event.to_ical().splitlines()
|
||||
>>> self.assertTrue(
|
||||
>>> assert (
|
||||
... b"DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20101010T100000"
|
||||
... in lines)
|
||||
|
||||
|
@ -247,9 +248,9 @@ dictionary to the add method like so::
|
|||
|
||||
>>> event = Event()
|
||||
>>> event.add('X-TEST-PROP', 'tryout.',
|
||||
.... parameters={'prop1': 'val1', 'prop2': 'val2'})
|
||||
... parameters={'prop1':'val1', 'prop2':'val2'})
|
||||
>>> lines = event.to_ical().splitlines()
|
||||
>>> self.assertTrue(b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines)
|
||||
>>> assert b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines
|
||||
|
||||
|
||||
Example
|
||||
|
@ -270,7 +271,6 @@ Some properties are required to be compliant::
|
|||
|
||||
We need at least one subcomponent for a calendar to be compliant::
|
||||
|
||||
>>> import pytz
|
||||
>>> event = Event()
|
||||
>>> event.add('summary', 'Python meeting about calendaring')
|
||||
>>> event.add('dtstart', datetime(2005,4,4,8,0,0,tzinfo=pytz.utc))
|
||||
|
@ -314,6 +314,7 @@ Write to disk::
|
|||
>>> directory = tempfile.mkdtemp()
|
||||
>>> f = open(os.path.join(directory, 'example.ics'), 'wb')
|
||||
>>> f.write(cal.to_ical())
|
||||
570
|
||||
>>> f.close()
|
||||
|
||||
|
||||
|
|
|
@ -7,13 +7,12 @@ ignore =
|
|||
|
||||
[zest.releaser]
|
||||
python-file-with-version = src/icalendar/__init__.py
|
||||
create-wheel = yes
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
universal = 0
|
||||
|
||||
[metadata]
|
||||
license_file = LICENSE.rst
|
||||
license_files = LICENSE.rst
|
||||
|
||||
[tool:pytest]
|
||||
norecursedirs = .* env* docs *.egg src/icalendar/tests/hypothesis
|
||||
|
|
3
setup.py
3
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,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
__version__ = '5.0.0a2.dev0'
|
||||
__version__ = '5.0.1'
|
||||
|
||||
from icalendar.cal import (
|
||||
Calendar,
|
||||
|
|
|
@ -370,8 +370,7 @@ class Component(CaselessDict):
|
|||
factory = types_factory.for_property(name)
|
||||
component = stack[-1] if stack else None
|
||||
if not component:
|
||||
raise ValueError('Property "{prop}" does not have '
|
||||
'a parent component.'.format(prop=name))
|
||||
raise ValueError(f'Property "{name}" does not have a parent component.')
|
||||
datetime_names = ('DTSTART', 'DTEND', 'RECURRENCE-ID', 'DUE',
|
||||
'FREEBUSY', 'RDATE', 'EXDATE')
|
||||
try:
|
||||
|
@ -383,7 +382,6 @@ class Component(CaselessDict):
|
|||
if not component.ignore_exceptions:
|
||||
raise
|
||||
component.errors.append((uname, str(e)))
|
||||
component.add(name, None, encode=0)
|
||||
else:
|
||||
vals.params = params
|
||||
component.add(name, vals, encode=0)
|
||||
|
@ -391,14 +389,22 @@ class Component(CaselessDict):
|
|||
if multiple:
|
||||
return comps
|
||||
if len(comps) > 1:
|
||||
raise ValueError(f'Found multiple components where '
|
||||
f'only one is allowed: {st!r}')
|
||||
raise ValueError(cls._format_error(
|
||||
'Found multiple components where only one is allowed', st))
|
||||
if len(comps) < 1:
|
||||
raise ValueError(f'Found no components where '
|
||||
f'exactly one is required: '
|
||||
f'{st!r}')
|
||||
raise ValueError(cls._format_error(
|
||||
'Found no components where exactly one is required', st))
|
||||
return comps[0]
|
||||
|
||||
def _format_error(error_description, bad_input, elipsis='[...]'):
|
||||
# there's three character more in the error, ie. ' ' x2 and a ':'
|
||||
max_error_length = 100 - 3
|
||||
if len(error_description) + len(bad_input) + len(elipsis) > max_error_length:
|
||||
truncate_to = max_error_length - len(error_description) - len(elipsis)
|
||||
return f'{error_description}: {bad_input[:truncate_to]} {elipsis}'
|
||||
else:
|
||||
return f'{error_description}: {bad_input}'
|
||||
|
||||
def content_line(self, name, value, sorted=True):
|
||||
"""Returns property as content line.
|
||||
"""
|
||||
|
@ -427,12 +433,8 @@ class Component(CaselessDict):
|
|||
def __repr__(self):
|
||||
"""String representation of class with all of it's subcomponents.
|
||||
"""
|
||||
subs = ', '.join([str(it) for it in self.subcomponents])
|
||||
return '{}({}{})'.format(
|
||||
self.name or type(self).__name__,
|
||||
dict(self),
|
||||
', %s' % subs if subs else ''
|
||||
)
|
||||
subs = ', '.join(str(it) for it in self.subcomponents)
|
||||
return f"{self.name or type(self).__name__}({dict(self)}{', ' + subs if subs else ''})"
|
||||
|
||||
|
||||
#######################################
|
||||
|
@ -605,12 +607,10 @@ class Timezone(Component):
|
|||
tzname = component['TZNAME'].encode('ascii', 'replace')
|
||||
tzname = self._make_unique_tzname(tzname, tznames)
|
||||
except KeyError:
|
||||
tzname = '{}_{}_{}_{}'.format(
|
||||
zone,
|
||||
component['DTSTART'].to_ical().decode('utf-8'),
|
||||
component['TZOFFSETFROM'].to_ical(), # for whatever reason this is str/unicode
|
||||
component['TZOFFSETTO'].to_ical(), # for whatever reason this is str/unicode
|
||||
)
|
||||
# for whatever reason this is str/unicode
|
||||
tzname = f"{zone}_{component['DTSTART'].to_ical().decode('utf-8')}_" + \
|
||||
f"{component['TZOFFSETFROM'].to_ical()}_" + \
|
||||
f"{component['TZOFFSETTO'].to_ical()}"
|
||||
tzname = self._make_unique_tzname(tzname, tznames)
|
||||
|
||||
dst[tzname], component_transitions = self._extract_offsets(
|
||||
|
|
|
@ -51,13 +51,14 @@ def tzid_from_dt(dt):
|
|||
if hasattr(dt.tzinfo, 'zone'):
|
||||
tzid = dt.tzinfo.zone # pytz implementation
|
||||
elif hasattr(dt.tzinfo, 'key'):
|
||||
tzid = dt.tzinfo.key # ZoneInfo implementation
|
||||
tzid = dt.tzinfo.key # ZoneInfo implementation
|
||||
elif hasattr(dt.tzinfo, 'tzname'):
|
||||
# dateutil implementation, but this is broken
|
||||
# See https://github.com/collective/icalendar/issues/333 for details
|
||||
tzid = dt.tzinfo.tzname(dt)
|
||||
return tzid
|
||||
|
||||
|
||||
def foldline(line, limit=75, fold_sep='\r\n '):
|
||||
"""Make a string folded as defined in RFC5545
|
||||
Lines of text SHOULD NOT be longer than 75 octets, excluding the line
|
||||
|
@ -142,7 +143,7 @@ def dquote(val):
|
|||
# so replace it with a single-quote character
|
||||
val = val.replace('"', "'")
|
||||
if QUOTABLE.search(val):
|
||||
return '"%s"' % val
|
||||
return f'"{val}"'
|
||||
return val
|
||||
|
||||
|
||||
|
@ -158,8 +159,7 @@ def q_split(st, sep=',', maxsplit=-1):
|
|||
length = len(st)
|
||||
inquote = 0
|
||||
splits = 0
|
||||
for i in range(length):
|
||||
ch = st[i]
|
||||
for i, ch in enumerate(st):
|
||||
if ch == '"':
|
||||
inquote = not inquote
|
||||
if not inquote and ch == sep:
|
||||
|
@ -255,13 +255,13 @@ class Parameters(CaselessDict):
|
|||
else:
|
||||
result[key] = vals
|
||||
except ValueError as exc:
|
||||
raise ValueError('%r is not a valid parameter string: %s'
|
||||
% (param, exc))
|
||||
raise ValueError(
|
||||
f'{param!r} is not a valid parameter string: {exc}')
|
||||
return result
|
||||
|
||||
|
||||
def escape_string(val):
|
||||
# '%{:02X}'.format(i)
|
||||
# f'{i:02X}'
|
||||
return val.replace(r'\,', '%2C').replace(r'\:', '%3A')\
|
||||
.replace(r'\;', '%3B').replace(r'\\', '%5C')
|
||||
|
||||
|
@ -288,7 +288,7 @@ class Contentline(str):
|
|||
def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING):
|
||||
value = to_unicode(value, encoding=encoding)
|
||||
assert '\n' not in value, ('Content line can not contain unescaped '
|
||||
'new line characters.')
|
||||
'new line characters.')
|
||||
self = super().__new__(cls, value)
|
||||
self.strict = strict
|
||||
return self
|
||||
|
@ -346,9 +346,7 @@ class Contentline(str):
|
|||
return (name, params, values)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
"Content line could not be parsed into parts: '%s': %s"
|
||||
% (self, exc)
|
||||
)
|
||||
f"Content line could not be parsed into parts: '{self}': {exc}")
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical, strict=False):
|
||||
|
@ -370,6 +368,7 @@ class Contentlines(list):
|
|||
Then this should be efficient. for Huge files, an iterator should probably
|
||||
be used instead.
|
||||
"""
|
||||
|
||||
def to_ical(self):
|
||||
"""Simply join self.
|
||||
"""
|
||||
|
|
|
@ -90,6 +90,7 @@ DSTDIFF = DSTOFFSET - STDOFFSET
|
|||
class FixedOffset(tzinfo):
|
||||
"""Fixed offset in minutes east from UTC.
|
||||
"""
|
||||
|
||||
def __init__(self, offset, name):
|
||||
self.__offset = timedelta(minutes=offset)
|
||||
self.__name = name
|
||||
|
@ -107,17 +108,12 @@ class FixedOffset(tzinfo):
|
|||
class LocalTimezone(tzinfo):
|
||||
"""Timezone of the machine where the code is running.
|
||||
"""
|
||||
|
||||
def utcoffset(self, dt):
|
||||
if self._isdst(dt):
|
||||
return DSTOFFSET
|
||||
else:
|
||||
return STDOFFSET
|
||||
return DSTOFFSET if self._isdst(dt) else STDOFFSET
|
||||
|
||||
def dst(self, dt):
|
||||
if self._isdst(dt):
|
||||
return DSTDIFF
|
||||
else:
|
||||
return ZERO
|
||||
return DSTDIFF if self._isdst(dt) else ZERO
|
||||
|
||||
def tzname(self, dt):
|
||||
return _time.tzname[self._isdst(dt)]
|
||||
|
@ -140,7 +136,7 @@ class vBinary:
|
|||
self.params = Parameters(encoding='BASE64', value="BINARY")
|
||||
|
||||
def __repr__(self):
|
||||
return "vBinary('%s')" % self.to_ical()
|
||||
return f"vBinary('{self.to_ical()}')"
|
||||
|
||||
def to_ical(self):
|
||||
return binascii.b2a_base64(self.obj.encode('utf-8'))[:-1]
|
||||
|
@ -164,16 +160,14 @@ class vBoolean(int):
|
|||
return self
|
||||
|
||||
def to_ical(self):
|
||||
if self:
|
||||
return b'TRUE'
|
||||
return b'FALSE'
|
||||
return b'TRUE' if self else b'FALSE'
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical):
|
||||
try:
|
||||
return cls.BOOL_MAP[ical]
|
||||
except Exception:
|
||||
raise ValueError("Expected 'TRUE' or 'FALSE'. Got %s" % ical)
|
||||
raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}")
|
||||
|
||||
|
||||
class vCalAddress(str):
|
||||
|
@ -186,7 +180,7 @@ class vCalAddress(str):
|
|||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return "vCalAddress('%s')" % self.to_ical()
|
||||
return f"vCalAddress('{self.to_ical()}')"
|
||||
|
||||
def to_ical(self):
|
||||
return self.encode(DEFAULT_ENCODING)
|
||||
|
@ -212,7 +206,7 @@ class vFloat(float):
|
|||
try:
|
||||
return cls(ical)
|
||||
except Exception:
|
||||
raise ValueError('Expected float value, got: %s' % ical)
|
||||
raise ValueError(f'Expected float value, got: {ical}')
|
||||
|
||||
|
||||
class vInt(int):
|
||||
|
@ -231,12 +225,13 @@ class vInt(int):
|
|||
try:
|
||||
return cls(ical)
|
||||
except Exception:
|
||||
raise ValueError('Expected int, got: %s' % ical)
|
||||
raise ValueError(f'Expected int, got: {ical}')
|
||||
|
||||
|
||||
class vDDDLists:
|
||||
"""A list of vDDDTypes values.
|
||||
"""
|
||||
|
||||
def __init__(self, dt_list):
|
||||
if not hasattr(dt_list, '__iter__'):
|
||||
dt_list = [dt_list]
|
||||
|
@ -265,6 +260,7 @@ class vDDDLists:
|
|||
out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone))
|
||||
return out
|
||||
|
||||
|
||||
class vCategory:
|
||||
|
||||
def __init__(self, c_list):
|
||||
|
@ -287,6 +283,7 @@ class vDDDTypes:
|
|||
cannot be confused, and often values can be of either types.
|
||||
So this is practical.
|
||||
"""
|
||||
|
||||
def __init__(self, dt):
|
||||
if not isinstance(dt, (datetime, date, timedelta, time, tuple)):
|
||||
raise ValueError('You must use datetime, date, timedelta, '
|
||||
|
@ -344,13 +341,14 @@ class vDDDTypes:
|
|||
return vTime.from_ical(ical)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Expected datetime, date, or time, got: '%s'" % ical
|
||||
f"Expected datetime, date, or time, got: '{ical}'"
|
||||
)
|
||||
|
||||
|
||||
class vDate:
|
||||
"""Render and generates iCalendar date format.
|
||||
"""
|
||||
|
||||
def __init__(self, dt):
|
||||
if not isinstance(dt, date):
|
||||
raise ValueError('Value MUST be a date instance')
|
||||
|
@ -358,7 +356,7 @@ class vDate:
|
|||
self.params = Parameters({'value': 'DATE'})
|
||||
|
||||
def to_ical(self):
|
||||
s = "%04d%02d%02d" % (self.dt.year, self.dt.month, self.dt.day)
|
||||
s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}"
|
||||
return s.encode('utf-8')
|
||||
|
||||
@staticmethod
|
||||
|
@ -371,7 +369,7 @@ class vDate:
|
|||
)
|
||||
return date(*timetuple)
|
||||
except Exception:
|
||||
raise ValueError('Wrong date format %s' % ical)
|
||||
raise ValueError(f'Wrong date format {ical}')
|
||||
|
||||
|
||||
class vDatetime:
|
||||
|
@ -385,6 +383,7 @@ class vDatetime:
|
|||
created. Be aware that there are certain limitations with timezone naive
|
||||
DATE-TIME components in the icalendar standard.
|
||||
"""
|
||||
|
||||
def __init__(self, dt):
|
||||
self.dt = dt
|
||||
self.params = Parameters()
|
||||
|
@ -393,14 +392,7 @@ class vDatetime:
|
|||
dt = self.dt
|
||||
tzid = tzid_from_dt(dt)
|
||||
|
||||
s = "%04d%02d%02dT%02d%02d%02d" % (
|
||||
dt.year,
|
||||
dt.month,
|
||||
dt.day,
|
||||
dt.hour,
|
||||
dt.minute,
|
||||
dt.second
|
||||
)
|
||||
s = f"{dt.year:04}{dt.month:02}{dt.day:02}T{dt.hour:02}{dt.minute:02}{dt.second:02}"
|
||||
if tzid == 'UTC':
|
||||
s += "Z"
|
||||
elif tzid:
|
||||
|
@ -412,10 +404,11 @@ class vDatetime:
|
|||
tzinfo = None
|
||||
if timezone:
|
||||
try:
|
||||
tzinfo = pytz.timezone(timezone)
|
||||
tzinfo = pytz.timezone(timezone.strip('/'))
|
||||
except pytz.UnknownTimeZoneError:
|
||||
if timezone in WINDOWS_TO_OLSON:
|
||||
tzinfo = pytz.timezone(WINDOWS_TO_OLSON.get(timezone))
|
||||
tzinfo = pytz.timezone(
|
||||
WINDOWS_TO_OLSON.get(timezone.strip('/')))
|
||||
else:
|
||||
tzinfo = _timezone_cache.get(timezone, None)
|
||||
|
||||
|
@ -437,7 +430,7 @@ class vDatetime:
|
|||
else:
|
||||
raise ValueError(ical)
|
||||
except Exception:
|
||||
raise ValueError('Wrong datetime format: %s' % ical)
|
||||
raise ValueError(f'Wrong datetime format: {ical}')
|
||||
|
||||
|
||||
class vDuration:
|
||||
|
@ -464,18 +457,18 @@ class vDuration:
|
|||
minutes = td.seconds % 3600 // 60
|
||||
seconds = td.seconds % 60
|
||||
if hours:
|
||||
timepart += "%dH" % hours
|
||||
timepart += f"{hours}H"
|
||||
if minutes or (hours and seconds):
|
||||
timepart += "%dM" % minutes
|
||||
timepart += f"{minutes}M"
|
||||
if seconds:
|
||||
timepart += "%dS" % seconds
|
||||
timepart += f"{seconds}S"
|
||||
if td.days == 0 and timepart:
|
||||
return (str(sign).encode('utf-8') + b'P' +
|
||||
str(timepart).encode('utf-8'))
|
||||
return (str(sign).encode('utf-8') + b'P'
|
||||
+ str(timepart).encode('utf-8'))
|
||||
else:
|
||||
return (str(sign).encode('utf-8') + b'P' +
|
||||
str(abs(td.days)).encode('utf-8') +
|
||||
b'D' + str(timepart).encode('utf-8'))
|
||||
return (str(sign).encode('utf-8') + b'P'
|
||||
+ str(abs(td.days)).encode('utf-8')
|
||||
+ b'D' + str(timepart).encode('utf-8'))
|
||||
|
||||
@staticmethod
|
||||
def from_ical(ical):
|
||||
|
@ -493,19 +486,20 @@ class vDuration:
|
|||
value = -value
|
||||
return value
|
||||
except Exception:
|
||||
raise ValueError('Invalid iCalendar duration: %s' % ical)
|
||||
raise ValueError(f'Invalid iCalendar duration: {ical}')
|
||||
|
||||
|
||||
class vPeriod:
|
||||
"""A precise period of time.
|
||||
"""
|
||||
|
||||
def __init__(self, per):
|
||||
start, end_or_duration = per
|
||||
if not (isinstance(start, datetime) or isinstance(start, date)):
|
||||
raise ValueError('Start value MUST be a datetime or date instance')
|
||||
if not (isinstance(end_or_duration, datetime) or
|
||||
isinstance(end_or_duration, date) or
|
||||
isinstance(end_or_duration, timedelta)):
|
||||
if not (isinstance(end_or_duration, datetime)
|
||||
or isinstance(end_or_duration, date)
|
||||
or isinstance(end_or_duration, timedelta)):
|
||||
raise ValueError('end_or_duration MUST be a datetime, '
|
||||
'date or timedelta instance')
|
||||
by_duration = 0
|
||||
|
@ -533,7 +527,8 @@ class vPeriod:
|
|||
|
||||
def __cmp__(self, other):
|
||||
if not isinstance(other, vPeriod):
|
||||
raise NotImplementedError('Cannot compare vPeriod with %r' % other)
|
||||
raise NotImplementedError(
|
||||
f'Cannot compare vPeriod with {other!r}')
|
||||
return cmp((self.start, self.end), (other.start, other.end))
|
||||
|
||||
def overlaps(self, other):
|
||||
|
@ -545,10 +540,10 @@ class vPeriod:
|
|||
|
||||
def to_ical(self):
|
||||
if self.by_duration:
|
||||
return (vDatetime(self.start).to_ical() + b'/' +
|
||||
vDuration(self.duration).to_ical())
|
||||
return (vDatetime(self.start).to_ical() + b'/' +
|
||||
vDatetime(self.end).to_ical())
|
||||
return (vDatetime(self.start).to_ical() + b'/'
|
||||
+ vDuration(self.duration).to_ical())
|
||||
return (vDatetime(self.start).to_ical() + b'/'
|
||||
+ vDatetime(self.end).to_ical())
|
||||
|
||||
@staticmethod
|
||||
def from_ical(ical):
|
||||
|
@ -558,7 +553,7 @@ class vPeriod:
|
|||
end_or_duration = vDDDTypes.from_ical(end_or_duration)
|
||||
return (start, end_or_duration)
|
||||
except Exception:
|
||||
raise ValueError('Expected period format, got: %s' % ical)
|
||||
raise ValueError(f'Expected period format, got: {ical}')
|
||||
|
||||
def __repr__(self):
|
||||
if self.by_duration:
|
||||
|
@ -580,13 +575,13 @@ class vWeekday(str):
|
|||
self = super().__new__(cls, value)
|
||||
match = WEEKDAY_RULE.match(self)
|
||||
if match is None:
|
||||
raise ValueError('Expected weekday abbrevation, got: %s' % self)
|
||||
raise ValueError(f'Expected weekday abbrevation, got: {self}')
|
||||
match = match.groupdict()
|
||||
sign = match['signal']
|
||||
weekday = match['weekday']
|
||||
relative = match['relative']
|
||||
if weekday not in vWeekday.week_days or sign not in '+-':
|
||||
raise ValueError('Expected weekday abbrevation, got: %s' % self)
|
||||
raise ValueError(f'Expected weekday abbrevation, got: {self}')
|
||||
self.relative = relative and int(relative) or None
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
@ -599,7 +594,7 @@ class vWeekday(str):
|
|||
try:
|
||||
return cls(ical.upper())
|
||||
except Exception:
|
||||
raise ValueError('Expected weekday abbrevation, got: %s' % ical)
|
||||
raise ValueError(f'Expected weekday abbrevation, got: {ical}')
|
||||
|
||||
|
||||
class vFrequency(str):
|
||||
|
@ -620,7 +615,7 @@ class vFrequency(str):
|
|||
value = to_unicode(value, encoding=encoding)
|
||||
self = super().__new__(cls, value)
|
||||
if self not in vFrequency.frequencies:
|
||||
raise ValueError('Expected frequency, got: %s' % self)
|
||||
raise ValueError(f'Expected frequency, got: {self}')
|
||||
self.params = Parameters()
|
||||
return self
|
||||
|
||||
|
@ -632,7 +627,7 @@ class vFrequency(str):
|
|||
try:
|
||||
return cls(ical.upper())
|
||||
except Exception:
|
||||
raise ValueError('Expected frequency, got: %s' % ical)
|
||||
raise ValueError(f'Expected frequency, got: {ical}')
|
||||
|
||||
|
||||
class vRecur(CaselessDict):
|
||||
|
@ -706,7 +701,7 @@ class vRecur(CaselessDict):
|
|||
recur[key] = cls.parse_type(key, vals)
|
||||
return dict(recur)
|
||||
except Exception:
|
||||
raise ValueError('Error in recurrence rule: %s' % ical)
|
||||
raise ValueError(f'Error in recurrence rule: {ical}')
|
||||
|
||||
|
||||
class vText(str):
|
||||
|
@ -721,7 +716,7 @@ class vText(str):
|
|||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return "vText('%s')" % self.to_ical()
|
||||
return f"vText('{self.to_ical()}')"
|
||||
|
||||
def to_ical(self):
|
||||
return escape_char(self).encode(self.encoding)
|
||||
|
@ -739,7 +734,7 @@ class vTime:
|
|||
def __init__(self, *args):
|
||||
if len(args) == 1:
|
||||
if not isinstance(args[0], (time, datetime)):
|
||||
raise ValueError('Expected a datetime.time, got: %s' % args[0])
|
||||
raise ValueError(f'Expected a datetime.time, got: {args[0]}')
|
||||
self.dt = args[0]
|
||||
else:
|
||||
self.dt = time(*args)
|
||||
|
@ -755,7 +750,7 @@ class vTime:
|
|||
timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
|
||||
return time(*timetuple)
|
||||
except Exception:
|
||||
raise ValueError('Expected time, got: %s' % ical)
|
||||
raise ValueError(f'Expected time, got: {ical}')
|
||||
|
||||
|
||||
class vUri(str):
|
||||
|
@ -776,7 +771,7 @@ class vUri(str):
|
|||
try:
|
||||
return cls(ical)
|
||||
except Exception:
|
||||
raise ValueError('Expected , got: %s' % ical)
|
||||
raise ValueError(f'Expected , got: {ical}')
|
||||
|
||||
|
||||
class vGeo:
|
||||
|
@ -804,7 +799,7 @@ class vGeo:
|
|||
latitude, longitude = ical.split(';')
|
||||
return (float(latitude), float(longitude))
|
||||
except Exception:
|
||||
raise ValueError("Expected 'float;float' , got: %s" % ical)
|
||||
raise ValueError(f"Expected 'float;float' , got: {ical}")
|
||||
|
||||
|
||||
class vUTCOffset:
|
||||
|
@ -812,9 +807,9 @@ class vUTCOffset:
|
|||
"""
|
||||
|
||||
ignore_exceptions = False # if True, and we cannot parse this
|
||||
# component, we will silently ignore
|
||||
# it, rather than let the exception
|
||||
# propagate upwards
|
||||
# component, we will silently ignore
|
||||
# it, rather than let the exception
|
||||
# propagate upwards
|
||||
|
||||
def __init__(self, td):
|
||||
if not isinstance(td, timedelta):
|
||||
|
@ -838,9 +833,9 @@ class vUTCOffset:
|
|||
minutes = abs((seconds % 3600) // 60)
|
||||
seconds = abs(seconds % 60)
|
||||
if seconds:
|
||||
duration = '%02i%02i%02i' % (hours, minutes, seconds)
|
||||
duration = f'{hours:02}{minutes:02}{seconds:02}'
|
||||
else:
|
||||
duration = '%02i%02i' % (hours, minutes)
|
||||
duration = f'{hours:02}{minutes:02}'
|
||||
return sign % duration
|
||||
|
||||
@classmethod
|
||||
|
@ -854,10 +849,10 @@ class vUTCOffset:
|
|||
int(ical[5:7] or 0))
|
||||
offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
||||
except Exception:
|
||||
raise ValueError('Expected utc offset, got: %s' % ical)
|
||||
raise ValueError(f'Expected utc offset, got: {ical}')
|
||||
if not cls.ignore_exceptions and offset >= timedelta(hours=24):
|
||||
raise ValueError(
|
||||
'Offset must be less than 24 hours, was %s' % ical)
|
||||
f'Offset must be less than 24 hours, was {ical}')
|
||||
if sign == '-':
|
||||
return -offset
|
||||
return offset
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,12 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
UID:0cab49a0-1167-40f0-bfed-ecb4d117047d
|
||||
DTSTAMP:20221019T102950Z
|
||||
DTSTART;TZID=/Europe/Stockholm:20221021T200000
|
||||
DTEND;TZID=/Europe/Stockholm:20221021T210000
|
||||
SUMMARY:Just chatting
|
||||
DESCRIPTION:Just Chatting.
|
||||
CATEGORIES:Just Chatting
|
||||
RRULE:FREQ=WEEKLY;BYDAY=FR
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,29 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:/Europe/CUSTOM
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:CEST
|
||||
DTSTART:19700329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:CET
|
||||
DTSTART:19701025T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:0cab49a0-1167-40f0-bfed-ecb4d117047d
|
||||
DTSTAMP:20221019T102950Z
|
||||
DTSTART;TZID=/Europe/CUSTOM:20221021T200000
|
||||
DTEND;TZID=/Europe/CUSTOM:20221021T210000
|
||||
SUMMARY:Just chatting
|
||||
DESCRIPTION:Just Chatting.
|
||||
CATEGORIES:Just Chatting
|
||||
RRULE:FREQ=WEEKLY;BYDAY=FR
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,7 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:Example calendar with a ': ' in the summary
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:Another event with a ': ' in the summary
|
||||
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
END:VEVENT
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
BEGIN:VEVENT
|
||||
ORGANIZER;CN=Society\, 2014:that
|
||||
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
|||
BEGIN:VEVENT
|
||||
ORGANIZER;CN=Society\\ 2014:that
|
||||
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
|||
BEGIN:VEVENT
|
||||
ORGANIZER;CN=Society\; 2014:that
|
||||
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
|||
BEGIN:VEVENT
|
||||
ORGANIZER;CN=Society\: 2014:that
|
||||
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
|||
BEGIN:VEVENT
|
||||
ORGANIZER;CN=that\, that\; %th%%at%\ that\::это\, то\; that\ %th%%at%\:
|
||||
END:VEVENT
|
|
@ -0,0 +1,3 @@
|
|||
BEGIN:VEVENT
|
||||
ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org
|
||||
END:VEVENT
|
|
@ -0,0 +1,7 @@
|
|||
BEGIN:VEVENT
|
||||
SUMMARY:RDATE period
|
||||
DTSTART:19961230T020000Z
|
||||
DTEND:19961230T060000Z
|
||||
UID:rdate_period
|
||||
RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z
|
||||
END:VEVENT
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
BEGIN:VEVENT
|
||||
SUMMARY:RDATE period
|
||||
DTSTART:19961230T020000Z
|
||||
DTEND:19961230T060000Z
|
||||
UID:rdate_period
|
||||
RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z,199709T180000Z/PT5H30M
|
||||
END:VEVENT
|
|
@ -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
|
|
@ -0,0 +1,41 @@
|
|||
import pytest
|
||||
|
||||
from icalendar import Event, Calendar
|
||||
|
||||
def test_ignore_exceptions_on_broken_events_issue_104(events):
|
||||
''' Issue #104 - line parsing error in a VEVENT
|
||||
(which has ignore_exceptions). Should mark the event broken
|
||||
but not raise an exception.
|
||||
|
||||
https://github.com/collective/icalendar/issues/104
|
||||
'''
|
||||
assert events.issue_104_mark_events_broken.is_broken # TODO: REMOVE FOR NEXT MAJOR RELEASE
|
||||
assert events.issue_104_mark_events_broken.errors == [(None, "Content line could not be parsed into parts: 'X': Invalid content line")]
|
||||
|
||||
def test_dont_ignore_exceptions_on_broken_calendars_issue_104(calendars):
|
||||
'''Issue #104 - line parsing error in a VCALENDAR
|
||||
(which doesn't have ignore_exceptions). Should raise an exception.
|
||||
'''
|
||||
with pytest.raises(ValueError):
|
||||
calendars.issue_104_broken_calendar
|
||||
|
||||
def test_rdate_dosent_become_none_on_invalid_input_issue_464(events):
|
||||
'''Issue #464 - [BUG] RDATE can become None if value is invalid
|
||||
https://github.com/collective/icalendar/issues/464
|
||||
'''
|
||||
assert events.issue_464_invalid_rdate.is_broken
|
||||
assert ('RDATE', 'Expected period format, got: 199709T180000Z/PT5H30M') in events.issue_464_invalid_rdate.errors
|
||||
assert not b'RDATE:None' in events.issue_464_invalid_rdate.to_ical()
|
||||
|
||||
@pytest.mark.parametrize('calendar_name', [
|
||||
'big_bad_calendar',
|
||||
'small_bad_calendar',
|
||||
'multiple_calendar_components',
|
||||
'pr_480_summary_with_colon',
|
||||
])
|
||||
def test_error_message_doesnt_get_too_big(calendars, calendar_name):
|
||||
with pytest.raises(ValueError) as exception:
|
||||
calendars[calendar_name]
|
||||
# Ignore part before first : for the test.
|
||||
assert len(str(exception).split(': ', 1)[1]) <= 100
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
import datetime
|
||||
|
||||
@pytest.mark.parametrize('field, expected_value', [
|
||||
('PRODID', '-//Plönë.org//NONSGML plone.app.event//EN'),
|
||||
|
@ -22,8 +23,49 @@ def test_event_from_ical_respects_unicode(test_input, field, expected_value, eve
|
|||
event = events[test_input]
|
||||
assert event[field].to_ical().decode('utf-8') == expected_value
|
||||
|
||||
def test_events_parameter_unicoded(events):
|
||||
'''chokes on umlauts in ORGANIZER
|
||||
https://github.com/collective/icalendar/issues/101
|
||||
'''
|
||||
assert events.issue_101_icalendar_chokes_on_umlauts_in_organizer['ORGANIZER'].params['CN'] == 'acme, ädmin'
|
||||
@pytest.mark.parametrize('test_input, expected_output', [
|
||||
# chokes on umlauts in ORGANIZER
|
||||
# https://github.com/collective/icalendar/issues/101
|
||||
('issue_101_icalendar_chokes_on_umlauts_in_organizer', 'acme, ädmin'),
|
||||
('event_with_unicode_organizer', 'Джон Доу'),
|
||||
])
|
||||
def test_events_parameter_unicoded(events, test_input, expected_output):
|
||||
assert events[test_input]['ORGANIZER'].params['CN'] == expected_output
|
||||
|
||||
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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from icalendar import Event, Calendar
|
||||
|
||||
def test_ignore_exceptions_on_broken_events_issue_104(events):
|
||||
''' Issue #104 - line parsing error in a VEVENT
|
||||
(which has ignore_exceptions). Should mark the event broken
|
||||
but not raise an exception.
|
||||
|
||||
https://github.com/collective/icalendar/issues/104
|
||||
'''
|
||||
assert events.issue_104_mark_events_broken.is_broken # TODO: REMOVE FOR NEXT MAJOR RELEASE
|
||||
assert events.issue_104_mark_events_broken.errors == [(None, "Content line could not be parsed into parts: 'X': Invalid content line")]
|
||||
|
||||
def test_dont_ignore_exceptions_on_broken_calendars_issue_104(calendars):
|
||||
'''Issue #104 - line parsing error in a VCALENDAR
|
||||
(which doesn't have ignore_exceptions). Should raise an exception.
|
||||
'''
|
||||
with pytest.raises(ValueError):
|
||||
calendars.issue_104_broken_calendar
|
|
@ -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')
|
||||
|
|
|
@ -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,15 @@ 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',
|
||||
'event_with_unicode_organizer',
|
||||
])
|
||||
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):
|
||||
|
@ -154,3 +161,29 @@ def test_creates_event_with_base64_encoded_attachment_issue_82(events):
|
|||
event = Event()
|
||||
event.add('ATTACH', b)
|
||||
assert event.to_ical() == events.issue_82_expected_output.raw_ics
|
||||
|
||||
@pytest.mark.parametrize('calendar_name', [
|
||||
# Issue #466 - [BUG] TZID timezone is ignored when forward-slash is used
|
||||
# https://github.com/collective/icalendar/issues/466
|
||||
'issue_466_respect_unique_timezone',
|
||||
'issue_466_convert_tzid_with_slash'
|
||||
])
|
||||
def test_handles_unique_tzid(calendars, in_timezone, calendar_name):
|
||||
calendar = calendars[calendar_name]
|
||||
start_dt = calendar.walk('VEVENT')[0]['dtstart'].dt
|
||||
end_dt = calendar.walk('VEVENT')[0]['dtend'].dt
|
||||
assert start_dt == in_timezone(datetime(2022, 10, 21, 20, 0, 0), 'Europe/Stockholm')
|
||||
assert end_dt == in_timezone(datetime(2022, 10, 21, 21, 0, 0), 'Europe/Stockholm')
|
||||
|
||||
@pytest.mark.parametrize('event_name, expected_cn, expected_ics', [
|
||||
('event_with_escaped_characters', r'that, that; %th%%at%\ that:', 'это, то; that\\ %th%%at%:'),
|
||||
('event_with_escaped_character1', r'Society, 2014', 'that'),
|
||||
('event_with_escaped_character2', r'Society\ 2014', 'that'),
|
||||
('event_with_escaped_character3', r'Society; 2014', 'that'),
|
||||
('event_with_escaped_character4', r'Society: 2014', 'that'),
|
||||
])
|
||||
def test_escaped_characters_read(event_name, expected_cn, expected_ics, events):
|
||||
event = events[event_name]
|
||||
assert event['ORGANIZER'].params['CN'] == expected_cn
|
||||
assert event['ORGANIZER'].to_ical() == expected_ics.encode('utf-8')
|
||||
|
||||
|
|
|
@ -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
|
|
@ -1,12 +1,71 @@
|
|||
from icalendar import Calendar
|
||||
from icalendar import Event
|
||||
from icalendar import Parameters
|
||||
from icalendar import vCalAddress
|
||||
import unittest
|
||||
import pytest
|
||||
from icalendar import Calendar, Event, Parameters, vCalAddress
|
||||
|
||||
import unittest
|
||||
import icalendar
|
||||
import re
|
||||
|
||||
@pytest.mark.parametrize('parameter, expected', [
|
||||
# Simple parameter:value pair
|
||||
(Parameters(parameter1='Value1'), b'PARAMETER1=Value1'),
|
||||
# Parameter with list of values must be separated by comma
|
||||
(Parameters({'parameter1': ['Value1', 'Value2']}), b'PARAMETER1=Value1,Value2'),
|
||||
# Multiple parameters must be separated by a semicolon
|
||||
(Parameters({'RSVP': 'TRUE', 'ROLE': 'REQ-PARTICIPANT'}), b'ROLE=REQ-PARTICIPANT;RSVP=TRUE'),
|
||||
# Parameter values containing ',;:' must be double quoted
|
||||
(Parameters({'ALTREP': 'http://www.wiz.org'}), b'ALTREP="http://www.wiz.org"'),
|
||||
# list items must be quoted separately
|
||||
(Parameters({'MEMBER': ['MAILTO:projectA@host.com',
|
||||
'MAILTO:projectB@host.com']}),
|
||||
b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'),
|
||||
(Parameters({'parameter1': 'Value1',
|
||||
'parameter2': ['Value2', 'Value3'],
|
||||
'ALTREP': ['http://www.wiz.org', 'value4']}),
|
||||
b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'),
|
||||
# Including empty strings
|
||||
(Parameters({'PARAM': ''}), b'PARAM='),
|
||||
# We can also parse parameter strings
|
||||
(Parameters({'MEMBER': ['MAILTO:projectA@host.com',
|
||||
'MAILTO:projectB@host.com']}),
|
||||
b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'),
|
||||
# We can also parse parameter strings
|
||||
(Parameters({'PARAMETER1': 'Value1',
|
||||
'ALTREP': ['http://www.wiz.org', 'value4'],
|
||||
'PARAMETER2': ['Value2', 'Value3']}),
|
||||
b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'),
|
||||
])
|
||||
def test_parameter_to_ical_is_inverse_of_from_ical(parameter, expected):
|
||||
assert parameter.to_ical() == expected
|
||||
assert Parameters.from_ical(expected.decode('utf-8')) == parameter
|
||||
|
||||
def test_parse_parameter_string_without_quotes():
|
||||
assert Parameters.from_ical('PARAM1=Value 1;PARA2=Value 2') == Parameters({'PARAM1': 'Value 1', 'PARA2': 'Value 2'})
|
||||
|
||||
def test_parametr_is_case_insensitive():
|
||||
parameter = Parameters(parameter1='Value1')
|
||||
assert parameter['parameter1'] == parameter['PARAMETER1'] == parameter['PaRaMeTer1']
|
||||
|
||||
def test_parameter_keys_are_uppercase():
|
||||
parameter = Parameters(parameter1='Value1')
|
||||
assert list(parameter.keys()) == ['PARAMETER1']
|
||||
|
||||
@pytest.mark.parametrize('cn_param, cn_quoted', [
|
||||
# not double-quoted
|
||||
('Aramis', 'Aramis'),
|
||||
# if a space is present - enclose in double quotes
|
||||
('Aramis Alameda', '"Aramis Alameda"'),
|
||||
# a single quote in parameter value - double quote the value
|
||||
('Aramis d\'Alameda', '"Aramis d\'Alameda"'),
|
||||
('Арамис д\'Аламеда', '"Арамис д\'Аламеда"'),
|
||||
# double quote is replaced with single quote
|
||||
('Aramis d\"Alameda', '"Aramis d\'Alameda"'),
|
||||
])
|
||||
def test_quoting(cn_param, cn_quoted):
|
||||
event = Event()
|
||||
attendee = vCalAddress('test@example.com')
|
||||
attendee.params['CN'] = cn_param
|
||||
event.add('ATTENDEE', attendee)
|
||||
assert f'ATTENDEE;CN={cn_quoted}:test@example.com' in event.to_ical().decode('utf-8')
|
||||
|
||||
class TestPropertyParams(unittest.TestCase):
|
||||
|
||||
|
@ -30,146 +89,6 @@ class TestPropertyParams(unittest.TestCase):
|
|||
ical2 = Calendar.from_ical(ical_str)
|
||||
self.assertEqual(ical2.get('ORGANIZER').params.get('CN'), 'Doe, John')
|
||||
|
||||
def test_unicode_param(self):
|
||||
cal_address = vCalAddress('mailto:john.doe@example.org')
|
||||
cal_address.params["CN"] = "Джон Доу"
|
||||
vevent = Event()
|
||||
vevent['ORGANIZER'] = cal_address
|
||||
self.assertEqual(
|
||||
vevent.to_ical().decode('utf-8'),
|
||||
'BEGIN:VEVENT\r\n'
|
||||
'ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org\r\n'
|
||||
'END:VEVENT\r\n'
|
||||
)
|
||||
|
||||
self.assertEqual(vevent['ORGANIZER'].params['CN'],
|
||||
'Джон Доу')
|
||||
|
||||
def test_quoting(self):
|
||||
# not double-quoted
|
||||
self._test_quoting("Aramis", 'Aramis')
|
||||
# if a space is present - enclose in double quotes
|
||||
self._test_quoting("Aramis Alameda", '"Aramis Alameda"')
|
||||
# a single quote in parameter value - double quote the value
|
||||
self._test_quoting("Aramis d'Alameda", '"Aramis d\'Alameda"')
|
||||
# double quote is replaced with single quote
|
||||
self._test_quoting("Aramis d\"Alameda", '"Aramis d\'Alameda"')
|
||||
self._test_quoting("Арамис д'Аламеда", '"Арамис д\'Аламеда"')
|
||||
|
||||
def _test_quoting(self, cn_param, cn_quoted):
|
||||
"""
|
||||
@param cn_param: CN parameter value to test for quoting
|
||||
@param cn_quoted: expected quoted parameter in icalendar format
|
||||
"""
|
||||
vevent = Event()
|
||||
attendee = vCalAddress('test@mail.com')
|
||||
attendee.params['CN'] = cn_param
|
||||
vevent.add('ATTENDEE', attendee)
|
||||
self.assertEqual(
|
||||
vevent.to_ical(),
|
||||
b'BEGIN:VEVENT\r\nATTENDEE;CN=' + cn_quoted.encode('utf-8') +
|
||||
b':test@mail.com\r\nEND:VEVENT\r\n'
|
||||
)
|
||||
|
||||
def test_escaping(self):
|
||||
# verify that escaped non safe chars are decoded correctly
|
||||
NON_SAFE_CHARS = ',\\;:'
|
||||
for char in NON_SAFE_CHARS:
|
||||
cn_escaped = "Society\\%s 2014" % char
|
||||
cn_decoded = "Society%s 2014" % char
|
||||
vevent = Event.from_ical(
|
||||
'BEGIN:VEVENT\r\n'
|
||||
'ORGANIZER;CN=%s:that\r\n'
|
||||
'END:VEVENT\r\n' % cn_escaped
|
||||
)
|
||||
self.assertEqual(vevent['ORGANIZER'].params['CN'], cn_decoded)
|
||||
|
||||
vevent = Event.from_ical(
|
||||
'BEGIN:VEVENT\r\n'
|
||||
'ORGANIZER;CN=that\\, that\\; %th%%at%\\\\ that\\:'
|
||||
':это\\, то\\; that\\\\ %th%%at%\\:\r\n'
|
||||
'END:VEVENT\r\n'
|
||||
)
|
||||
self.assertEqual(
|
||||
vevent['ORGANIZER'].params['CN'],
|
||||
r'that, that; %th%%at%\ that:'
|
||||
)
|
||||
self.assertEqual(
|
||||
vevent['ORGANIZER'].to_ical().decode('utf-8'),
|
||||
'это, то; that\\ %th%%at%:'
|
||||
)
|
||||
|
||||
def test_parameters_class(self):
|
||||
|
||||
# Simple parameter:value pair
|
||||
p = Parameters(parameter1='Value1')
|
||||
self.assertEqual(p.to_ical(), b'PARAMETER1=Value1')
|
||||
|
||||
# keys are converted to upper
|
||||
self.assertEqual(list(p.keys()), ['PARAMETER1'])
|
||||
|
||||
# Parameters are case insensitive
|
||||
self.assertEqual(p['parameter1'], 'Value1')
|
||||
self.assertEqual(p['PARAMETER1'], 'Value1')
|
||||
|
||||
# Parameter with list of values must be separated by comma
|
||||
p = Parameters({'parameter1': ['Value1', 'Value2']})
|
||||
self.assertEqual(p.to_ical(), b'PARAMETER1=Value1,Value2')
|
||||
|
||||
# Multiple parameters must be separated by a semicolon
|
||||
p = Parameters({'RSVP': 'TRUE', 'ROLE': 'REQ-PARTICIPANT'})
|
||||
self.assertEqual(p.to_ical(), b'ROLE=REQ-PARTICIPANT;RSVP=TRUE')
|
||||
|
||||
# Parameter values containing ',;:' must be double quoted
|
||||
p = Parameters({'ALTREP': 'http://www.wiz.org'})
|
||||
self.assertEqual(p.to_ical(), b'ALTREP="http://www.wiz.org"')
|
||||
|
||||
# list items must be quoted separately
|
||||
p = Parameters({'MEMBER': ['MAILTO:projectA@host.com',
|
||||
'MAILTO:projectB@host.com']})
|
||||
self.assertEqual(
|
||||
p.to_ical(),
|
||||
b'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'
|
||||
)
|
||||
|
||||
# Now the whole sheebang
|
||||
p = Parameters({'parameter1': 'Value1',
|
||||
'parameter2': ['Value2', 'Value3'],
|
||||
'ALTREP': ['http://www.wiz.org', 'value4']})
|
||||
self.assertEqual(
|
||||
p.to_ical(),
|
||||
(b'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;'
|
||||
b'PARAMETER2=Value2,Value3')
|
||||
)
|
||||
|
||||
# We can also parse parameter strings
|
||||
self.assertEqual(
|
||||
Parameters.from_ical('PARAMETER1=Value 1;param2=Value 2'),
|
||||
Parameters({'PARAMETER1': 'Value 1', 'PARAM2': 'Value 2'})
|
||||
)
|
||||
|
||||
# Including empty strings
|
||||
self.assertEqual(Parameters.from_ical('param='),
|
||||
Parameters({'PARAM': ''}))
|
||||
|
||||
# We can also parse parameter strings
|
||||
self.assertEqual(
|
||||
Parameters.from_ical(
|
||||
'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'
|
||||
),
|
||||
Parameters({'MEMBER': ['MAILTO:projectA@host.com',
|
||||
'MAILTO:projectB@host.com']})
|
||||
)
|
||||
|
||||
# We can also parse parameter strings
|
||||
self.assertEqual(
|
||||
Parameters.from_ical('ALTREP="http://www.wiz.org",value4;'
|
||||
'PARAMETER1=Value1;PARAMETER2=Value2,Value3'),
|
||||
Parameters({'PARAMETER1': 'Value1',
|
||||
'ALTREP': ['http://www.wiz.org', 'value4'],
|
||||
'PARAMETER2': ['Value2', 'Value3']})
|
||||
)
|
||||
|
||||
def test_parse_and_access_property_params(self):
|
||||
"""Parse an ics string and access some property parameters then.
|
||||
This is a follow-up of a question received per email.
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
"""This file tests the source code provided by the documentation.
|
||||
|
||||
See
|
||||
- doctest documentation: https://docs.python.org/3/library/doctest.html
|
||||
- Issue 443: https://github.com/collective/icalendar/issues/443
|
||||
|
||||
This file should be tests, too:
|
||||
|
||||
>>> print("Hello World!")
|
||||
Hello World!
|
||||
|
||||
"""
|
||||
import doctest
|
||||
import os
|
||||
import pytest
|
||||
import importlib
|
||||
|
||||
HERE = os.path.dirname(__file__) or "."
|
||||
ICALENDAR_PATH = os.path.dirname(HERE)
|
||||
|
||||
PYTHON_FILES = [
|
||||
os.path.join(dirpath, filename)
|
||||
for dirpath, dirnames, filenames in os.walk(ICALENDAR_PATH)
|
||||
for filename in filenames if filename.lower().endswith(".py")
|
||||
]
|
||||
|
||||
MODULE_NAMES = [
|
||||
"icalendar" + python_file[len(ICALENDAR_PATH):-3].replace("/", ".")
|
||||
for python_file in PYTHON_FILES
|
||||
]
|
||||
|
||||
def test_this_module_is_among_them():
|
||||
assert __name__ in MODULE_NAMES
|
||||
|
||||
@pytest.mark.parametrize("module_name", MODULE_NAMES)
|
||||
def test_docstring_of_python_file(module_name):
|
||||
"""This test runs doctest on the Python module."""
|
||||
module = importlib.import_module(module_name)
|
||||
test_result = doctest.testmod(module, name=module_name)
|
||||
assert test_result.failed == 0, f"{test_result.failed} errors in {module_name}"
|
||||
|
||||
|
||||
# This collection needs to exclude .tox and other subdirectories
|
||||
DOCUMENTATION_PATH = os.path.join(HERE, "../../../")
|
||||
|
||||
DOCUMENT_PATHS = [
|
||||
os.path.join(DOCUMENTATION_PATH, subdir, filename)
|
||||
for subdir in ["docs", "."]
|
||||
for filename in os.listdir(os.path.join(DOCUMENTATION_PATH, subdir))
|
||||
if filename.lower().endswith(".rst")
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("filename", [
|
||||
"README.rst",
|
||||
"index.rst",
|
||||
])
|
||||
def test_files_is_included(filename):
|
||||
assert any(path.endswith(filename) for path in DOCUMENT_PATHS)
|
||||
|
||||
@pytest.mark.parametrize("document", DOCUMENT_PATHS)
|
||||
def test_documentation_file(document):
|
||||
"""This test runs doctest on a documentation file."""
|
||||
test_result = doctest.testfile(document, module_relative=False)
|
||||
assert test_result.failed == 0, f"{test_result.failed} errors in {os.path.basename(document)}"
|
||||
|
||||
|
||||
|
|
@ -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
|
|
@ -30,6 +30,4 @@ class UIDGenerator:
|
|||
host_name = to_unicode(host_name)
|
||||
unique = unique or UIDGenerator.rnd_string()
|
||||
today = to_unicode(vDatetime(datetime.today()).to_ical())
|
||||
return vText('{}-{}@{}'.format(today,
|
||||
unique,
|
||||
host_name))
|
||||
return vText(f'{today}-{unique}@{host_name}')
|
||||
|
|
1
tox.ini
1
tox.ini
|
@ -10,6 +10,7 @@ usedevelop=True
|
|||
deps =
|
||||
pytest
|
||||
coverage
|
||||
hypothesis
|
||||
commands =
|
||||
coverage run --source=src/icalendar --omit=*/tests/* --module pytest []
|
||||
coverage report
|
||||
|
|
Ładowanie…
Reference in New Issue