Merge branch 'master' into issue-318-skip-value-parameter-for-default-date-time

pull/450/head
Nicco Kunzmann 2022-11-02 21:52:37 +00:00
commit e5ae796862
49 zmienionych plików z 936 dodań i 493 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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!
-->

Wyświetl plik

@ -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/*

Wyświetl plik

@ -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)
--------------------

Wyświetl plik

@ -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::

Wyświetl plik

@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Wyświetl plik

@ -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>`__

Wyświetl plik

@ -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()

Wyświetl plik

@ -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

Wyświetl plik

@ -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,

Wyświetl plik

@ -1,4 +1,4 @@
__version__ = '5.0.0a2.dev0'
__version__ = '5.0.1'
from icalendar.cal import (
Calendar,

Wyświetl plik

@ -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(

Wyświetl plik

@ -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.
"""

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
END:VEVENT

Wyświetl plik

@ -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

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=Society\, 2014:that
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=Society\\ 2014:that
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=Society\; 2014:that
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=Society\: 2014:that
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN=that\, that\; %th%%at%\ that\::это\, то\; that\ %th%%at%\:
END:VEVENT

Wyświetl plik

@ -0,0 +1,3 @@
BEGIN:VEVENT
ORGANIZER;CN="Джон Доу":mailto:john.doe@example.org
END:VEVENT

Wyświetl plik

@ -0,0 +1,7 @@
BEGIN:VEVENT
SUMMARY:RDATE period
DTSTART:19961230T020000Z
DTEND:19961230T060000Z
UID:rdate_period
RDATE;VALUE=PERIOD:19970101T180000Z/19970102T070000Z
END:VEVENT

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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'

Wyświetl plik

@ -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)

Wyświetl plik

@ -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')

Wyświetl plik

@ -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

Wyświetl plik

@ -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')

Wyświetl plik

@ -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')

Wyświetl plik

@ -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

Wyświetl plik

@ -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.

Wyświetl plik

@ -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)}"

Wyświetl plik

@ -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

Wyświetl plik

@ -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}')

Wyświetl plik

@ -10,6 +10,7 @@ usedevelop=True
deps =
pytest
coverage
hypothesis
commands =
coverage run --source=src/icalendar --omit=*/tests/* --module pytest []
coverage report