Merge branch 'master' into error_handling_2

pull/331/head
Nicco Kunzmann 2022-08-22 13:53:02 +01:00 zatwierdzone przez GitHub
commit 0080a3942f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
38 zmienionych plików z 681 dodań i 305 usunięć

51
.github/workflows/tests.yml vendored 100644
Wyświetl plik

@ -0,0 +1,51 @@
name: tests
on:
push:
branches: [ main ]
pull_request:
schedule:
- cron: '14 7 * * 0' # run once a week on Sunday
workflow_dispatch:
jobs:
build:
strategy:
matrix:
config:
# [Python version, tox env]
- ["3.8", "py38"]
- ["3.9", "py39"]
- ["3.10", "py310"]
- ["pypy-3.9", "pypy3"]
- ["3.10", "docs"]
- ["3.11.0-rc.1", "py311"]
runs-on: ubuntu-latest
name: ${{ matrix.config[1] }}
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.config[0] }}
- name: Pip cache
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.config[0] }}-
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- name: Test
run: tox -e ${{ matrix.config[1] }}
- name: Coverage
run: |
pip install coveralls coverage-python-version
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
Wyświetl plik

@ -18,4 +18,5 @@ parts/
pip-selfcheck.json
src/icalendar.egg-info/
!.gitattributes
!.github
!.gitignore

Wyświetl plik

@ -25,16 +25,58 @@ Bug fixes:
- *add item here*
4.0.9 (unreleased)
------------------
5.0.0a2 (unreleased)
--------------------
Minor changes:
- removed deprecated test checks [tuergeist]
Breaking changes:
- *add item here*
- Require Python 3.8 as minimum Python version. [maurits]
- icalenar now takes a ics file directly as an input
- icalendar's output is different
New features:
- *add item here*
- icalendar utility outputs a 'Duration' row
- icalendar can take multiple ics files as an input
Bug fixes:
- Changed tools.UIDGenerator instance methods to static methods
Ref: #345
[spralja]
- proper handling of datetime objects with `tzinfo` generated through zoneinfo.ZoneInfo.
Ref: #334
Fixes: #333
[tobixen]
- Timestamps in UTC does not need tzid
Ref: #338
Fixes: #335
[tobixen]
5.0.0a1 (2022-07-11)
--------------------
Breaking changes:
- Drop support for Python 3.4, 3.5 and PyPy2. [maurits]
New features:
- Document development setup
Ref: #358
[niccokunzmann]
Bug fixes:
- Test with GitHub Actions. [maurits]
4.0.9 (2021-10-16)
------------------
Bug fixes:
@ -80,7 +122,6 @@ Bug fixes:
- Fixed a docs issue related to building on Read the Docs [davidfischer]
4.0.4 (2019-11-25)
------------------
@ -666,9 +707,9 @@ Fixes:
- created sphinx documentation and started documenting development and goals.
[garbas]
- hook out github repository to http://readthedocs.org service so sphinx
- hook out github repository to https://readthedocs.org service so sphinx
documentation is generated on each commit (for master). Documentation can be
visible on: http://readthedocs.org/docs/icalendar/en/latest/
visible on: https://icalendar.readthedocs.io/en/latest/
[garbas]

Wyświetl plik

@ -10,7 +10,7 @@ These are some contribution examples
- Extending the documentation.
- Sponsor a Sprint (http://plone.org/events/sprints/whatis).
- Sponsor a Sprint (https://plone.org/events/sprints/whatis).
For pull requests, keep this in mind
@ -21,3 +21,11 @@ For pull requests, keep this in mind
- Describe your change in CHANGES.rst
- Add yourself to the docs/credits.rst
Development Setup
-----------------
If you would like to setup icalendar to
contribute changes, the `Installation Section
<https://icalendar.readthedocs.io/en/latest/install.html>`_
should help you further.

Wyświetl plik

@ -16,10 +16,21 @@ files.
----
.. image:: https://badge.fury.io/py/icalendar.svg
:target: https://pypi.org/project/icalendar/
:alt: Python Package Version on PyPI
.. image:: https://travis-ci.org/collective/icalendar.svg?branch=master
:target: https://travis-ci.org/collective/icalendar
.. image:: https://img.shields.io/pypi/dm/icalendar.svg
:target: https://pypi.org/project/icalendar/#files
:alt: Downloads from PyPI
.. image:: https://img.shields.io/github/workflow/status/collective/icalendar/tests/master?label=master&logo=github
:target: https://github.com/collective/icalendar/actions/workflows/tests.yml?query=branch%3Amaster
:alt: GitHub Actions build status for master
.. image:: https://img.shields.io/github/workflow/status/collective/icalendar/tests/4.x?label=4.x&logo=github
:target: https://github.com/collective/icalendar/actions/workflows/tests.yml?query=branch%3A4.x++
:alt: GitHub Actions build status for 4.x
.. _`icalendar`: https://pypi.org/project/icalendar/
.. _`RFC 5545`: https://www.ietf.org/rfc/rfc5545.txt
@ -27,9 +38,26 @@ files.
.. _`pytz`: https://pypi.org/project/pytz/
.. _`BSD`: https://github.com/collective/icalendar/issues/2
Versions and Compatibility
--------------------------
``icalendar`` is a critical project used by many. It has been there for a long time and maintaining
long-term compatibility with projects conflicts partially with providing and using the features that
the latest Python versions bring.
Since we pour more `effort into maintaining and developing icalendar <https://github.com/collective/icalendar/discussions/360>`__,
we split the project into two:
- `Branch 4.x <https://github.com/collective/icalendar/tree/4.x>`__ with maximum compatibility to Python versions ``2.7`` and ``3.4+``, ``PyPy2`` and ``PyPy3``.
- `Branch master <https://github.com/collective/icalendar/>`__ with the compatibility to Python versions ``3.7+`` and ``PyPy3``.
We expect the ``master`` branch with versions ``5+`` receive the latest updates and features,
and the ``4.x`` branch the subset of security and bug fixes only.
We recomment migrating to later Python versions and also providing feedback if you depend on the ``4.x`` features.
Related projects
================
* `icalevents <https://github.com/irgangla/icalevents>`_. It is built on top of icalendar and allows you to query iCal files and get the events happening on specific dates. It manages recurrent events as well.
* `recurring-ical-events <https://pypi.org/project/recurring-ical-events/>`_. Library to query an ``ICalendar`` object for events happening at a certain date or within a certain time.
* `x-wr-timezone <https://pypi.org/project/x-wr-timezone/>`_. Library to make ``ICalendar`` objects and files using the non-standard ``X-WR-TIMEZONE`` compliant with the standard (RFC 5545).

Wyświetl plik

@ -111,7 +111,7 @@ cmd = [sys.executable, '-c',
find_links = os.environ.get(
'bootstrap-testing-find-links',
options.find_links or
('http://downloads.buildout.org/'
('https://downloads.buildout.org/'
if options.accept_buildout_test_releases else None)
)
if find_links:

Wyświetl plik

@ -45,7 +45,8 @@ clean:
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
@echo "Build finished. The HTML pages are in docs/$(BUILDDIR)/html."
rm -rf build
manual: *.rst
$(SPHINXBUILD) -b html -t manual . manual

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# icalendar documentation build configuration file
import pkg_resources
import datetime
@ -26,9 +25,9 @@ extensions = [
source_suffix = '.rst'
master_doc = 'index'
project = u'icalendar'
project = 'icalendar'
this_year = datetime.date.today().year
copyright = u'{}, Plone Foundation'.format(this_year)
copyright = f'{this_year}, Plone Foundation'
version = pkg_resources.get_distribution('icalendar').version
release = version
@ -38,6 +37,6 @@ pygments_style = 'sphinx'
htmlhelp_basename = 'icalendardoc'
man_pages = [
('index', 'icalendar', u'icalendar Documentation',
[u'Plone Foundation'], 1)
('index', 'icalendar', 'icalendar Documentation',
['Plone Foundation'], 1)
]

Wyświetl plik

@ -56,6 +56,11 @@ icalendar contributors
- Clive Stevens <clivest2@gmail.com>
- Dalton Durst <github@daltondur.st>
- Kamil Mańkowski <kam193@wp.pl>
- Tobias Brox <tobias@redpill-linpro.com>
- `Nicco Kunzmann <https://github.com/niccokunzmann>`_
- Robert Spralja <robert.spralja@gmail.com>
- Maurits van Rees <maurits@vanrees.org>
- jacadzaca <vitouejj@gmail.com>
Find out who contributed::

Wyświetl plik

@ -10,6 +10,109 @@ package, like this::
>>> import icalendar
Development Setup
-----------------
To start contributing changes to icalendar,
you can clone the project to your file system
using Git.
You can `fork <https://github.com/collective/icalendar/fork>`_
the project first and clone your fork, too.
.. code-block:: bash
git clone https://github.com/collective/icalendar.git
cd icalendar
Installing Python
-----------------
You will need a version of Python installed to run the tests
and execute the code.
The latest version of Python 3 should work and will be enough
to get you started.
If you like to run the tests with different Python versions,
the following setup proecess should work the same.
Install Tox
-----------
First, install `tox <https://pypi.org/project/tox/>`_..
.. code-block:: bash
pip install tox
From now on, tox will manage Python versions and
test commands for you.
Running Tests
-------------
``tox`` manages all test environments in all Python versions.
To run all tests in all environments, simply run ``tox``
.. code-block:: bash
tox
You may not have all Python versions installed or
you may want to run a specific one.
Have a look at the `documentation
<https://tox.wiki/en/latest/example/general.html#selecting-one-or-more-environments-to-run-tests-against>`__.
This is how you can run ``tox`` with Python 3.9:
.. code-block:: bash
tox -e py39
Accessing a ``tox`` environment
-------------------------------
If you like to enter a specific tox environment,
you can do this:
.. code-block:: bash
source .tox/py39/bin/activate
Install ``icalendar`` Manually
-------------------------------
The best way to test the package is to use ``tox`` as
described above.
If for some reason you cannot install ``tox``, you can
go ahead with the following section using your
installed version of Python and ``pip``.
If for example, you would like to use your local copy of
icalendar in another Python environment,
this section explains how to do it.
You can install the local copy of ``icalendar`` with ``pip``
like this:
.. code-block:: bash
cd icalendar
python -m pip install -e .
This installs the module and dependencies in your
Python environment so that you can access local changes.
If tox fails to install ``icalendar`` during its first run,
you can activate the environment in the ``.tox`` folder and
manually setup ``icalendar`` like this.
Try it out:
.. code-block:: python
Python 3.9.5 (default, Nov 23 2021, 15:27:38)
Type "help", "copyright", "credits" or "license" for more information.
>>> import icalendar
>>> icalendar.__version__
'4.0.10.dev0'
Building the documentation locally
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -18,10 +121,7 @@ To build the documentation follow these steps:
.. code-block:: bash
$ git clone https://github.com/collective/icalendar.git
$ cd icalendar
$ virtualenv-2.7 .
$ source bin/activate
$ source .tox/py39/bin/activate
$ pip install -r requirements_docs.txt
$ cd docs
$ make html
@ -29,3 +129,10 @@ To build the documentation follow these steps:
You can now open the output from ``_build/html/index.html``. To build the
presentation-version use ``make presentation`` instead of ``make html``. You
can open the presentation at ``presentation/index.html``.
You can also use ``tox`` to build the documentation:
.. code-block:: bash
cd icalendar
tox -e docs

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import codecs
import setuptools
import re
@ -36,13 +35,6 @@ setuptools.setup(
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
@ -59,7 +51,7 @@ setuptools.setup(
package_dir={'': 'src'},
include_package_data=True,
zip_safe=False,
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
python_requires=">=3.8",
install_requires=install_requires,
entry_points = {'console_scripts': ['icalendar = icalendar.cli:main']},
extras_require={

Wyświetl plik

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
__version__ = '4.0.9.dev0'
__version__ = '5.0.0a2.dev0'
from icalendar.cal import (
Calendar,

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Calendar is a dictionary like Python object that can render itself as VCAL
files according to rfc2445.
@ -20,7 +19,6 @@ import pytz
import dateutil.rrule, dateutil.tz
from pytz.tzinfo import DstTzInfo
from icalendar.compat import unicode_type
######################################
@ -34,7 +32,7 @@ class ComponentFactory(CaselessDict):
def __init__(self, *args, **kwargs):
"""Set keys to upper for initial dict.
"""
super(ComponentFactory, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self['VEVENT'] = Event
self['VTODO'] = Todo
self['VJOURNAL'] = Journal
@ -60,7 +58,7 @@ _marker = []
class Component(CaselessDict):
"""Component is the base object for calendar, Event and the other
components defined in RFC 2445. normally you will not use this class
directy, but rather one of the subclasses.
directly, but rather one of the subclasses.
"""
name = None # should be defined in each component
@ -79,7 +77,7 @@ class Component(CaselessDict):
def __init__(self, *args, **kwargs):
"""Set keys to upper for initial dict.
"""
super(Component, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# set parameters here for properties that use non-default values
self.subcomponents = [] # Components can be nested.
self.errors = [] # If we ignored exception(s) while
@ -91,7 +89,7 @@ class Component(CaselessDict):
#
# If the parser is too strict it might prevent parsing erroneous but
# otherwise compliant properties. So the parser is pretty lax, but it is
# possible to test for non-complience by calling this method.
# possible to test for non-compliance by calling this method.
# """
# return name in not_compliant
@ -345,7 +343,7 @@ class Component(CaselessDict):
component = stack[-1] if stack else None
if not component or not component.ignore_exceptions:
raise
component.errors.append((None, unicode_type(e)))
component.errors.append((None, str(e)))
continue
uname = name.upper()
@ -410,22 +408,27 @@ class Component(CaselessDict):
except ValueError as e:
if not component.ignore_exceptions:
raise
component.errors.append((uname, unicode_type(e)))
# fallback to vText and store the original value
vals = types_factory['text'](vals)
vals.params = params
component.add(name, vals, encode=0)
# component.errors.append((uname, unicode_type(e)))
# # fallback to vText and store the original value
# vals = types_factory['text'](vals)
#
# vals.params = params
# component.add(name, vals, encode=0)
component.errors.append((uname, str(e)))
component.add(name, None, encode=0)
else:
vals.params = params
component.add(name, vals, encode=0)
if multiple:
return comps
if len(comps) > 1:
raise ValueError('Found multiple components where '
'only one is allowed: {st!r}'.format(**locals()))
raise ValueError(f'Found multiple components where '
f'only one is allowed: {st!r}')
if len(comps) < 1:
raise ValueError('Found no components where '
'exactly one is required: '
'{st!r}'.format(**locals()))
raise ValueError(f'Found no components where '
f'exactly one is required: '
f'{st!r}')
return comps[0]
def content_line(self, name, value, sorted=True):
@ -457,7 +460,7 @@ class Component(CaselessDict):
"""String representation of class with all of it's subcomponents.
"""
subs = ', '.join([str(it) for it in self.subcomponents])
return '%s(%s%s)' % (
return '{}({}{})'.format(
self.name or type(self).__name__,
dict(self),
', %s' % subs if subs else ''
@ -634,7 +637,7 @@ class Timezone(Component):
tzname = component['TZNAME'].encode('ascii', 'replace')
tzname = self._make_unique_tzname(tzname, tznames)
except KeyError:
tzname = '{0}_{1}_{2}_{3}'.format(
tzname = '{}_{}_{}_{}'.format(
zone,
component['DTSTART'].to_ical().decode('utf-8'),
component['TZOFFSETFROM'].to_ical(), # for whatever reason this is str/unicode

Wyświetl plik

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from icalendar.compat import iteritems
from icalendar.parser_tools import to_unicode
from collections import OrderedDict
@ -30,62 +28,62 @@ class CaselessDict(OrderedDict):
def __init__(self, *args, **kwargs):
"""Set keys to upper for initial dict.
"""
super(CaselessDict, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
for key, value in self.items():
key_upper = to_unicode(key).upper()
if key != key_upper:
super(CaselessDict, self).__delitem__(key)
super().__delitem__(key)
self[key_upper] = value
def __getitem__(self, key):
key = to_unicode(key)
return super(CaselessDict, self).__getitem__(key.upper())
return super().__getitem__(key.upper())
def __setitem__(self, key, value):
key = to_unicode(key)
super(CaselessDict, self).__setitem__(key.upper(), value)
super().__setitem__(key.upper(), value)
def __delitem__(self, key):
key = to_unicode(key)
super(CaselessDict, self).__delitem__(key.upper())
super().__delitem__(key.upper())
def __contains__(self, key):
key = to_unicode(key)
return super(CaselessDict, self).__contains__(key.upper())
return super().__contains__(key.upper())
def get(self, key, default=None):
key = to_unicode(key)
return super(CaselessDict, self).get(key.upper(), default)
return super().get(key.upper(), default)
def setdefault(self, key, value=None):
key = to_unicode(key)
return super(CaselessDict, self).setdefault(key.upper(), value)
return super().setdefault(key.upper(), value)
def pop(self, key, default=None):
key = to_unicode(key)
return super(CaselessDict, self).pop(key.upper(), default)
return super().pop(key.upper(), default)
def popitem(self):
return super(CaselessDict, self).popitem()
return super().popitem()
def has_key(self, key):
key = to_unicode(key)
return super(CaselessDict, self).__contains__(key.upper())
return super().__contains__(key.upper())
def update(self, *args, **kwargs):
# Multiple keys where key1.upper() == key2.upper() will be lost.
mappings = list(args) + [kwargs]
for mapping in mappings:
if hasattr(mapping, 'items'):
mapping = iteritems(mapping)
mapping = iter(mapping.items())
for key, value in mapping:
self[key] = value
def copy(self):
return type(self)(super(CaselessDict, self).copy())
return type(self)(super().copy())
def __repr__(self):
return '%s(%s)' % (type(self).__name__, dict(self))
return f'{type(self).__name__}({dict(self)})'
def __eq__(self, other):
return self is other or dict(self.items()) == dict(other.items())

128
src/icalendar/cli.py 100644 → 100755
Wyświetl plik

@ -1,43 +1,24 @@
# -*- coding: utf-8 -*-
"""iCalendar utility"""
from __future__ import unicode_literals
import argparse
#!/usr/bin/env python3
"""utility program that allows user to preview calendar's events"""
import sys
import pathlib
import argparse
from datetime import datetime
from . import Calendar, __version__
_template = """Organiser: {organiser}
Attendees:
{attendees}
Summary: {summary}
When: {time_from}-{time_to}
Location: {location}
Comment: {comment}
Description:
{description}
"""
from icalendar import Calendar, __version__
def _format_name(address):
"""Retrieve the e-mail and optionally the name from an address.
"""Retrieve the e-mail and the name from an address.
:arg vCalAddress address: An address object.
:arg an address object, e.g. mailto:test@test.test
:returns str: The name and optionally the e-mail address.
:returns str: The name and the e-mail address.
"""
if not address:
email = address.split(':')[-1]
name = email.split('@')[0]
if not email:
return ''
email = address.title().split(':')[1]
if 'cn' in address.params:
return '{} <{}>'.format(address.params['cn'], email)
return email
return f"{name} <{email}>"
def _format_attendees(attendees):
@ -47,65 +28,54 @@ def _format_attendees(attendees):
:returns str: Formatted list of attendees.
"""
if type(attendees) == list:
return '\n '.join(map(_format_name, attendees))
if isinstance(attendees, list):
return '\n'.join(map(lambda s: s.rjust(len(s) + 5), map(_format_name, attendees)))
return _format_name(attendees)
def view(input_handle, output_handle):
def view(event):
"""Make a human readable summary of an iCalendar file.
:arg stream handle: Open readable handle to an iCalendar file.
:returns str: Human readable summary.
"""
cal = Calendar.from_ical(input_handle.read())
summary = event.get('summary', default='')
organizer = _format_name(event.get('organizer', default=''))
attendees = _format_attendees(event.get('attendee', default=[]))
location = event.get('location', default='')
comment = event.get('comment', '')
description = event.get('description', '').split('\n')
description = '\n'.join(map(lambda s: s.rjust(len(s) + 5), description))
for event in cal.walk('vevent'):
output_handle.write(_template.format(
organiser=_format_name(event.get('organizer', '')),
attendees=_format_attendees(event.get('attendee')),
summary=event.get('summary', ''),
time_from=datetime.strftime(
event.get('dtstart').dt, '%a %d %b %Y %H:%M'),
time_to=datetime.strftime(event.get('dtend').dt, '%H:%M'),
location=event.get('location', ''),
comment=event.get('comment', ''),
description=event.get('description', '')).encode('utf-8'))
start = event.decoded('dtstart')
end = event.decoded('dtend', default=start)
duration = end - start
start = start.astimezone(start.tzinfo).strftime('%c')
end = end.astimezone(end.tzinfo).strftime('%c')
return f""" Organizer: {organizer}
Attendees:
{attendees}
Summary : {summary}
Starts : {start}
End : {end}
Duration : {duration}
Location : {location}
Comment : {comment}
Description:
{description}"""
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=__doc__)
parser.add_argument(
'-v', '--version', action='version',
version='{} version {}'.format(parser.prog, __version__))
# This seems a bit of an overkill now, but we will probably add more
# functionality later, e.g., iCalendar to JSON / YAML and vice versa.
subparsers = parser.add_subparsers(dest='subcommand')
subparser = subparsers.add_parser(
'view', description=view.__doc__.split('\n\n')[0])
subparser.add_argument(
'input_handle', metavar='INPUT', type=argparse.FileType('r'),
help='iCalendar file')
subparser.add_argument(
'-o', dest='output_handle', metavar='OUTPUT',
type=argparse.FileType('w'), default=sys.stdout,
help='output file (default=<stdout>)')
subparser.set_defaults(func=view)
args = parser.parse_args()
try:
args.func(**{k: v for k, v in vars(args).items()
if k not in ('func', 'subcommand')})
except ValueError as error:
parser.error(error)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('calendar_files', nargs='+', type=pathlib.Path)
parser.add_argument('--output', '-o', type=argparse.FileType('w'), default=sys.stdout, help='output file')
parser.add_argument('-v', '--version', action='version', version=f'{parser.prog} version {__version__}')
argv = parser.parse_args()
for calendar_file in argv.calendar_files:
with open(calendar_file) as f:
calendar = Calendar.from_ical(f.read())
for event in calendar.walk('vevent'):
argv.output.write(view(event) + '\n\n')
if __name__ == '__main__':
main()

Wyświetl plik

@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
import sys
if sys.version_info[0] == 2: # pragma: no cover
unicode_type = unicode
bytes_type = str
iteritems = lambda d, *args, **kwargs: iter(d.iteritems(*args, **kwargs))
else: # pragma: no cover
unicode_type = str
bytes_type = bytes
iteritems = lambda d, *args, **kwargs: iter(d.items(*args, **kwargs))

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""This module parses and generates contentlines as defined in RFC 2445
(iCalendar), but will probably work for other MIME types with similar syntax.
Eg. RFC 2426 (vCard)
@ -6,9 +5,7 @@ Eg. RFC 2426 (vCard)
It is stupid in the sense that it treats the content purely as strings. No type
conversion is attempted.
"""
from __future__ import unicode_literals
from icalendar import compat
from icalendar.caselessdict import CaselessDict
from icalendar.parser_tools import DEFAULT_ENCODING
from icalendar.parser_tools import SEQUENCE_TYPES
@ -20,7 +17,7 @@ import re
def escape_char(text):
"""Format value according to iCalendar TEXT escaping rules.
"""
assert isinstance(text, (compat.unicode_type, compat.bytes_type))
assert isinstance(text, (str, bytes))
# NOTE: ORDER MATTERS!
return text.replace(r'\N', '\n')\
.replace('\\', '\\\\')\
@ -31,16 +28,16 @@ def escape_char(text):
def unescape_char(text):
assert isinstance(text, (compat.unicode_type, compat.bytes_type))
assert isinstance(text, (str, bytes))
# NOTE: ORDER MATTERS!
if isinstance(text, compat.unicode_type):
if isinstance(text, str):
return text.replace('\\N', '\\n')\
.replace('\r\n', '\n')\
.replace('\\n', '\n')\
.replace('\\,', ',')\
.replace('\\;', ';')\
.replace('\\\\', '\\')
elif isinstance(text, compat.bytes_type):
elif isinstance(text, bytes):
return text.replace(b'\\N', b'\\n')\
.replace(b'\r\n', b'\n')\
.replace(b'\n', b'\n')\
@ -53,15 +50,14 @@ def tzid_from_dt(dt):
tzid = None
if hasattr(dt.tzinfo, 'zone'):
tzid = dt.tzinfo.zone # pytz implementation
elif hasattr(dt.tzinfo, 'key'):
tzid = dt.tzinfo.key # ZoneInfo implementation
elif hasattr(dt.tzinfo, 'tzname'):
try:
tzid = dt.tzinfo.tzname(dt) # dateutil implementation
except AttributeError:
# No tzid available
pass
# 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
@ -71,7 +67,7 @@ def foldline(line, limit=75, fold_sep='\r\n '):
immediately followed by a single linear white-space character (i.e.,
SPACE or HTAB).
"""
assert isinstance(line, compat.unicode_type)
assert isinstance(line, str)
assert '\n' not in line
# Use a fast and simple variant for the common case that line is all ASCII.
@ -220,7 +216,7 @@ class Parameters(CaselessDict):
for key, value in items:
value = param_value(value)
if isinstance(value, compat.unicode_type):
if isinstance(value, str):
value = value.encode(DEFAULT_ENCODING)
# CaselessDict keys are always unicode
key = key.upper().encode(DEFAULT_ENCODING)
@ -285,7 +281,7 @@ def unescape_list_or_string(val):
#########################################
# parsing and generation of content lines
class Contentline(compat.unicode_type):
class Contentline(str):
"""A content line is basically a string that can be folded and parsed into
parts.
"""
@ -293,7 +289,7 @@ class Contentline(compat.unicode_type):
value = to_unicode(value, encoding=encoding)
assert '\n' not in value, ('Content line can not contain unescaped '
'new line characters.')
self = super(Contentline, cls).__new__(cls, value)
self = super().__new__(cls, value)
self.strict = strict
return self
@ -315,8 +311,8 @@ class Contentline(compat.unicode_type):
values = to_unicode(values)
if params:
params = to_unicode(params.to_ical(sorted=sorted))
return cls('%s;%s:%s' % (name, params, values))
return cls('%s:%s' % (name, values))
return cls(f'{name};{params}:{values}')
return cls(f'{name}:{values}')
def parts(self):
"""Split the content line up into (name, parameters, values) parts.
@ -344,7 +340,7 @@ class Contentline(compat.unicode_type):
strict=self.strict)
params = Parameters(
(unescape_string(key), unescape_list_or_string(value))
for key, value in compat.iteritems(params)
for key, value in iter(params.items())
)
values = unescape_string(st[value_split + 1:])
return (name, params, values)

Wyświetl plik

@ -1,7 +1,3 @@
# -*- coding: utf-8 -*-
from icalendar import compat
SEQUENCE_TYPES = (list, tuple)
DEFAULT_ENCODING = 'utf-8'
@ -9,9 +5,9 @@ DEFAULT_ENCODING = 'utf-8'
def to_unicode(value, encoding='utf-8'):
"""Converts a value to unicode, even if it is already a unicode string.
"""
if isinstance(value, compat.unicode_type):
if isinstance(value, str):
return value
elif isinstance(value, compat.bytes_type):
elif isinstance(value, bytes):
try:
value = value.decode(encoding)
except UnicodeDecodeError:
@ -23,11 +19,11 @@ def data_encode(data, encoding=DEFAULT_ENCODING):
"""Encode all datastructures to the given encoding.
Currently unicode strings, dicts and lists are supported.
"""
# http://stackoverflow.com/questions/1254454/fastest-way-to-convert-a-dicts-keys-values-from-unicode-to-str
if isinstance(data, compat.unicode_type):
# https://stackoverflow.com/questions/1254454/fastest-way-to-convert-a-dicts-keys-values-from-unicode-to-str
if isinstance(data, str):
return data.encode(encoding)
elif isinstance(data, dict):
return dict(map(data_encode, compat.iteritems(data)))
return dict(map(data_encode, iter(data.items())))
elif isinstance(data, list) or isinstance(data, tuple):
return list(map(data_encode, data))
else:

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""This module contains the parser/generators (or coders/encoders if you
prefer) for the classes/datatypes that are used in iCalendar:
@ -46,7 +45,6 @@ try:
except ImportError:
tzutc = None
from icalendar import compat
from icalendar.caselessdict import CaselessDict
from icalendar.parser import Parameters
from icalendar.parser import escape_char
@ -67,7 +65,7 @@ import time as _time
DATE_PART = r'(\d+)D'
TIME_PART = r'T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?'
DATETIME_PART = '(?:%s)?(?:%s)?' % (DATE_PART, TIME_PART)
DATETIME_PART = f'(?:{DATE_PART})?(?:{TIME_PART})?'
WEEKS_PART = r'(\d+)W'
DURATION_REGEX = re.compile(r'([-+]?)P(?:%s|%s)$'
% (WEEKS_PART, DATETIME_PART))
@ -133,7 +131,7 @@ class LocalTimezone(tzinfo):
return tt.tm_isdst > 0
class vBinary(object):
class vBinary:
"""Binary property values are base 64 encoded.
"""
@ -161,7 +159,7 @@ class vBoolean(int):
BOOL_MAP = CaselessDict({'true': True, 'false': False})
def __new__(cls, *args, **kwargs):
self = super(vBoolean, cls).__new__(cls, *args, **kwargs)
self = super().__new__(cls, *args, **kwargs)
self.params = Parameters()
return self
@ -178,12 +176,12 @@ class vBoolean(int):
raise ValueError("Expected 'TRUE' or 'FALSE'. Got %s" % ical)
class vCalAddress(compat.unicode_type):
class vCalAddress(str):
"""This just returns an unquoted string.
"""
def __new__(cls, value, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
self = super(vCalAddress, cls).__new__(cls, value)
self = super().__new__(cls, value)
self.params = Parameters()
return self
@ -202,12 +200,12 @@ class vFloat(float):
"""Just a float.
"""
def __new__(cls, *args, **kwargs):
self = super(vFloat, cls).__new__(cls, *args, **kwargs)
self = super().__new__(cls, *args, **kwargs)
self.params = Parameters()
return self
def to_ical(self):
return compat.unicode_type(self).encode('utf-8')
return str(self).encode('utf-8')
@classmethod
def from_ical(cls, ical):
@ -221,12 +219,12 @@ class vInt(int):
"""Just an int.
"""
def __new__(cls, *args, **kwargs):
self = super(vInt, cls).__new__(cls, *args, **kwargs)
self = super().__new__(cls, *args, **kwargs)
self.params = Parameters()
return self
def to_ical(self):
return compat.unicode_type(self).encode('utf-8')
return str(self).encode('utf-8')
@classmethod
def from_ical(cls, ical):
@ -236,7 +234,7 @@ class vInt(int):
raise ValueError('Expected int, got: %s' % ical)
class vDDDLists(object):
class vDDDLists:
"""A list of vDDDTypes values.
"""
def __init__(self, dt_list, type_class=None):
@ -280,7 +278,7 @@ class vDDDLists(object):
out.append(unit_type.from_ical(ical_dt, timezone=timezone))
return out
class vCategory(object):
class vCategory:
def __init__(self, c_list):
if not hasattr(c_list, '__iter__'):
@ -298,7 +296,7 @@ class vCategory(object):
return out
class vDDDTypes(object):
class vDDDTypes:
"""A combined Datetime, Date or Duration parser/generator. Their format
cannot be confused, and often values can be of either types.
So this is practical.
@ -318,12 +316,9 @@ class vDDDTypes(object):
if type(dt) in (datetime, time) and hasattr(dt, 'tzinfo'):
tzinfo = dt.tzinfo
if tzinfo is not pytz.utc and\
(tzutc is None or not isinstance(tzinfo, tzutc)):
# set the timezone as a parameter to the property
tzid = tzid_from_dt(dt)
if tzid:
self.params.update({'TZID': tzid})
tzid = tzid_from_dt(dt)
if tzid != 'UTC':
self.params.update({'TZID': tzid})
self.dt = dt
def to_ical(self):
@ -339,7 +334,7 @@ class vDDDTypes(object):
elif type(dt) is tuple and len(dt) == 2:
return vPeriod(dt).to_ical()
else:
raise ValueError('Unknown date type: {}'.format(type(dt)))
raise ValueError(f'Unknown date type: {type(dt)}')
@classmethod
def from_ical(cls, ical, timezone=None):
@ -363,7 +358,7 @@ class vDDDTypes(object):
)
class vDate(object):
class vDate:
"""Render and generates iCalendar date format.
"""
def __init__(self, dt):
@ -394,7 +389,7 @@ class vDate(object):
raise ValueError("Wrong date format '%s'" % ical)
class vDatetime(object):
class vDatetime:
"""Render and generates icalendar datetime format.
vDatetime is timezone aware and uses the pytz library, an implementation of
@ -474,7 +469,7 @@ class vDatetime(object):
raise ValueError("Wrong datetime format '%s'" % ical)
class vDuration(object):
class vDuration:
"""Subclass of timedelta that renders itself in the iCalendar DURATION
format.
"""
@ -510,12 +505,12 @@ class vDuration(object):
if seconds:
timepart += "%dS" % seconds
if td.days == 0 and timepart:
return (compat.unicode_type(sign).encode('utf-8') + b'P' +
compat.unicode_type(timepart).encode('utf-8'))
return (str(sign).encode('utf-8') + b'P' +
str(timepart).encode('utf-8'))
else:
return (compat.unicode_type(sign).encode('utf-8') + b'P' +
compat.unicode_type(abs(td.days)).encode('utf-8') +
b'D' + compat.unicode_type(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'))
@classmethod
def from_ical(cls, ical):
@ -538,7 +533,7 @@ class vDuration(object):
raise ValueError('Invalid iCalendar duration: %s' % ical)
class vPeriod(object):
class vPeriod:
"""A precise period of time.
"""
def __init__(self, per):
@ -607,10 +602,10 @@ class vPeriod(object):
p = (self.start, self.duration)
else:
p = (self.start, self.end)
return 'vPeriod(%r)' % (p, )
return f'vPeriod({p!r})'
class vWeekday(compat.unicode_type):
class vWeekday(str):
"""This returns an unquoted weekday abbrevation.
"""
week_days = CaselessDict({
@ -619,7 +614,7 @@ class vWeekday(compat.unicode_type):
def __new__(cls, value, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
self = super(vWeekday, cls).__new__(cls, value)
self = super().__new__(cls, value)
match = WEEKDAY_RULE.match(self)
if match is None:
raise ValueError('Expected weekday abbrevation, got: %s' % self)
@ -644,7 +639,7 @@ class vWeekday(compat.unicode_type):
raise ValueError('Expected weekday abbrevation, got: %s' % ical)
class vFrequency(compat.unicode_type):
class vFrequency(str):
"""A simple class that catches illegal values.
"""
@ -660,7 +655,7 @@ class vFrequency(compat.unicode_type):
def __new__(cls, value, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
self = super(vFrequency, cls).__new__(cls, value)
self = super().__new__(cls, value)
if self not in vFrequency.frequencies:
raise ValueError('Expected frequency, got: %s' % self)
self.params = Parameters()
@ -709,7 +704,7 @@ class vRecur(CaselessDict):
})
def __init__(self, *args, **kwargs):
super(vRecur, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.params = Parameters()
def to_ical(self):
@ -751,13 +746,13 @@ class vRecur(CaselessDict):
raise ValueError('Error in recurrence rule: %s' % ical)
class vText(compat.unicode_type):
class vText(str):
"""Simple text.
"""
def __new__(cls, value, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
self = super(vText, cls).__new__(cls, value)
self = super().__new__(cls, value)
self.encoding = encoding
self.params = Parameters()
return self
@ -774,7 +769,7 @@ class vText(compat.unicode_type):
return cls(ical_unesc)
class vTime(object):
class vTime:
"""Render and generates iCalendar time format.
"""
@ -810,13 +805,13 @@ class vTime(object):
raise ValueError("Expected time, got: '%s'" % ical)
class vUri(compat.unicode_type):
class vUri(str):
"""Uniform resource identifier is basically just an unquoted string.
"""
def __new__(cls, value, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
self = super(vUri, cls).__new__(cls, value)
self = super().__new__(cls, value)
self.params = Parameters()
return self
@ -831,7 +826,7 @@ class vUri(compat.unicode_type):
raise ValueError('Expected , got: %s' % ical)
class vGeo(object):
class vGeo:
"""A special type that is only indirectly defined in the rfc.
"""
@ -848,7 +843,7 @@ class vGeo(object):
self.params = Parameters()
def to_ical(self):
return '%s;%s' % (self.latitude, self.longitude)
return f'{self.latitude};{self.longitude}'
@staticmethod
def from_ical(ical):
@ -859,7 +854,7 @@ class vGeo(object):
raise ValueError("Expected 'float;float' , got: %s" % ical)
class vUTCOffset(object):
class vUTCOffset:
"""Renders itself as a utc offset.
"""
@ -915,14 +910,14 @@ class vUTCOffset(object):
return offset
class vInline(compat.unicode_type):
class vInline(str):
"""This is an especially dumb class that just holds raw unparsed text and
has parameters. Conversion of inline values are handled by the Component
class, so no further processing is needed.
"""
def __new__(cls, value, encoding=DEFAULT_ENCODING):
value = to_unicode(value, encoding=encoding)
self = super(vInline, cls).__new__(cls, value)
self = super().__new__(cls, value)
self.params = Parameters()
return self
@ -944,7 +939,7 @@ class TypesFactory(CaselessDict):
def __init__(self, *args, **kwargs):
"Set keys to upper for initial dict"
super(TypesFactory, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.all_types = (
vBinary,
vBoolean,

Wyświetl plik

@ -0,0 +1,73 @@
import unittest
from icalendar import Calendar, cli
INPUT = '''
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
SUMMARY:Test Summary
ORGANIZER:organizer@test.test
ATTENDEE:attendee1@example.com
ATTENDEE:attendee2@test.test
COMMENT:Comment
DTSTART;TZID=Europe/Warsaw:20220820T103400
DTEND;TZID=Europe/Warsaw:20220820T113400
LOCATION:New Amsterdam, 1000 Sunrise Test Street
DESCRIPTION: Test Description
END:VEVENT
BEGIN:VEVENT
ORGANIZER:organizer@test.test
ATTENDEE:attendee1@example.com
ATTENDEE:attendee2@test.test
SUMMARY:Test summury
DTSTART;TZID=Europe/Warsaw:20220820T200000
DTEND;TZID=Europe/Warsaw:20220820T203000
LOCATION:New Amsterdam, 1010 Test Street
DESCRIPTION:Test Description\\nThis one is multiline
END:VEVENT
END:VCALENDAR
'''
PROPER_OUTPUT = ''' Organizer: organizer <organizer@test.test>
Attendees:
attendee1 <attendee1@example.com>
attendee2 <attendee2@test.test>
Summary : Test Summary
Starts : Sat Aug 20 10:34:00 2022
End : Sat Aug 20 11:34:00 2022
Duration : 1:00:00
Location : New Amsterdam, 1000 Sunrise Test Street
Comment : Comment
Description:
Test Description
Organizer: organizer <organizer@test.test>
Attendees:
attendee1 <attendee1@example.com>
attendee2 <attendee2@test.test>
Summary : Test summury
Starts : Sat Aug 20 20:00:00 2022
End : Sat Aug 20 20:30:00 2022
Duration : 0:30:00
Location : New Amsterdam, 1010 Test Street
Comment :
Description:
Test Description
This one is multiline
'''
class CLIToolTest(unittest.TestCase):
def test_output_is_proper(self):
self.maxDiff = None
calendar = Calendar.from_ical(INPUT)
output = ''
for event in calendar.walk('vevent'):
output += cli.view(event) + '\n\n'
self.assertEqual(PROPER_OUTPUT, output)
if __name__ == '__main__':
unittest.main()

Wyświetl plik

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import unittest
import datetime

Wyświetl plik

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from icalendar.parser_tools import to_unicode
import unittest
@ -153,7 +150,7 @@ END:VEVENT"""
icalendar.Event.from_ical(ical_content).to_ical()
def test_issue_101(self):
"""Issue #101 - icalender is choking on umlauts in ORGANIZER
"""Issue #101 - icalendar is choking on umlauts in ORGANIZER
https://github.com/collective/icalendar/issues/101
"""
@ -471,8 +468,8 @@ END:VCALENDAR"""
self.assertEqual(dtstart, expected)
try:
expected_zone = str('(UTC-03:00) Brasília')
expected_tzname = str('Brasília standard')
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')
@ -565,3 +562,16 @@ END:VCALENDAR"""
event.add('DURATION', datetime.timedelta(hours=2))
self.assertEqual(event["DURATION"].td, datetime.timedelta(seconds=7200)) # Official API
self.assertEqual(event["DURATION"].dt, datetime.timedelta(seconds=7200)) # Backwards compatibility
def test_issue_345(self):
"""Issue #345 - Why is tools.UIDGenerator a class (that must be instantiated) instead of a module? """
uid1 = icalendar.tools.UIDGenerator.uid()
uid2 = icalendar.tools.UIDGenerator.uid('test.test')
uid3 = icalendar.tools.UIDGenerator.uid(unique='123')
uid4 = icalendar.tools.UIDGenerator.uid('test.test', '123')
self.assertEqual(uid1.split('@')[1], 'example.com')
self.assertEqual(uid2.split('@')[1], 'test.test')
self.assertEqual(uid3.split('-')[1], '123@example.com')
self.assertEqual(uid4.split('-')[1], '123@test.test')

Wyświetl plik

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import icalendar
import os
import textwrap
@ -59,13 +56,13 @@ class IcalendarTestCase (unittest.TestCase):
'123456789 123456789 123456789 123456789 ')
)
# http://tools.ietf.org/html/rfc5545#section-3.3.11
# https://tools.ietf.org/html/rfc5545#section-3.3.11
# An intentional formatted text line break MUST only be included in
# a "TEXT" property value by representing the line break with the
# character sequence of BACKSLASH, followed by a LATIN SMALL LETTER
# N or a LATIN CAPITAL LETTER N, that is "\n" or "\N".
# Newlines are not allwoed in content lines
# Newlines are not allowed in content lines
self.assertRaises(AssertionError, Contentline, b'1234\r\n\r\n1234')
self.assertEqual(
@ -165,14 +162,14 @@ class IcalendarTestCase (unittest.TestCase):
)
# And the traditional failure
with self.assertRaisesRegexp(
with self.assertRaisesRegex(
ValueError,
'Content line could not be parsed into parts'
):
Contentline('ATTENDEE;maxm@example.com').parts()
# Another failure:
with self.assertRaisesRegexp(
with self.assertRaisesRegex(
ValueError,
'Content line could not be parsed into parts'
):
@ -189,7 +186,7 @@ class IcalendarTestCase (unittest.TestCase):
)
# Should bomb on missing param:
with self.assertRaisesRegexp(
with self.assertRaisesRegex(
ValueError,
'Content line could not be parsed into parts'
):
@ -214,12 +211,12 @@ class IcalendarTestCase (unittest.TestCase):
)
contains_base64 = (
'X-APPLE-STRUCTURED-LOCATION;'
'VALUE=URI;X-ADDRESS="Kaiserliche Hofburg, 1010 Wien";'
'X-APPLE-MAPKIT-HANDLE=CAESxQEZgr3QZXJyZWljaA==;'
'X-APPLE-RADIUS=328.7978217977285;X-APPLE-REFERENCEFRAME=1;'
'X-TITLE=Heldenplatz:geo:48.206686,16.363235'
).encode('utf-8')
b'X-APPLE-STRUCTURED-LOCATION;'
b'VALUE=URI;X-ADDRESS="Kaiserliche Hofburg, 1010 Wien";'
b'X-APPLE-MAPKIT-HANDLE=CAESxQEZgr3QZXJyZWljaA==;'
b'X-APPLE-RADIUS=328.7978217977285;X-APPLE-REFERENCEFRAME=1;'
b'X-TITLE=Heldenplatz:geo:48.206686,16.363235'
)
self.assertEqual(
Contentline(contains_base64, strict=True).parts(),
@ -252,7 +249,7 @@ class IcalendarTestCase (unittest.TestCase):
# at least just but bytes in there
# porting it to "run" under python 2 & 3 makes it not much better
with self.assertRaises(AssertionError):
foldline('привет'.encode('utf-8'), limit=3)
foldline('привет'.encode(), limit=3)
self.assertEqual(foldline('foobar', limit=4), 'foo\r\n bar')
self.assertEqual(

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from icalendar import Calendar
from icalendar.prop import vText
import unittest

Wyświetl plik

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from icalendar import Calendar
from icalendar import Event
from icalendar import Parameters
@ -115,11 +112,11 @@ class TestPropertyParams(unittest.TestCase):
self.assertEqual(p['parameter1'], 'Value1')
self.assertEqual(p['PARAMETER1'], 'Value1')
# Parameter with list of values must be seperated by comma
# 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 seperated by a semicolon
# 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')
@ -127,7 +124,7 @@ class TestPropertyParams(unittest.TestCase):
p = Parameters({'ALTREP': 'http://www.wiz.org'})
self.assertEqual(p.to_ical(), b'ALTREP="http://www.wiz.org"')
# list items must be quoted seperately
# list items must be quoted separately
p = Parameters({'MEMBER': ['MAILTO:projectA@host.com',
'MAILTO:projectB@host.com']})
self.assertEqual(
@ -175,7 +172,7 @@ class TestPropertyParams(unittest.TestCase):
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 recieved per email.
This is a follow-up of a question received per email.
"""
ics = """BEGIN:VCALENDAR

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from icalendar.caselessdict import CaselessDict
import unittest

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import unittest
import datetime

Wyświetl plik

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import unittest
import datetime
@ -8,11 +5,53 @@ import dateutil.parser
import icalendar
import os
import pytz
try:
import zoneinfo
except:
try:
from backports import zoneinfo
except:
zoneinfo = None
class TestTimezoned(unittest.TestCase):
def test_create_from_ical(self):
def test_create_from_ical_zoneinfo(self):
if zoneinfo is None:
self.skipTest("zoneinfo library not found for this python version")
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp:
data = fp.read()
cal = icalendar.Calendar.from_ical(data)
self.assertEqual(
cal['prodid'].to_ical(),
b"-//Plone.org//NONSGML plone.app.event//EN"
)
timezones = cal.walk('VTIMEZONE')
self.assertEqual(len(timezones), 1)
tz = timezones[0]
self.assertEqual(tz['tzid'].to_ical(), b"Europe/Vienna")
std = tz.walk('STANDARD')[0]
self.assertEqual(
std.decoded('TZOFFSETFROM'),
datetime.timedelta(0, 7200)
)
ev1 = cal.walk('VEVENT')[0]
self.assertEqual(
ev1.decoded('DTSTART'),
datetime.datetime(2012, 2, 13, 10, 0, 0, tzinfo=zoneinfo.ZoneInfo('Europe/Vienna'))
)
self.assertEqual(
ev1.decoded('DTSTAMP'),
datetime.datetime(2010, 10, 10, 9, 10, 10, tzinfo=zoneinfo.ZoneInfo('UTC'))
)
def test_create_from_ical_pytz(self):
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp:
data = fp.read()
@ -49,7 +88,7 @@ class TestTimezoned(unittest.TestCase):
)
)
def test_create_to_ical(self):
def test_create_to_ical_pytz(self):
cal = icalendar.Calendar()
cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN")
@ -109,6 +148,92 @@ class TestTimezoned(unittest.TestCase):
event.add('attendee', 'franz')
event.add('attendee', 'sepp')
event.add('contact', 'Max Mustermann, 1010 Wien')
event.add('url', 'https://plone.org')
cal.add_component(event)
test_out = b'|'.join(cal.to_ical().splitlines())
test_out = test_out.decode('utf-8')
vtimezone_lines = "BEGIN:VTIMEZONE|TZID:Europe/Vienna|X-LIC-LOCATION:"
"Europe/Vienna|BEGIN:STANDARD|DTSTART;VALUE=DATE-TIME:19701025T03"
"0000|RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10|RRULE:FREQ=YEARLY;B"
"YDAY=-1SU;BYMONTH=3|TZNAME:CET|TZOFFSETFROM:+0200|TZOFFSETTO:+01"
"00|END:STANDARD|BEGIN:DAYLIGHT|DTSTART;VALUE=DATE-TIME:19700329T"
"020000|TZNAME:CEST|TZOFFSETFROM:+0100|TZOFFSETTO:+0200|END:DAYLI"
"GHT|END:VTIMEZONE"
self.assertTrue(vtimezone_lines in test_out)
test_str = "DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20120213T100000"
self.assertTrue(test_str in test_out)
self.assertTrue("ATTENDEE:sepp" in test_out)
# ical standard expects DTSTAMP and CREATED in UTC
self.assertTrue("DTSTAMP;VALUE=DATE-TIME:20101010T081010Z" in test_out)
self.assertTrue("CREATED;VALUE=DATE-TIME:20101010T081010Z" in test_out)
def test_create_to_ical_zoneinfo(self):
if zoneinfo is None:
self.skipTest("zoneinfo library not found for this python version")
cal = icalendar.Calendar()
cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN")
cal.add('version', "2.0")
cal.add('x-wr-calname', "test create calendar")
cal.add('x-wr-caldesc', "icalendar tests")
cal.add('x-wr-relcalid', "12345")
cal.add('x-wr-timezone', "Europe/Vienna")
tzc = icalendar.Timezone()
tzc.add('tzid', 'Europe/Vienna')
tzc.add('x-lic-location', 'Europe/Vienna')
tzs = icalendar.TimezoneStandard()
tzs.add('tzname', 'CET')
tzs.add('dtstart', datetime.datetime(1970, 10, 25, 3, 0, 0))
tzs.add('rrule', {'freq': 'yearly', 'bymonth': 10, 'byday': '-1su'})
tzs.add('TZOFFSETFROM', datetime.timedelta(hours=2))
tzs.add('TZOFFSETTO', datetime.timedelta(hours=1))
tzd = icalendar.TimezoneDaylight()
tzd.add('tzname', 'CEST')
tzd.add('dtstart', datetime.datetime(1970, 3, 29, 2, 0, 0))
tzs.add('rrule', {'freq': 'yearly', 'bymonth': 3, 'byday': '-1su'})
tzd.add('TZOFFSETFROM', datetime.timedelta(hours=1))
tzd.add('TZOFFSETTO', datetime.timedelta(hours=2))
tzc.add_component(tzs)
tzc.add_component(tzd)
cal.add_component(tzc)
event = icalendar.Event()
tz = zoneinfo.ZoneInfo("Europe/Vienna")
event.add(
'dtstart',
datetime.datetime(2012, 2, 13, 10, 00, 00, tzinfo=tz))
event.add(
'dtend',
datetime.datetime(2012, 2, 17, 18, 00, 00, tzinfo=tz))
event.add(
'dtstamp',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add(
'created',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add('uid', '123456')
event.add(
'last-modified',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add('summary', 'artsprint 2012')
# event.add('rrule', 'FREQ=YEARLY;INTERVAL=1;COUNT=10')
event.add('description', 'sprinting at the artsprint')
event.add('location', 'aka bild, wien')
event.add('categories', 'first subject')
event.add('categories', 'second subject')
event.add('attendee', 'häns')
event.add('attendee', 'franz')
event.add('attendee', 'sepp')
event.add('contact', 'Max Mustermann, 1010 Wien')
event.add('url', 'http://plone.org')
cal.add_component(event)
@ -132,6 +257,7 @@ class TestTimezoned(unittest.TestCase):
self.assertTrue("DTSTAMP;VALUE=DATE-TIME:20101010T081010Z" in test_out)
self.assertTrue("CREATED;VALUE=DATE-TIME:20101010T081010Z" in test_out)
def test_tzinfo_dateutil(self):
# Test for issues #77, #63
# references: #73,7430b66862346fe3a6a100ab25e35a8711446717

Wyświetl plik

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from datetime import timedelta
import unittest
@ -96,7 +93,7 @@ class TestCalComponent(unittest.TestCase):
b'BEGIN:VCALENDAR\r\nATTENDEE:Max M\r\nEND:VCALENDAR\r\n'
)
# Components can be nested, so You can add a subcompont. Eg a calendar
# Components can be nested, so You can add a subcomponent. Eg a calendar
# holds events.
e = Component(summary='A brief history of time')
e.name = 'VEVENT'

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import unittest
import icalendar

Wyświetl plik

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from icalendar.parser_tools import data_encode
from icalendar.parser_tools import to_unicode
import unittest
@ -12,9 +9,9 @@ class TestParserTools(unittest.TestCase):
self.assertEqual(to_unicode(b'spam'), 'spam')
self.assertEqual(to_unicode('spam'), 'spam')
self.assertEqual(to_unicode('spam'.encode('utf-8')), 'spam')
self.assertEqual(to_unicode(b'spam'), 'spam')
self.assertEqual(to_unicode(b'\xc6\xb5'), '\u01b5')
self.assertEqual(to_unicode('\xc6\xb5'.encode('iso-8859-1')),
self.assertEqual(to_unicode(b'\xc6\xb5'),
'\u01b5')
self.assertEqual(to_unicode(b'\xc6\xb5', encoding='ascii'), '\u01b5')
self.assertEqual(to_unicode(1), 1)

Wyświetl plik

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import date
from datetime import datetime
from datetime import time

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
import unittest
from icalendar.tools import UIDGenerator

Wyświetl plik

@ -1,3 +1,2 @@
# -*- coding: utf-8 -*-
# we save all timezone with TZIDs unknow to the TZDB in here
# we save all timezone with TZIDs unknown to the TZDB in here
_timezone_cache = {}

Wyświetl plik

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from icalendar.parser_tools import to_unicode
from icalendar.prop import vDatetime
@ -9,26 +8,28 @@ from string import digits
import random
class UIDGenerator(object):
class UIDGenerator:
"""If you are too lazy to create real uid's.
"""
chars = list(ascii_letters + digits)
def rnd_string(self, length=16):
@staticmethod
def rnd_string(length=16):
"""Generates a string with random characters of length.
"""
return ''.join([random.choice(self.chars) for _ in range(length)])
return ''.join([random.choice(UIDGenerator.chars) for _ in range(length)])
def uid(self, host_name='example.com', unique=''):
@staticmethod
def uid(host_name='example.com', unique=''):
"""Generates a unique id consisting of:
datetime-uniquevalue@host.
Like:
20050105T225746Z-HKtJMqUgdO0jDUwm@example.com
"""
host_name = to_unicode(host_name)
unique = unique or self.rnd_string()
unique = unique or UIDGenerator.rnd_string()
today = to_unicode(vDatetime(datetime.today()).to_ical())
return vText('%s-%s@%s' % (today,
return vText('{}-{}@{}'.format(today,
unique,
host_name))

Wyświetl plik

@ -5,8 +5,8 @@ The data is taken from the unicode consortium [0], the proposal and rationale
for this mapping is also available at the unicode consortium [1].
[0] http://www.unicode.org/cldr/charts/29/supplemental/zone_tzid.html
[1] http://cldr.unicode.org/development/development-process/design-proposals/extended-windows-olson-zid-mapping # noqa
[0] https://www.unicode.org/cldr/cldr-aux/charts/29/supplemental/zone_tzid.html
[1] https://cldr.unicode.org/development/development-process/design-proposals/extended-windows-olson-zid-mapping # noqa
"""
WINDOWS_TO_OLSON = {

17
tox.ini
Wyświetl plik

@ -1,15 +1,24 @@
# to run for a specific environment, use ``tox -e ENVNAME``
[tox]
envlist = py27,py34,py35,py36,py37,py38,py39,py310,pypy,pypy3
envlist = py38,py39,py310,pypy3,docs
# Note: the 'docs' env creates a 'build' directory which may interfere in strange ways
# with the other environments. You might see this when you run the tests in parallel.
# See https://github.com/collective/icalendar/pull/359#issuecomment-1214150269
[testenv]
usedevelop=True
deps =
pytest
coverage
py{27,34,35,36}: hypothesis>=3.0
.[test]
commands =
coverage run --source=src/icalendar --omit=*/tests/* --module pytest []
py{27,34,35,36}: coverage run --append --source=src/icalendar --omit=*/tests/* --module pytest [] src/icalendar/tests/hypothesis/
coverage report
coverage html
[testenv:docs]
deps =
-r {toxinidir}/requirements_docs.txt
changedir = docs
allowlist_externals = make
commands =
make html

3
venv/pyvenv.cfg 100644
Wyświetl plik

@ -0,0 +1,3 @@
home = /usr/bin
include-system-site-packages = false
version = 3.10.6