kopia lustrzana https://github.com/collective/icalendar
Initial import of iCalendar, by Max M.
commit
ee533bd510
|
@ -0,0 +1,125 @@
|
|||
------------------------------------------------------------------------
|
||||
r314 | maxm | 2005-03-22 13:43:04 +0100 (Tue, 22 Mar 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r294 | maxm | 2005-02-25 03:22:27 +0100 (Fri, 25 Feb 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r283 | maxm | 2005-02-17 23:51:40 +0100 (Thu, 17 Feb 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r282 | maxm | 2005-02-17 23:50:51 +0100 (Thu, 17 Feb 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r274 | maxm | 2005-02-07 14:41:37 +0100 (Mon, 07 Feb 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r273 | maxm | 2005-02-07 12:05:38 +0100 (Mon, 07 Feb 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r272 | maxm | 2005-02-07 11:09:48 +0100 (Mon, 07 Feb 2005) | 1 line
|
||||
|
||||
Added default value to the "decoded()" method.
|
||||
------------------------------------------------------------------------
|
||||
r271 | maxm | 2005-02-07 09:31:42 +0100 (Mon, 07 Feb 2005) | 1 line
|
||||
|
||||
- Fixed bug with propertyValues resetting themself when being read.
|
||||
------------------------------------------------------------------------
|
||||
r270 | maxm | 2005-02-07 09:21:40 +0100 (Mon, 07 Feb 2005) | 1 line
|
||||
|
||||
- Fixed bug with propertyValues resetting themself when being read.
|
||||
------------------------------------------------------------------------
|
||||
r249 | maxm | 2005-01-27 13:44:59 +0100 (Thu, 27 Jan 2005) | 1 line
|
||||
|
||||
Fixed problem with Unicode in Property values
|
||||
------------------------------------------------------------------------
|
||||
r184 | maxm | 2005-01-22 13:33:08 +0100 (Sat, 22 Jan 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r183 | maxm | 2005-01-22 13:32:14 +0100 (Sat, 22 Jan 2005) | 1 line
|
||||
|
||||
Fixed bug when adding unicode paramter values
|
||||
------------------------------------------------------------------------
|
||||
r182 | maxm | 2005-01-22 13:06:21 +0100 (Sat, 22 Jan 2005) | 1 line
|
||||
|
||||
added smal unicode test
|
||||
------------------------------------------------------------------------
|
||||
r181 | maxm | 2005-01-21 16:18:15 +0100 (Fri, 21 Jan 2005) | 1 line
|
||||
|
||||
added more descriptive errormessage
|
||||
------------------------------------------------------------------------
|
||||
r180 | maxm | 2005-01-21 16:04:32 +0100 (Fri, 21 Jan 2005) | 1 line
|
||||
|
||||
- changed a few defaults
|
||||
------------------------------------------------------------------------
|
||||
r159 | maxm | 2005-01-17 16:16:53 +0100 (Mon, 17 Jan 2005) | 1 line
|
||||
|
||||
Parameters where not being added when parsing
|
||||
------------------------------------------------------------------------
|
||||
r158 | maxm | 2005-01-17 01:31:42 +0100 (Mon, 17 Jan 2005) | 1 line
|
||||
|
||||
Moved remotely
|
||||
------------------------------------------------------------------------
|
||||
r157 | maxm | 2005-01-17 01:30:00 +0100 (Mon, 17 Jan 2005) | 1 line
|
||||
|
||||
This is the first public release
|
||||
------------------------------------------------------------------------
|
||||
r156 | maxm | 2005-01-16 23:33:46 +0100 (Sun, 16 Jan 2005) | 1 line
|
||||
|
||||
Added examples
|
||||
------------------------------------------------------------------------
|
||||
r155 | maxm | 2005-01-16 03:13:03 +0100 (Sun, 16 Jan 2005) | 1 line
|
||||
|
||||
Getting ready for Gold!
|
||||
------------------------------------------------------------------------
|
||||
r147 | maxm | 2005-01-11 23:24:52 +0100 (Tue, 11 Jan 2005) | 1 line
|
||||
|
||||
Last step before the parser is complete. All subtypes can parse from ical.
|
||||
------------------------------------------------------------------------
|
||||
r146 | maxm | 2005-01-10 03:38:11 +0100 (Mon, 10 Jan 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r145 | maxm | 2005-01-10 03:23:35 +0100 (Mon, 10 Jan 2005) | 1 line
|
||||
|
||||
Added parsing to Property Value Data Types. So they can parse themself.
|
||||
------------------------------------------------------------------------
|
||||
r144 | maxm | 2005-01-09 21:05:13 +0100 (Sun, 09 Jan 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r143 | maxm | 2005-01-09 18:15:08 +0100 (Sun, 09 Jan 2005) | 1 line
|
||||
|
||||
Fixed a few parameters that needed objects with ical() method.
|
||||
------------------------------------------------------------------------
|
||||
r142 | maxm | 2005-01-09 17:44:08 +0100 (Sun, 09 Jan 2005) | 1 line
|
||||
|
||||
non functional
|
||||
------------------------------------------------------------------------
|
||||
r141 | maxm | 2005-01-09 15:39:44 +0100 (Sun, 09 Jan 2005) | 1 line
|
||||
|
||||
Changed to use ical() method instead of __str__() for representation.
|
||||
------------------------------------------------------------------------
|
||||
r140 | maxm | 2005-01-09 09:52:58 +0100 (Sun, 09 Jan 2005) | 1 line
|
||||
|
||||
Fixed minor import bugs
|
||||
------------------------------------------------------------------------
|
||||
r139 | maxm | 2005-01-09 09:40:53 +0100 (Sun, 09 Jan 2005) | 1 line
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
r138 | maxm | 2005-01-09 09:28:18 +0100 (Sun, 09 Jan 2005) | 1 line
|
||||
|
||||
First version. Can only generate iCalendar files. Not parse.
|
||||
------------------------------------------------------------------------
|
||||
r137 | maxm | 2005-01-09 09:26:05 +0100 (Sun, 09 Jan 2005) | 1 line
|
||||
|
||||
Created folder remotely
|
||||
------------------------------------------------------------------------
|
|
@ -0,0 +1,91 @@
|
|||
iCalendar package for Python
|
||||
|
||||
The iCalendar package is a parser/generator of iCalender files for use with
|
||||
Python. It follows the RFC 2445 spec.
|
||||
|
||||
|
||||
Summary
|
||||
|
||||
I have often needed to parse and generate iCalendar files. Finally I got
|
||||
tired of writing ad-hoc tools.
|
||||
|
||||
So this is my attempt at making an iCalendar package for Python. The
|
||||
inspiration has come from the email package in the standard lib, which I
|
||||
think is pretty simple, yet efficient and powerfull.
|
||||
|
||||
The aim is to make a package that is fully compliant to RFC 2445, well
|
||||
designed, simple to use and well documented.
|
||||
|
||||
Look in
|
||||
"doc/example.py":http://www.mxm.dk/products/public/ical/example.py/file_view
|
||||
for introductory doctests and explanations.
|
||||
|
||||
All modules and classes have doctest that shows how they work, so it is all
|
||||
pretty well documented.
|
||||
|
||||
It can generate and parse iCalender files, and can easily be used as is.
|
||||
|
||||
But it does needs a bit more polish before i will considder it finished. I
|
||||
would say that it's about 95% done.
|
||||
|
||||
Examples
|
||||
|
||||
To open and parse a file::
|
||||
|
||||
>>> from iCalendar import Calendar, Event
|
||||
>>> cal = Calendar.from_string(open('test.ics','rb').read())
|
||||
>>> cal
|
||||
VCALENDAR({'VERSION': '2.0', 'METHOD': 'Request', 'PRODID': '-//My product//mxm.dk/'})
|
||||
|
||||
>>> for component in cal.walk():
|
||||
... component.name
|
||||
'VCALENDAR'
|
||||
'VEVENT'
|
||||
'VEVENT'
|
||||
|
||||
To create a calendar and write it to disc::
|
||||
|
||||
|
||||
>>> cal = Calendar()
|
||||
>>> from datetime import datetime
|
||||
>>> from iCalendar import UTC # timezone
|
||||
>>> cal.add('prodid', '-//My calendar product//mxm.dk//')
|
||||
>>> cal.add('version', '2.0')
|
||||
|
||||
>>> event = Event()
|
||||
>>> event.add('summary', 'Python meeting about calendaring')
|
||||
>>> event.add('dtstart', datetime(2005,4,4,8,0,0,tzinfo=UTC()))
|
||||
>>> event.add('dtend', datetime(2005,4,4,10,0,0,tzinfo=UTC()))
|
||||
>>> event.add('dtstamp', datetime(2005,4,4,0,10,0,tzinfo=UTC()))
|
||||
>>> event['uid'] = '20050115T101010/27346262376@mxm.dk'
|
||||
>>> event.add('priority', 5)
|
||||
|
||||
>>> cal.add_component(event)
|
||||
|
||||
>>> f = open('example.ics', 'wb')
|
||||
>>> f.write(cal.as_string())
|
||||
>>> f.close()
|
||||
|
||||
|
||||
Note!
|
||||
|
||||
This is the first public release, so it is most likely buggy in some degree.
|
||||
But it is usable for production.
|
||||
|
||||
It is dependent on the datetime package, so it requires Python >= 2.2
|
||||
|
||||
Feedback/contact
|
||||
|
||||
If you have any comments or feedback on the module, please contact me at:
|
||||
"maxm@mxm.dk":maxm@mxm.dk
|
||||
|
||||
I would love to hear use cases, or get ideas for improvements.
|
||||
|
||||
Download
|
||||
|
||||
Get the latest version from the
|
||||
"download page":http://www.mxm.dk/products/public/ical/downloads
|
||||
|
||||
License
|
||||
|
||||
LGPL
|
|
@ -0,0 +1,5 @@
|
|||
- Automatic En- & de-coding of parameter values
|
||||
|
||||
Most of the work is done allread. Just need to get it finished. Look at line
|
||||
153 in 'ContentlinesParser.py'
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//My calendar product//mxm.dk//
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com
|
||||
ATTENDEE;CN=The Dude;ROLE=REQ-PARTICIPANT:MAILTO:the-dude@example.com
|
||||
DTEND:20050404T100000Z
|
||||
DTSTAMP:20050404T001000Z
|
||||
DTSTART:20050404T080000Z
|
||||
LOCATION:Odense\, Denmark
|
||||
ORGANIZER;CN=Max Rasmussen;ROLE=CHAIR:MAILTO:noone@example.com
|
||||
PRIORITY:5
|
||||
SUMMARY:Python meeting about calendaring
|
||||
UID:20050115T101010/27346262376@mxm.dk
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,307 @@
|
|||
# -*- coding: latin-1 -*-
|
||||
"""
|
||||
|
||||
iCalendar package
|
||||
=================
|
||||
|
||||
|
||||
This package is used for parsing and generating iCalendar files following the
|
||||
standard in RFC 2445.
|
||||
|
||||
It should be fully compliant, but it is possible to generate and parse invalid
|
||||
files if you really want to.
|
||||
|
||||
|
||||
File structure
|
||||
--------------
|
||||
|
||||
An iCalendar file is a text file (utf-8) with a special format. Basically it
|
||||
consists of content lines.
|
||||
|
||||
Each content line defines a property that has 3 parts (name, parameters,
|
||||
values). Parameters are optional.
|
||||
|
||||
A simple content line with only name and value could look like this:
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
|
||||
A content line with parameters can look like this:
|
||||
|
||||
ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:example@example.com
|
||||
|
||||
And the parts are:
|
||||
|
||||
Name: ATTENDEE
|
||||
Params: CN=Max Rasmussen;ROLE=REQ-PARTICIPANT
|
||||
Value: MAILTO:example@example.com
|
||||
|
||||
Long content lines are usually "folded" to less than 75 character, but the
|
||||
package takes care of that.
|
||||
|
||||
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
On a higher level iCalendar files consists of components. Components can have
|
||||
sub components.
|
||||
|
||||
The root component is the VCALENDAR.
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
... vcalendar properties ...
|
||||
END:VCALENDAR
|
||||
|
||||
The most frequent subcomponent to a VCALENDAR is a VEVENT. They are nested like
|
||||
this:
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
... vcalendar properties ...
|
||||
BEGIN:VEVENT
|
||||
... vevent properties ...
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
Inside the components there are properties with values. The values have special
|
||||
types. like integer, text, datetime etc. These values are encoded in a special
|
||||
text format in an iCalendar file.
|
||||
|
||||
There are methods for converting to and from these encodings in the package.
|
||||
|
||||
|
||||
These are the most important imports.
|
||||
|
||||
>>> from iCalendar import Calendar, Event
|
||||
|
||||
Components
|
||||
----------
|
||||
|
||||
Components are like (Case Insensitive) dicts. So if you want to set a property
|
||||
you do it like this. The calendar is a component.
|
||||
|
||||
>>> cal = Calendar()
|
||||
>>> cal['dtstart'] = '20050404T080000'
|
||||
>>> cal['summary'] = 'Python meeting about calendaring'
|
||||
>>> for k,v in cal.items():
|
||||
... k,v
|
||||
('DTSTART', '20050404T080000')
|
||||
('SUMMARY', 'Python meeting about calendaring')
|
||||
|
||||
You can generate a string for a file with the as_string() method. (Calling
|
||||
str(cal) does the same):
|
||||
|
||||
>>> cal.as_string()
|
||||
'BEGIN:VCALENDAR\\r\\nDTSTART:20050404T080000\\r\\nSUMMARY:Python meeting about calendaring\\r\\nEND:VCALENDAR\\r\\n'
|
||||
|
||||
>>> str(cal)
|
||||
'BEGIN:VCALENDAR\\r\\nDTSTART:20050404T080000\\r\\nSUMMARY:Python meeting about calendaring\\r\\nEND:VCALENDAR\\r\\n'
|
||||
|
||||
in the calendar examples below the as_string() is implied. The rendered view is
|
||||
easier to read:
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
DTSTART:20050404T080000
|
||||
SUMMARY:Python meeting about calendaring
|
||||
END:VCALENDAR
|
||||
|
||||
You can set multiple properties like this:
|
||||
|
||||
>>> cal = Calendar()
|
||||
>>> cal['attendee'] = ['MAILTO:maxm@mxm.dk','MAILTO:test@example.com']
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
ATTENDEE:MAILTO:maxm@mxm.dk
|
||||
ATTENDEE:MAILTO:test@example.com
|
||||
END:VCALENDAR
|
||||
|
||||
|
||||
if you don't want to care about whether a property value is a list or a single
|
||||
value, just use the add() method. It will automatically convert the property to
|
||||
a list of values if more than one value is added.
|
||||
|
||||
>>> cal = Calendar()
|
||||
>>> cal.add('attendee', 'MAILTO:maxm@mxm.dk')
|
||||
>>> cal.add('attendee', 'MAILTO:test@example.com')
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
ATTENDEE:MAILTO:maxm@mxm.dk
|
||||
ATTENDEE:MAILTO:test@example.com
|
||||
END:VCALENDAR
|
||||
|
||||
|
||||
Note: this version doesn't check for compliance, so you should look in the RFC
|
||||
2445 spec for legal properties for each component, or look in the iCalendar.py
|
||||
file, where it is at least defined for each component.
|
||||
|
||||
Subcomponents
|
||||
-------------
|
||||
|
||||
Any component can have subcomponents. Eg. inside a calendar there can be events.
|
||||
They can be arbitrarily nested. First by making a new component:
|
||||
|
||||
>>> event = Event()
|
||||
>>> event['uid'] = '42'
|
||||
>>> event['dtstart'] = '20050404T080000'
|
||||
|
||||
And then appending it to a "parent".
|
||||
|
||||
>>> cal.add_component(event)
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
ATTENDEE:MAILTO:maxm@mxm.dk
|
||||
ATTENDEE:MAILTO:test@example.com
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20050404T080000
|
||||
UID:42
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
Subcomponents are appended to the subcomponents property on the component.
|
||||
|
||||
>>> cal.subcomponents
|
||||
[VEVENT({'DTSTART': '20050404T080000', 'UID': '42'})]
|
||||
|
||||
Value types
|
||||
-----------
|
||||
|
||||
Property values are utf-8 encoded strings.
|
||||
|
||||
This is impractical if you want to use the data for further computation. Eg. the
|
||||
datetime format looks like this: '20050404T080000'. But the package makes it
|
||||
simple to Parse and generate iCalendar formatted strings.
|
||||
|
||||
Basically you can make the add() method do the thinking, or you can do it
|
||||
yourself.
|
||||
|
||||
To add a datetime value, you can use Pythons built in datetime types, and the
|
||||
set the encode parameter to true, and it will convert to the type defined in the
|
||||
spec.
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> cal.add('dtstart', datetime(2005,4,4,8,0,0))
|
||||
>>> str(cal['dtstart'])
|
||||
'20050404T080000'
|
||||
|
||||
If that doesn't work satisfactorily for some reason, you can also do it
|
||||
manually.
|
||||
|
||||
In 'PropertyValues.py', all the iCalendar data types are defined. Each type has
|
||||
a class that can parse and encode the type.
|
||||
|
||||
So if you want to do it manually ...
|
||||
|
||||
>>> from iCalendar import vDatetime
|
||||
>>> now = datetime(2005,4,4,8,0,0)
|
||||
>>> vDatetime(now).ical()
|
||||
'20050404T080000'
|
||||
|
||||
So the drill is to initialise the object with a python built in type, and then
|
||||
call the "ical()" method on the object. That will return an ical encoded string.
|
||||
|
||||
You can do it the other way around too. To parse an encoded string, just call
|
||||
the "from_ical()" method, and it will return an instance of the corresponding
|
||||
Python type.
|
||||
|
||||
>>> vDatetime.from_ical('20050404T080000')
|
||||
datetime.datetime(2005, 4, 4, 8, 0)
|
||||
|
||||
>>> dt = vDatetime.from_ical('20050404T080000Z')
|
||||
>>> repr(dt)[:85] + repr(dt)[85+8:]
|
||||
'datetime.datetime(2005, 4, 4, 8, 0, tzinfo=<iCalendar.PropertyValues.UTC object at 0x>)'
|
||||
|
||||
|
||||
You can also choose to use the decoded() method, which will return a decoded
|
||||
value directly.
|
||||
|
||||
>>> cal = Calendar()
|
||||
>>> cal.add('dtstart', datetime(2005,4,4,8,0,0))
|
||||
>>> str(cal['dtstart'])
|
||||
'20050404T080000'
|
||||
>>> cal.decoded('dtstart')
|
||||
datetime.datetime(2005, 4, 4, 8, 0)
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Here is an example generating a complete iCal calendar file with a single event
|
||||
that can be loaded into the Mozilla calendar
|
||||
|
||||
Init the calendar
|
||||
>>> cal = Calendar()
|
||||
>>> from datetime import datetime
|
||||
>>> from iCalendar import UTC # timezone
|
||||
|
||||
Som properties are required to be complient
|
||||
>>> cal.add('prodid', '-//My calendar product//mxm.dk//')
|
||||
>>> cal.add('version', '2.0')
|
||||
|
||||
We need at least one subcomponent for a calendar to be complient
|
||||
>>> event = Event()
|
||||
>>> event.add('summary', 'Python meeting about calendaring')
|
||||
>>> event.add('dtstart', datetime(2005,4,4,8,0,0,tzinfo=UTC()))
|
||||
>>> event.add('dtend', datetime(2005,4,4,10,0,0,tzinfo=UTC()))
|
||||
>>> event.add('dtstamp', datetime(2005,4,4,0,10,0,tzinfo=UTC()))
|
||||
|
||||
|
||||
A property with parameters. Notice that they are an attribute on the value.
|
||||
>>> from iCalendar import vCalAddress, vText
|
||||
>>> organizer = vCalAddress('MAILTO:noone@example.com')
|
||||
|
||||
Automatic encoding is not yet implemented for parameter values.
|
||||
So you must use the 'v*' types defined in PropertyValues.py
|
||||
>>> organizer.params['cn'] = vText('Max Rasmussen')
|
||||
>>> organizer.params['role'] = vText('CHAIR')
|
||||
>>> event['organizer'] = organizer
|
||||
>>> event['location'] = vText('Odense, Denmark')
|
||||
|
||||
>>> event['uid'] = '20050115T101010/27346262376@mxm.dk'
|
||||
>>> event.add('priority', 5)
|
||||
|
||||
>>> attendee = vCalAddress('MAILTO:maxm@example.com')
|
||||
>>> attendee.params['cn'] = vText('Max Rasmussen')
|
||||
>>> attendee.params['ROLE'] = vText('REQ-PARTICIPANT')
|
||||
>>> event.add('attendee', attendee, encode=0)
|
||||
|
||||
>>> attendee = vCalAddress('MAILTO:the-dude@example.com')
|
||||
>>> attendee.params['cn'] = vText('The Dude')
|
||||
>>> attendee.params['ROLE'] = vText('REQ-PARTICIPANT')
|
||||
>>> event.add('attendee', attendee, encode=0)
|
||||
|
||||
Add the event to the calendar
|
||||
>>> cal.add_component(event)
|
||||
|
||||
Write to disc
|
||||
>>> f = open('example.ics', 'wb')
|
||||
>>> f.write(cal.as_string())
|
||||
>>> f.close()
|
||||
|
||||
And that generates this file.
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//My calendar product//mxm.dk//
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
ATTENDEE:MAILTO:maxm@example.com
|
||||
ATTENDEE:MAILTO:the-dude@example.com
|
||||
DTEND:20050404T100000Z
|
||||
DTSTAMP:20050404T001000Z
|
||||
DTSTART:20050404T080000Z
|
||||
LOCATION:Odense, Denmark
|
||||
ORGANIZER;CN=Max Rasmussen;ROLE=CHAIR:MAILTO:noone@example.com
|
||||
PRIORITY:5
|
||||
SUMMARY:Python meeting about calendaring
|
||||
UID:20050115T101010/27346262376@mxm.dk
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
--
|
||||
|
||||
Enjoy, Max M, maxm@mxm.dk
|
||||
|
||||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os.path, doctest, example
|
||||
# import and test this file
|
||||
doctest.testmod(example)
|
|
@ -0,0 +1,36 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//RDU Software//NONSGML HandCal//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:US-Eastern
|
||||
BEGIN:STANDARD
|
||||
DTSTART:19981025T020000
|
||||
RDATE:19981025T020000
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
TZNAME:EST
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:19990404T020000
|
||||
RDATE:19990404T020000
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
TZNAME:EDT
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:19980309T231000Z
|
||||
UID:guid-1.host1.com
|
||||
ORGANIZER;ROLE=CHAIR:MAILTO:mrbig@host.com
|
||||
ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;CUTYPE=GROUP:
|
||||
MAILTO:employee-A@host.com
|
||||
DESCRIPTION:Project XYZ Review Meeting
|
||||
CATEGORIES:MEETING
|
||||
CLASS:PUBLIC
|
||||
CREATED:19980309T130000Z
|
||||
SUMMARY:XYZ Project Review
|
||||
DTSTART;TZID=US-Eastern:19980312T083000
|
||||
DTEND;TZID=US-Eastern:19980312T093000
|
||||
LOCATION:1CP Conference Room 4350
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: latin-1 -*-
|
||||
"""
|
||||
|
||||
An example from the RFC 2445 spec.
|
||||
|
||||
>>> from iCalendar import Calendar
|
||||
>>> cal = Calendar.from_string(open('groupscheduled.ics','rb').read())
|
||||
>>> cal
|
||||
VCALENDAR({'VERSION': '2.0', 'PRODID': '-//RDU Software//NONSGML HandCal//EN'})
|
||||
|
||||
>>> timezones = cal.walk('VTIMEZONE')
|
||||
>>> len(timezones)
|
||||
1
|
||||
|
||||
>>> tz = timezones[0]
|
||||
>>> tz
|
||||
VTIMEZONE({'TZID': 'US-Eastern'})
|
||||
|
||||
>>> std = tz.walk('STANDARD')[0]
|
||||
>>> std.decoded('TZOFFSETFROM')
|
||||
datetime.timedelta(-1, 72000)
|
||||
|
||||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os.path, doctest, groupscheduled
|
||||
# import and test this file
|
||||
doctest.testmod(groupscheduled)
|
|
@ -0,0 +1,25 @@
|
|||
BEGIN:VCALENDAR
|
||||
METHOD:Request
|
||||
PRODID:-//My product//mxm.dk/
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
DESCRIPTION:This is a very long description that will be folded This is a
|
||||
very long description that will be folded This is a very long description
|
||||
that will be folded This is a very long description that will be folded Th
|
||||
is is a very long description that will be folded This is a very long desc
|
||||
ription that will be folded This is a very long description that will be f
|
||||
olded This is a very long description that will be folded This is a very l
|
||||
ong description that will be folded This is a very long description that w
|
||||
ill be folded
|
||||
PARTICIPANT;CN=Max M:MAILTO:maxm@mxm.dk
|
||||
DTEND:20050107T160000
|
||||
DTSTART:20050107T120000
|
||||
SUMMARY:A second event
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTEND:20050108T235900
|
||||
DTSTART:20050108T230000
|
||||
SUMMARY:A single event
|
||||
UID:42
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: latin-1 -*-
|
||||
"""
|
||||
|
||||
A small example
|
||||
|
||||
>>> from iCalendar import Calendar
|
||||
>>> cal = Calendar.from_string(open('test.ics','rb').read())
|
||||
>>> cal
|
||||
VCALENDAR({'VERSION': '2.0', 'METHOD': 'Request', 'PRODID': '-//My product//mxm.dk/'})
|
||||
|
||||
>>> for component in cal.walk():
|
||||
... component.name
|
||||
'VCALENDAR'
|
||||
'VEVENT'
|
||||
'VEVENT'
|
||||
|
||||
>>> cal['prodid']
|
||||
'-//My product//mxm.dk/'
|
||||
|
||||
>>> cal.decoded('prodid')
|
||||
u'-//My product//mxm.dk/'
|
||||
|
||||
>>> first_event = cal.walk('vevent')[0]
|
||||
>>> first_event['description'][:75]
|
||||
'This is a very long description that will be folded This is a very long des'
|
||||
|
||||
>>> first_event['summary']
|
||||
'A second event'
|
||||
|
||||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os.path, doctest, test
|
||||
# import and test this file
|
||||
doctest.testmod(test)
|
|
@ -0,0 +1,16 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//My calendar product//mxm.dk//
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com
|
||||
ATTENDEE;CN=The Dude;ROLE=REQ-PARTICIPANT:MAILTO:the-dude@example.com
|
||||
DTEND:20050404T100000Z
|
||||
DTSTAMP:20050404T001000Z
|
||||
DTSTART:20050404T080000Z
|
||||
LOCATION:Odense\, Denmark
|
||||
ORGANIZER;CN=Max Rasmussen;ROLE=CHAIR:MAILTO:noone@example.com
|
||||
PRIORITY:5
|
||||
SUMMARY:Python meeting about calendaring
|
||||
UID:20050115T101010/27346262376@mxm.dk
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,102 @@
|
|||
# -*- coding: latin-1 -*-
|
||||
|
||||
class CaselessDict(dict):
|
||||
|
||||
"""
|
||||
A dictionary that isn't case sensitive, and only use string as keys.
|
||||
|
||||
>>> ncd = CaselessDict(key1='val1', key2='val2')
|
||||
>>> ncd
|
||||
CaselessDict({'KEY2': 'val2', 'KEY1': 'val1'})
|
||||
>>> ncd['key1']
|
||||
'val1'
|
||||
>>> ncd['KEY1']
|
||||
'val1'
|
||||
>>> ncd['KEY3'] = 'val3'
|
||||
>>> ncd['key3']
|
||||
'val3'
|
||||
>>> ncd.setdefault('key3', 'FOUND')
|
||||
'val3'
|
||||
>>> ncd.setdefault('key4', 'NOT FOUND')
|
||||
'NOT FOUND'
|
||||
>>> ncd['key4']
|
||||
'NOT FOUND'
|
||||
>>> ncd.get('key1')
|
||||
'val1'
|
||||
>>> ncd.get('key3', 'NOT FOUND')
|
||||
'val3'
|
||||
>>> ncd.get('key4', 'NOT FOUND')
|
||||
'NOT FOUND'
|
||||
>>> 'key4' in ncd
|
||||
True
|
||||
>>> del ncd['key4']
|
||||
>>> ncd.has_key('key4')
|
||||
False
|
||||
>>> ncd.update({'key5':'val5', 'KEY6':'val6', 'KEY5':'val7'})
|
||||
>>> ncd['key6']
|
||||
'val6'
|
||||
>>> keys = ncd.keys()
|
||||
>>> keys.sort()
|
||||
>>> keys
|
||||
['KEY1', 'KEY2', 'KEY3', 'KEY5', 'KEY6']
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Set keys to upper for initial dict"
|
||||
dict.__init__(self, *args, **kwargs)
|
||||
for k,v in self.items():
|
||||
k_upper = k.upper()
|
||||
if k != k_upper:
|
||||
dict.__delitem__(self, k)
|
||||
self[k_upper] = v
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, key.upper())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, key.upper(), value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key.upper())
|
||||
|
||||
def __contains__(self, item):
|
||||
return dict.__contains__(self, item.upper())
|
||||
|
||||
def get(self, key, default=None):
|
||||
return dict.get(self, key.upper(), default)
|
||||
|
||||
def setdefault(self, key, value=None):
|
||||
return dict.setdefault(self, key.upper(), value)
|
||||
|
||||
def pop(self, key, default=None):
|
||||
return dict.pop(self, key.upper(), default)
|
||||
|
||||
def popitem(self):
|
||||
return dict.popitem(self, key.upper())
|
||||
|
||||
def has_key(self, key):
|
||||
return dict.has_key(self, key.upper())
|
||||
|
||||
def update(self, indict):
|
||||
"""
|
||||
Multiple keys where key1.upper() == key2.upper() will be lost.
|
||||
"""
|
||||
for entry in indict:
|
||||
self[entry] = indict[entry]
|
||||
|
||||
def copy(self):
|
||||
return CaselessDict(dict.copy(self))
|
||||
|
||||
def clear(self):
|
||||
dict.clear(self)
|
||||
|
||||
def __repr__(self):
|
||||
return 'CaselessDict(' + dict.__repr__(self) + ')'
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os.path, doctest, CaselessDict
|
||||
# import and test this file
|
||||
doctest.testmod(CaselessDict)
|
||||
|
||||
|
|
@ -0,0 +1,415 @@
|
|||
# -*- coding: latin-1 -*-
|
||||
|
||||
"""
|
||||
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)
|
||||
|
||||
It is stupid in the sense that it treats the content purely as strings. No type
|
||||
conversion is attempted.
|
||||
|
||||
Copyright, 2005: Max M <maxm@mxm.dk>
|
||||
License: GPL (Just contact med if and why you would like it changed)
|
||||
"""
|
||||
|
||||
# from python
|
||||
from types import TupleType, ListType
|
||||
SequenceTypes = [TupleType, ListType]
|
||||
import re
|
||||
# from this package
|
||||
from CaselessDict import CaselessDict
|
||||
|
||||
|
||||
#################################################################
|
||||
# Property parameter stuff
|
||||
|
||||
def paramVal(val):
|
||||
"Returns a parameter value"
|
||||
if type(val) in SequenceTypes:
|
||||
return q_join(val)
|
||||
return dQuote(val)
|
||||
|
||||
QUOTABLE = re.compile('[,;:].')
|
||||
def dQuote(val):
|
||||
"""
|
||||
Parameter values containing [,;:] must be double quoted
|
||||
>>> dQuote('Max')
|
||||
'Max'
|
||||
>>> dQuote('Rasmussen, Max')
|
||||
'"Rasmussen, Max"'
|
||||
>>> dQuote('name:value')
|
||||
'"name:value"'
|
||||
"""
|
||||
if QUOTABLE.search(val):
|
||||
return '"%s"' % val
|
||||
return val
|
||||
|
||||
# parsing helper
|
||||
def q_split(st, sep=','):
|
||||
"""
|
||||
Splits a string on char, taking double (q)uotes into considderation
|
||||
>>> q_split('Max,Moller,"Rasmussen, Max"')
|
||||
['Max', 'Moller', '"Rasmussen, Max"']
|
||||
"""
|
||||
result = []
|
||||
cursor = 0
|
||||
length = len(st)
|
||||
inquote = 0
|
||||
for i in range(length):
|
||||
ch = st[i]
|
||||
if ch == '"':
|
||||
inquote = not inquote
|
||||
if not inquote and ch == sep:
|
||||
result.append(st[cursor:i])
|
||||
cursor = i + 1
|
||||
if i + 1 == length:
|
||||
result.append(st[cursor:])
|
||||
return result
|
||||
|
||||
def q_join(lst, sep=','):
|
||||
"""
|
||||
Joins a list on sep, quoting strings with QUOTABLE chars
|
||||
>>> s = ['Max', 'Moller', 'Rasmussen, Max']
|
||||
>>> q_join(s)
|
||||
'Max,Moller,"Rasmussen, Max"'
|
||||
"""
|
||||
return sep.join([dQuote(itm) for itm in lst])
|
||||
|
||||
class Parameters(CaselessDict):
|
||||
|
||||
"""
|
||||
Parser and generator of Property parameter strings. It knows nothing of
|
||||
datatypes. It's main concern is textual structure.
|
||||
|
||||
|
||||
Simple parameter:value pair
|
||||
>>> p = Parameters(parameter1='Value1')
|
||||
>>> str(p)
|
||||
'PARAMETER1=Value1'
|
||||
|
||||
|
||||
keys are converted to upper
|
||||
>>> p.keys()
|
||||
['PARAMETER1']
|
||||
|
||||
|
||||
Parameters are case insensitive
|
||||
>>> p['parameter1']
|
||||
'Value1'
|
||||
>>> p['PARAMETER1']
|
||||
'Value1'
|
||||
|
||||
|
||||
Parameter with list of values must be seperated by comma
|
||||
>>> p = Parameters({'parameter1':['Value1', 'Value2']})
|
||||
>>> str(p)
|
||||
'PARAMETER1=Value1,Value2'
|
||||
|
||||
|
||||
Multiple parameters must be seperated by a semicolon
|
||||
>>> p = Parameters({'RSVP':'TRUE', 'ROLE':'REQ-PARTICIPANT'})
|
||||
>>> str(p)
|
||||
'ROLE=REQ-PARTICIPANT;RSVP=TRUE'
|
||||
|
||||
|
||||
Parameter values containing ',;:' must be double quoted
|
||||
>>> p = Parameters({'ALTREP':'http://www.wiz.org'})
|
||||
>>> str(p)
|
||||
'ALTREP="http://www.wiz.org"'
|
||||
|
||||
|
||||
list items must be quoted seperately
|
||||
>>> p = Parameters({'MEMBER':['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com', ]})
|
||||
>>> str(p)
|
||||
'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']})
|
||||
>>> str(p)
|
||||
'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'
|
||||
|
||||
We can also parse parameter strings
|
||||
>>> Parameters.from_string('PARAMETER1=Value 1;param2=Value 2')
|
||||
Parameters({'PARAMETER1': 'Value 1', 'PARAM2': 'Value 2'})
|
||||
|
||||
We can also parse parameter strings
|
||||
>>> Parameters.from_string('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
|
||||
>>> Parameters.from_string('ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3')
|
||||
Parameters({'PARAMETER1': 'Value1', 'ALTREP': ['http://www.wiz.org', 'value4'], 'PARAMETER2': ['Value2', 'Value3']})
|
||||
"""
|
||||
|
||||
|
||||
def params(self):
|
||||
"""
|
||||
in rfc2445 keys are called parameters, so this is to be consitent with
|
||||
the naming conventions
|
||||
"""
|
||||
return self.keys()
|
||||
|
||||
### Later, when I get more time... need to finish this off now. The last majot thing missing.
|
||||
### def _encode(self, name, value, cond=1):
|
||||
### # internal, for conditional convertion of values.
|
||||
### if cond:
|
||||
### klass = types_factory.for_property(name)
|
||||
### return klass(value)
|
||||
### return value
|
||||
###
|
||||
### def add(self, name, value, encode=0):
|
||||
### "Add a parameter value and optionally encode it."
|
||||
### if encode:
|
||||
### value = self._encode(name, value, encode)
|
||||
### self[name] = value
|
||||
###
|
||||
### def decoded(self, name):
|
||||
### "returns a decoded value, or list of same"
|
||||
|
||||
def __repr__(self):
|
||||
return 'Parameters(' + dict.__repr__(self) + ')'
|
||||
|
||||
|
||||
def __str__(self):
|
||||
result = []
|
||||
items = self.items()
|
||||
items.sort() # To make doctests work
|
||||
for key, value in items:
|
||||
value = paramVal(value)
|
||||
result.append('%s=%s' % (key.upper(), value))
|
||||
return ';'.join(result)
|
||||
|
||||
|
||||
def from_string(st):
|
||||
"Parses the parameter format from ical text format"
|
||||
try:
|
||||
# parse into strings
|
||||
result = Parameters()
|
||||
for param in q_split(st, ';'):
|
||||
key, val = q_split(param, '=')
|
||||
# parsed and " stripped, but just strings
|
||||
vals = [v.strip('"') for v in q_split(val, ',')]
|
||||
if len(vals) == 1:
|
||||
result[key] = vals[0]
|
||||
else:
|
||||
result[key] = vals
|
||||
return result
|
||||
except:
|
||||
raise ValueError, 'Not a valid parameter string'
|
||||
from_string = staticmethod(from_string)
|
||||
|
||||
|
||||
#########################################
|
||||
# parsing and generation of content lines
|
||||
|
||||
class Contentline(str):
|
||||
|
||||
"""
|
||||
A content line is basically a string that can be folded and parsed into
|
||||
parts.
|
||||
|
||||
>>> c = Contentline('Si meliora dies, ut vina, poemata reddit')
|
||||
>>> str(c)
|
||||
'Si meliora dies, ut vina, poemata reddit'
|
||||
|
||||
A long line gets folded
|
||||
>>> c = Contentline(''.join(['123456789 ']*10))
|
||||
>>> str(c)
|
||||
'123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234\\r\\n 56789 123456789 123456789 '
|
||||
|
||||
A folded line gets unfolded
|
||||
>>> c = Contentline.from_string(str(c))
|
||||
>>> c
|
||||
'123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 '
|
||||
|
||||
It can parse itself into parts. Which is a tuple of (name, params, vals)
|
||||
|
||||
>>> c = Contentline('dtstart:20050101T120000')
|
||||
>>> c.parts()
|
||||
('dtstart', Parameters({}), '20050101T120000')
|
||||
|
||||
>>> c = Contentline('dtstart;value=datetime:20050101T120000')
|
||||
>>> c.parts()
|
||||
('dtstart', Parameters({'VALUE': 'datetime'}), '20050101T120000')
|
||||
|
||||
>>> c = Contentline('ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com')
|
||||
>>> c.parts()
|
||||
('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com')
|
||||
>>> str(c)
|
||||
'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com'
|
||||
|
||||
and back again
|
||||
>>> parts = ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com')
|
||||
>>> Contentline.from_parts(parts)
|
||||
'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com'
|
||||
|
||||
and again
|
||||
>>> parts = ('ATTENDEE', Parameters(), 'MAILTO:maxm@example.com')
|
||||
>>> Contentline.from_parts(parts)
|
||||
'ATTENDEE:MAILTO:maxm@example.com'
|
||||
|
||||
A value can also be any of the types defined in PropertyValues
|
||||
>>> from PropertyValues import vText
|
||||
>>> parts = ('ATTENDEE', Parameters(), vText('MAILTO:test@example.com'))
|
||||
>>> Contentline.from_parts(parts)
|
||||
'ATTENDEE:MAILTO:test@example.com'
|
||||
|
||||
A value can also be unicode
|
||||
>>> from PropertyValues import vText
|
||||
>>> parts = ('SUMMARY', Parameters(), vText(u'INternational char æ ø å'))
|
||||
>>> Contentline.from_parts(parts)
|
||||
'SUMMARY:INternational char \\xc3\\xa6 \\xc3\\xb8 \\xc3\\xa5'
|
||||
|
||||
Traversing could look like this.
|
||||
>>> name, params, vals = c.parts()
|
||||
>>> name
|
||||
'ATTENDEE'
|
||||
>>> vals
|
||||
'MAILTO:maxm@example.com'
|
||||
>>> for key, val in params.items():
|
||||
... (key, val)
|
||||
('ROLE', 'REQ-PARTICIPANT')
|
||||
('CN', 'Max Rasmussen')
|
||||
|
||||
And the traditional failure
|
||||
>>> c = Contentline('ATTENDEE;maxm@example.com')
|
||||
>>> c.parts()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Content line could not be parsed into parts
|
||||
|
||||
"""
|
||||
|
||||
def from_parts(parts):
|
||||
"Turns a tuple of parts into a content line"
|
||||
(name, params, values) = [str(p) for p in parts]
|
||||
try:
|
||||
if params:
|
||||
return Contentline('%s;%s:%s' % (name, params, values))
|
||||
return Contentline('%s:%s' % (name, values))
|
||||
except:
|
||||
raise ValueError, 'Property: %s Wrong values "%s" or "%s"' % (repr(name),
|
||||
repr(params),
|
||||
repr(values))
|
||||
from_parts = staticmethod(from_parts)
|
||||
|
||||
def parts(self):
|
||||
""" Splits the content line up into (name, parameters, values) parts
|
||||
"""
|
||||
try:
|
||||
name_split = None
|
||||
value_split = None
|
||||
inquotes = 0
|
||||
for i in range(len(self)):
|
||||
ch = self[i]
|
||||
if not inquotes:
|
||||
if ch in ':;' and not name_split:
|
||||
name_split = i
|
||||
if ch == ':' and not value_split:
|
||||
value_split = i
|
||||
if ch == '"':
|
||||
inquotes = not inquotes
|
||||
name = self[:name_split]
|
||||
params = Parameters.from_string(self[name_split+1:value_split])
|
||||
values = self[value_split+1:]
|
||||
return (name, params, values)
|
||||
except:
|
||||
raise ValueError, 'Content line could not be parsed into parts'
|
||||
|
||||
def from_string(st):
|
||||
"Unfolds the content lines in an iCalendar into long content lines"
|
||||
try:
|
||||
# a fold is carriage return followed by either a space or a tab
|
||||
a_fold = re.compile('\r\n[ \t]{1}')
|
||||
return Contentline(a_fold.sub('', st))
|
||||
except:
|
||||
raise ValueError, 'Expected StringType with content line'
|
||||
from_string = staticmethod(from_string)
|
||||
|
||||
def __str__(self):
|
||||
"Long content lines are folded so they are less than 75 characters wide"
|
||||
l_line = len(self)
|
||||
new_lines = []
|
||||
for i in range(0, l_line, 74):
|
||||
new_lines.append(self[i:i+74])
|
||||
return '\r\n '.join(new_lines)
|
||||
|
||||
|
||||
|
||||
class Contentlines(list):
|
||||
|
||||
"""
|
||||
I assume that iCalendar files generally are a few kilobytes in size. Then
|
||||
this should be efficient. for Huge files, an iterator should probably be
|
||||
used instead.
|
||||
|
||||
>>> c = Contentlines([Contentline('BEGIN:VEVENT\\r\\n')])
|
||||
>>> str(c)
|
||||
'BEGIN:VEVENT\\r\\n'
|
||||
|
||||
Lets try appending it with a 100 charater wide string
|
||||
>>> c.append(Contentline(''.join(['123456789 ']*10)+'\\r\\n'))
|
||||
>>> str(c)
|
||||
'BEGIN:VEVENT\\r\\n\\r\\n123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234\\r\\n 56789 123456789 123456789 \\r\\n'
|
||||
|
||||
Notice that there is an extra empty string in the end of the content lines.
|
||||
That is so they can be easily joined with: '\r\n'.join(contentlines)).
|
||||
>>> Contentlines.from_string('A short line\\r\\n')
|
||||
['A short line', '']
|
||||
>>> Contentlines.from_string('A faked\\r\\n long line\\r\\n')
|
||||
['A faked long line', '']
|
||||
>>> Contentlines.from_string('A faked\\r\\n long line\\r\\nAnd another lin\\r\\n\\te that is folded\\r\\n')
|
||||
['A faked long line', 'And another line that is folded', '']
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
"Simply join self."
|
||||
return '\r\n'.join(map(str, self))
|
||||
|
||||
def from_string(st):
|
||||
"Parses a string into content lines"
|
||||
try:
|
||||
# a fold is carriage return followed by either a space or a tab
|
||||
a_fold = re.compile('\r\n[ \t]{1}')
|
||||
unfolded = a_fold.sub('', st)
|
||||
lines = [Contentline(line) for line in unfolded.split('\r\n') if line]
|
||||
lines.append('') # we need a '\r\n' in the end of every content line
|
||||
return Contentlines(lines)
|
||||
except:
|
||||
raise ValueError, 'Expected StringType with content lines'
|
||||
from_string = staticmethod(from_string)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os.path, doctest, ContentlinesParser
|
||||
# import and test this file
|
||||
doctest.testmod(ContentlinesParser)
|
||||
|
||||
|
||||
# ran this:
|
||||
# sample = open('./samples/test.ics', 'rb').read() # binary file in windows!
|
||||
# lines = Contentlines.from_string(sample)
|
||||
# for line in lines[:-1]:
|
||||
# print line.parts()
|
||||
|
||||
# got this:
|
||||
#('BEGIN', Parameters({}), 'VCALENDAR')
|
||||
#('METHOD', Parameters({}), 'Request')
|
||||
#('PRODID', Parameters({}), '-//My product//mxm.dk/')
|
||||
#('VERSION', Parameters({}), '2.0')
|
||||
#('BEGIN', Parameters({}), 'VEVENT')
|
||||
#('DESCRIPTION', Parameters({}), 'This is a very long description that ...')
|
||||
#('PARTICIPANT', Parameters({'CN': 'Max M'}), 'MAILTO:maxm@mxm.dk')
|
||||
#('DTEND', Parameters({}), '20050107T160000')
|
||||
#('DTSTART', Parameters({}), '20050107T120000')
|
||||
#('SUMMARY', Parameters({}), 'A second event')
|
||||
#('END', Parameters({}), 'VEVENT')
|
||||
#('BEGIN', Parameters({}), 'VEVENT')
|
||||
#('DTEND', Parameters({}), '20050108T235900')
|
||||
#('DTSTART', Parameters({}), '20050108T230000')
|
||||
#('SUMMARY', Parameters({}), 'A single event')
|
||||
#('UID', Parameters({}), '42')
|
||||
#('END', Parameters({}), 'VEVENT')
|
||||
#('END', Parameters({}), 'VCALENDAR')
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,16 @@
|
|||
# Components
|
||||
from iCalendar import Calendar, Event, Todo, Journal, FreeBusy, Timezone, Alarm, \
|
||||
ComponentFactory
|
||||
|
||||
# Property Data Value Types
|
||||
from PropertyValues import vBinary, vBoolean, vCalAddress, vDatetime, vDate, \
|
||||
vDDDTypes, vDuration, vFloat, vInt, vPeriod, \
|
||||
vWeekday, vFrequency, vRecur, vText, vTime, vUri, \
|
||||
vGeo, vUTCOffset, TypesFactory
|
||||
|
||||
# usefull tzinfo subclasses
|
||||
from PropertyValues import FixedOffset, UTC, LocalTimezone
|
||||
|
||||
# Parameters and helper methods for splitting and joining string with escaped
|
||||
# chars.
|
||||
from ContentlinesParser import Parameters, q_split, q_join
|
|
@ -0,0 +1,532 @@
|
|||
# -*- coding: latin-1 -*-
|
||||
|
||||
"""
|
||||
|
||||
Calendar is a dictionary like Python object that can render itself as VCAL
|
||||
files according to rfc2445.
|
||||
|
||||
These are the defined components.
|
||||
|
||||
"""
|
||||
|
||||
# from python
|
||||
from types import ListType, TupleType
|
||||
SequenceTypes = (ListType, TupleType)
|
||||
import re
|
||||
|
||||
# from this package
|
||||
from CaselessDict import CaselessDict
|
||||
from ContentlinesParser import Contentlines, Contentline, Parameters,\
|
||||
q_split, q_join
|
||||
from PropertyValues import TypesFactory
|
||||
|
||||
|
||||
######################################
|
||||
# The component factory
|
||||
|
||||
class ComponentFactory(CaselessDict):
|
||||
|
||||
"""
|
||||
All components defined in rfc 2445 are registered in this factory class. To
|
||||
get a component you can use it like this.
|
||||
|
||||
>>> factory = ComponentFactory()
|
||||
>>> component = factory['VEVENT']
|
||||
>>> event = component(dtstart='19700101')
|
||||
>>> event.as_string()
|
||||
'BEGIN:VEVENT\\r\\nDTSTART:19700101\\r\\nEND:VEVENT\\r\\n'
|
||||
|
||||
>>> factory.get('VCALENDAR', Component)
|
||||
<class 'iCalendar.iCalendar.Calendar'>
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Set keys to upper for initial dict"
|
||||
CaselessDict.__init__(self, *args, **kwargs)
|
||||
self['VEVENT'] = Event
|
||||
self['VTODO'] = Todo
|
||||
self['VJOURNAL'] = Journal
|
||||
self['VFREEBUSY'] = FreeBusy
|
||||
self['VTIMEZONE'] = Timezone
|
||||
self['VALARM'] = Alarm
|
||||
self['VCALENDAR'] = Calendar
|
||||
|
||||
|
||||
# These Properties have multiple property values inlined in one propertyline
|
||||
# seperated by comma. Use CaselessDict as simple caseless set.
|
||||
INLINE = CaselessDict(
|
||||
[(cat, 1) for cat in ('CATEGORIES', 'RESOURCES', 'FREEBUSY')]
|
||||
)
|
||||
|
||||
_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.
|
||||
|
||||
A component is like a dictionary with extra methods and attributes.
|
||||
>>> c = Component()
|
||||
>>> c.name = 'VCALENDAR'
|
||||
|
||||
Every key defines a property. A property can consist of either a single
|
||||
item. This can be set with a single value
|
||||
>>> c['prodid'] = '-//max m//icalendar.mxm.dk/'
|
||||
>>> c
|
||||
VCALENDAR({'PRODID': '-//max m//icalendar.mxm.dk/'})
|
||||
|
||||
or with a list
|
||||
>>> c['ATTENDEE'] = ['Max M', 'Rasmussen']
|
||||
|
||||
if you use the add method you don't have to considder if a value is a list
|
||||
or not.
|
||||
>>> c = Component()
|
||||
>>> c.name = 'VEVENT'
|
||||
>>> c.add('attendee', 'maxm@mxm.dk')
|
||||
>>> c.add('attendee', 'test@example.dk')
|
||||
>>> c
|
||||
VEVENT({'ATTENDEE': [vCalAddress('maxm@mxm.dk'), vCalAddress('test@example.dk')]})
|
||||
|
||||
You can get the values back directly
|
||||
>>> c.add('prodid', '-//my product//')
|
||||
>>> c['prodid']
|
||||
vText(u'-//my product//')
|
||||
|
||||
or decoded to a python type
|
||||
>>> c.decoded('prodid')
|
||||
u'-//my product//'
|
||||
|
||||
With default values for non existing properties
|
||||
>>> c.decoded('version', 'No Version')
|
||||
'No Version'
|
||||
|
||||
The component can render itself in the RFC 2445 format.
|
||||
>>> c = Component()
|
||||
>>> c.name = 'VCALENDAR'
|
||||
>>> c.add('attendee', 'Max M')
|
||||
>>> c.as_string()
|
||||
'BEGIN:VCALENDAR\\r\\nATTENDEE:Max M\\r\\nEND:VCALENDAR\\r\\n'
|
||||
|
||||
>>> from PropertyValues import vDatetime
|
||||
|
||||
Components can be nested, so You can add a subcompont. Eg a calendar holds events.
|
||||
>>> e = Component(summary='A brief history of time')
|
||||
>>> e.name = 'VEVENT'
|
||||
>>> e.add('dtend', '20000102T000000', encode=0)
|
||||
>>> e.add('dtstart', '20000101T000000', encode=0)
|
||||
>>> e.as_string()
|
||||
'BEGIN:VEVENT\\r\\nDTEND:20000102T000000\\r\\nDTSTART:20000101T000000\\r\\nSUMMARY:A brief history of time\\r\\nEND:VEVENT\\r\\n'
|
||||
|
||||
>>> c.add_component(e)
|
||||
>>> c.subcomponents
|
||||
[VEVENT({'DTEND': '20000102T000000', 'DTSTART': '20000101T000000', 'SUMMARY': 'A brief history of time'})]
|
||||
|
||||
We can walk over nested componentes with the walk method.
|
||||
>>> [i.name for i in c.walk()]
|
||||
['VCALENDAR', 'VEVENT']
|
||||
|
||||
We can also just walk over specific component types, by filtering them on
|
||||
their name.
|
||||
>>> [i.name for i in c.walk('VEVENT')]
|
||||
['VEVENT']
|
||||
|
||||
>>> [i['dtstart'] for i in c.walk('VEVENT')]
|
||||
['20000101T000000']
|
||||
|
||||
INLINE properties have their values on one property line. Note the double
|
||||
quoting of the value with a colon in it.
|
||||
>>> c = Calendar()
|
||||
>>> c['resources'] = 'Chair, Table, "Room: 42"'
|
||||
>>> c
|
||||
VCALENDAR({'RESOURCES': 'Chair, Table, "Room: 42"'})
|
||||
|
||||
>>> c.as_string()
|
||||
'BEGIN:VCALENDAR\\r\\nRESOURCES:Chair, Table, "Room: 42"\\r\\nEND:VCALENDAR\\r\\n'
|
||||
|
||||
The inline values must be handled by the get_inline() and set_inline()
|
||||
methods.
|
||||
|
||||
>>> c.get_inline('resources', decode=0)
|
||||
['Chair', 'Table', 'Room: 42']
|
||||
|
||||
These can also be decoded
|
||||
>>> c.get_inline('resources', decode=1)
|
||||
[u'Chair', u'Table', u'Room: 42']
|
||||
|
||||
You can set them directly
|
||||
>>> c.set_inline('resources', ['A', 'List', 'of', 'some, recources'], encode=1)
|
||||
>>> c['resources']
|
||||
'A,List,of,"some, recources"'
|
||||
|
||||
and back again
|
||||
>>> c.get_inline('resources', decode=0)
|
||||
['A', 'List', 'of', 'some, recources']
|
||||
|
||||
>>> c['freebusy'] = '19970308T160000Z/PT3H,19970308T200000Z/PT1H,19970308T230000Z/19970309T000000Z'
|
||||
>>> c.get_inline('freebusy', decode=0)
|
||||
['19970308T160000Z/PT3H', '19970308T200000Z/PT1H', '19970308T230000Z/19970309T000000Z']
|
||||
|
||||
>>> freebusy = c.get_inline('freebusy', decode=1)
|
||||
>>> type(freebusy[0][0]), type(freebusy[0][1])
|
||||
(<type 'datetime.datetime'>, <type 'datetime.timedelta'>)
|
||||
"""
|
||||
|
||||
name = '' # must be defined in each component
|
||||
required = () # These properties are required
|
||||
singletons = () # These properties must only appear once
|
||||
multiple = () # may occur more than once
|
||||
exclusive = () # These properties are mutually exclusive
|
||||
inclusive = () # if any occurs the other(s) MUST occur ('duration', 'repeat')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Set keys to upper for initial dict"
|
||||
CaselessDict.__init__(self, *args, **kwargs)
|
||||
# set parameters here for properties that use non-default values
|
||||
self.subcomponents = [] # Components can be nested.
|
||||
|
||||
|
||||
# def non_complience(self, warnings=0):
|
||||
# """
|
||||
# not implemented yet!
|
||||
# Returns a dict describing non compliant properties, if any.
|
||||
# If warnings is true it also returns warnings.
|
||||
#
|
||||
# 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.
|
||||
# """
|
||||
# nc = {}
|
||||
# if not getattr(self, 'name', ''):
|
||||
# nc['name'] = {'type':'ERROR', 'description':'Name is not defined'}
|
||||
# return nc
|
||||
|
||||
|
||||
#############################
|
||||
# handling of property values
|
||||
|
||||
def _encode(self, name, value, cond=1):
|
||||
# internal, for conditional convertion of values.
|
||||
if cond:
|
||||
klass = types_factory.for_property(name)
|
||||
return klass(value)
|
||||
return value
|
||||
|
||||
|
||||
def set(self, name, value, encode=1):
|
||||
if type(value) == ListType:
|
||||
self[name] = [self._encode(name, v, encode) for v in value]
|
||||
else:
|
||||
self[name] = self._encode(name, value, encode)
|
||||
|
||||
|
||||
def add(self, name, value, encode=1):
|
||||
"If property exists append, else create and set it"
|
||||
if name in self:
|
||||
oldval = self[name]
|
||||
value = self._encode(name, value, encode)
|
||||
if type(oldval) == ListType:
|
||||
oldval.append(value)
|
||||
else:
|
||||
self.set(name, [oldval, value], encode=0)
|
||||
else:
|
||||
self.set(name, value, encode)
|
||||
|
||||
|
||||
def _decode(self, name, value):
|
||||
# internal for decoding property values
|
||||
decoded = types_factory.from_ical(name, value)
|
||||
return decoded
|
||||
|
||||
|
||||
def decoded(self, name, default=_marker):
|
||||
"Returns decoded value of property"
|
||||
if name in self:
|
||||
value = self[name]
|
||||
if type(value) == ListType:
|
||||
return [self._decode(name, v) for v in value]
|
||||
return self._decode(name, value)
|
||||
else:
|
||||
if default is _marker:
|
||||
raise KeyError, name
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
########################################################################
|
||||
# Inline values. A few properties have multiple values inlined in in one
|
||||
# property line. These methods are used for splitting and joining these.
|
||||
|
||||
def get_inline(self, name, decode=1):
|
||||
"""
|
||||
Returns a list of values (split on comma).
|
||||
"""
|
||||
vals = [v.strip('" ') for v in q_split(self[name])]
|
||||
if decode:
|
||||
return [self._decode(name, val) for val in vals]
|
||||
return vals
|
||||
|
||||
|
||||
def set_inline(self, name, values, encode=1):
|
||||
"""
|
||||
Converts a list of values into comma seperated string and sets value to
|
||||
that.
|
||||
"""
|
||||
if encode:
|
||||
values = [self._encode(name, value, 1) for value in values]
|
||||
self[name] = types_factory['inline'](q_join(values))
|
||||
|
||||
|
||||
#########################
|
||||
# Handling of components
|
||||
|
||||
def add_component(self, component):
|
||||
"add a subcomponent to this component"
|
||||
self.subcomponents.append(component)
|
||||
|
||||
|
||||
def _walk(self, name):
|
||||
# private!
|
||||
result = []
|
||||
if name is None or self.name == name:
|
||||
result.append(self)
|
||||
for subcomponent in self.subcomponents:
|
||||
result += subcomponent._walk(name)
|
||||
return result
|
||||
|
||||
|
||||
def walk(self, name=None):
|
||||
"""
|
||||
Recursively traverses component and subcomponents. Returns sequence of
|
||||
same. If name is passed, only components with name will be returned.
|
||||
"""
|
||||
if not name is None:
|
||||
name = name.upper()
|
||||
return self._walk(name)
|
||||
|
||||
#####################
|
||||
# Generation
|
||||
|
||||
def property_items(self):
|
||||
"""
|
||||
Returns properties in this component and subcomponents as:
|
||||
[(name, value), ...]
|
||||
"""
|
||||
vText = types_factory['text']
|
||||
properties = [('BEGIN', vText(self.name).ical())]
|
||||
property_names = self.keys()
|
||||
property_names.sort()
|
||||
for name in property_names:
|
||||
values = self[name]
|
||||
if type(values) == ListType:
|
||||
# normally one property is one line
|
||||
for value in values:
|
||||
properties.append((name, value))
|
||||
else:
|
||||
properties.append((name, values))
|
||||
# recursion is fun!
|
||||
for subcomponent in self.subcomponents:
|
||||
properties += subcomponent.property_items()
|
||||
properties.append(('END', vText(self.name).ical()))
|
||||
return properties
|
||||
|
||||
|
||||
def from_string(st):
|
||||
"""
|
||||
Populates the component recursively from a string
|
||||
"""
|
||||
stack = [] # a stack of components
|
||||
for line in Contentlines.from_string(st): # raw parsing
|
||||
if line: # last content line is empty string
|
||||
name, params, vals = line.parts()
|
||||
uname = name.upper()
|
||||
# check for start of component
|
||||
if uname == 'BEGIN':
|
||||
# try and create one of the components defined in the spec,
|
||||
# otherwise get a general Components for robustness.
|
||||
component_name = vals.upper()
|
||||
component_class = component_factory.get(component_name, Component)
|
||||
component = component_class()
|
||||
if not getattr(component, 'name', ''): # for undefined components
|
||||
component.name = component_name
|
||||
stack.append(component)
|
||||
# check for end of event
|
||||
elif uname == 'END':
|
||||
# we are done adding properties to this component
|
||||
# so pop it from the stack and add it to the new top.
|
||||
component = stack.pop()
|
||||
if not stack: # we are at the end
|
||||
return component
|
||||
else:
|
||||
stack[-1].add_component(component)
|
||||
# we are adding properties to the current top of the stack
|
||||
else:
|
||||
vals = types_factory['inline'](vals)
|
||||
vals.params = params
|
||||
stack[-1].add(name, vals, encode=0)
|
||||
from_string = staticmethod(from_string)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(' % self.name + dict.__repr__(self) + ')'
|
||||
|
||||
# def content_line(self, name):
|
||||
# "Returns property as content line"
|
||||
# value = self[name]
|
||||
# params = getattr(value, 'params', Parameters())
|
||||
# return Contentline.from_parts((name, params, value))
|
||||
|
||||
def content_lines(self):
|
||||
"Converts the Component and subcomponents into content lines"
|
||||
contentlines = Contentlines()
|
||||
for name, values in self.property_items():
|
||||
params = getattr(values, 'params', Parameters())
|
||||
contentlines.append(Contentline.from_parts((name, params, values)))
|
||||
contentlines.append('') # remember the empty string in the end
|
||||
return contentlines
|
||||
|
||||
|
||||
def as_string(self):
|
||||
return str(self.content_lines())
|
||||
|
||||
|
||||
def __str__(self):
|
||||
"Returns rendered iCalendar"
|
||||
return self.as_string()
|
||||
|
||||
|
||||
|
||||
#######################################
|
||||
# components defined in RFC 2445
|
||||
|
||||
|
||||
class Event(Component):
|
||||
|
||||
name = 'VEVENT'
|
||||
|
||||
required = ('UID',)
|
||||
singletons = (
|
||||
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'GEO',
|
||||
'LAST-MOD', 'LOCATION', 'ORGANIZER', 'PRIORITY', 'DTSTAMP', 'SEQUENCE',
|
||||
'STATUS', 'SUMMARY', 'TRANSP', 'URL', 'RECURID', 'DTEND', 'DURATION',
|
||||
'DTSTART',
|
||||
)
|
||||
exclusive = ('DTEND', 'DURATION', )
|
||||
multiple = (
|
||||
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT','CONTACT', 'EXDATE',
|
||||
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Todo(Component):
|
||||
|
||||
name = 'VTODO'
|
||||
|
||||
required = ('UID',)
|
||||
singletons = (
|
||||
'CLASS', 'COMPLETED', 'CREATED', 'DESCRIPTION', 'DTSTAMP', 'DTSTART',
|
||||
'GEO', 'LAST-MOD', 'LOCATION', 'ORGANIZER', 'PERCENT', 'PRIORITY',
|
||||
'RECURID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', 'DUE', 'DURATION',
|
||||
)
|
||||
exclusive = ('DUE', 'DURATION',)
|
||||
multiple = (
|
||||
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
|
||||
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Journal(Component):
|
||||
|
||||
name = 'VJOURNAL'
|
||||
|
||||
required = ('UID',)
|
||||
singletons = (
|
||||
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'DTSTAMP', 'LAST-MOD',
|
||||
'ORGANIZER', 'RECURID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL',
|
||||
)
|
||||
multiple = (
|
||||
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
|
||||
'EXRULE', 'RELATED', 'RDATE', 'RRULE', 'RSTATUS',
|
||||
)
|
||||
|
||||
|
||||
class FreeBusy(Component):
|
||||
|
||||
name = 'VFREEBUSY'
|
||||
|
||||
required = ('UID',)
|
||||
singletons = (
|
||||
'CONTACT', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP', 'ORGANIZER',
|
||||
'UID', 'URL',
|
||||
)
|
||||
multiple = ('ATTENDEE', 'COMMENT', 'FREEBUSY', 'RSTATUS',)
|
||||
|
||||
|
||||
class Timezone(Component):
|
||||
|
||||
name = 'VTIMEZONE'
|
||||
|
||||
required = (
|
||||
'TZID', 'STANDARDC', 'DAYLIGHTC', 'DTSTART', 'TZOFFSETTO',
|
||||
'TZOFFSETFROM'
|
||||
)
|
||||
singletons = ('LAST-MOD', 'TZURL', 'TZID',)
|
||||
multiple = ('COMMENT', 'RDATE', 'RRULE', 'TZNAME',)
|
||||
|
||||
|
||||
class Alarm(Component):
|
||||
|
||||
name = 'VALARM'
|
||||
# not quite shure about these ...
|
||||
required = ('ACTION', 'TRIGGER',)
|
||||
singletons = ('ATTACH', 'ACTION', 'TRIGGER', 'DURATION', 'REPEAT',)
|
||||
inclusive = (('DURATION', 'REPEAT',),)
|
||||
multiple = ('STANDARDC', 'DAYLIGHTC')
|
||||
|
||||
|
||||
class Calendar(Component):
|
||||
|
||||
"""
|
||||
This is the base object for an iCalendar file.
|
||||
|
||||
Setting up a minimal calendar component looks like this
|
||||
>>> cal = Calendar()
|
||||
|
||||
Som properties are required to be compliant
|
||||
>>> cal['prodid'] = '-//My calendar product//mxm.dk//'
|
||||
>>> cal['version'] = '2.0'
|
||||
|
||||
We also need at least one subcomponent for a calendar to be compliant
|
||||
>>> from datetime import datetime
|
||||
>>> event = Event()
|
||||
>>> event['summary'] = 'Python meeting about calendaring'
|
||||
>>> event['uid'] = '42'
|
||||
>>> event.set('dtstart', datetime(2005,4,4,8,0,0))
|
||||
>>> cal.add_component(event)
|
||||
>>> cal.subcomponents[0].as_string()
|
||||
'BEGIN:VEVENT\\r\\nDTSTART:20050404T080000\\r\\nSUMMARY:Python meeting about calendaring\\r\\nUID:42\\r\\nEND:VEVENT\\r\\n'
|
||||
|
||||
Write to disc
|
||||
>>> open('test.ics', 'wb').write(cal.as_string())
|
||||
"""
|
||||
|
||||
name = 'VCALENDAR'
|
||||
required = ('prodid', 'version', )
|
||||
singletons = ('prodid', 'version', )
|
||||
multiple = ('calscale', 'method', )
|
||||
|
||||
|
||||
# These are read only singleton, so one instance is enough for the module
|
||||
types_factory = TypesFactory()
|
||||
component_factory = ComponentFactory()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import os.path, doctest, iCalendar
|
||||
# import and test this file
|
||||
doctest.testmod(iCalendar)
|
|
@ -0,0 +1,9 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//My calendar product//mxm.dk//
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20050404T080000
|
||||
SUMMARY:Python meeting about calendaring
|
||||
UID:42
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1 @@
|
|||
# this is a package
|
|
@ -0,0 +1,14 @@
|
|||
import unittest, doctest
|
||||
from iCalendar import iCalendar, CaselessDict, ContentlinesParser
|
||||
from iCalendar import PropertyValues, tools
|
||||
|
||||
def test_suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(doctest.DocTestSuite(CaselessDict))
|
||||
suite.addTest(doctest.DocTestSuite(ContentlinesParser))
|
||||
suite.addTest(doctest.DocTestSuite(PropertyValues))
|
||||
suite.addTest(doctest.DocTestSuite(iCalendar))
|
||||
|
||||
# only has a disabled doctest
|
||||
# suite.addTest(doctest.DocTestSuite(tools))
|
||||
return suite
|
|
@ -0,0 +1,53 @@
|
|||
from string import ascii_letters, digits
|
||||
import random
|
||||
|
||||
"""
|
||||
This module contains non-essential tools for iCalendar. Pretty thin so far eh?
|
||||
|
||||
"""
|
||||
|
||||
class UIDGenerator:
|
||||
|
||||
"""
|
||||
If you are too lazy to create real uid's. Notice, this doctest is disabled!
|
||||
|
||||
Automatic semi-random uid
|
||||
>> g = UIDGenerator()
|
||||
>> uid = g.uid()
|
||||
>> uid.ical()
|
||||
'20050109T153222-7ekDDHKcw46QlwZK@example.com'
|
||||
|
||||
You Should at least insert your own hostname to be more complient
|
||||
>> g = UIDGenerator()
|
||||
>> uid = g.uid('Example.ORG')
|
||||
>> uid.ical()
|
||||
'20050109T153549-NbUItOPDjQj8Ux6q@Example.ORG'
|
||||
|
||||
You can also insert a path or similar
|
||||
>> g = UIDGenerator()
|
||||
>> uid = g.uid('Example.ORG', '/path/to/content')
|
||||
>> uid.ical()
|
||||
'20050109T153415-/path/to/content@Example.ORG'
|
||||
"""
|
||||
|
||||
chars = list(ascii_letters + digits)
|
||||
|
||||
def rnd_string(self, length=16):
|
||||
"Generates a string with random characters of length"
|
||||
return ''.join([random.choice(self.chars) for i in range(length)])
|
||||
|
||||
def uid(self, host_name='example.com', unique=''):
|
||||
"""
|
||||
Generates a unique id consisting of:
|
||||
datetime-uniquevalue@host. Like:
|
||||
20050105T225746Z-HKtJMqUgdO0jDUwm@example.com
|
||||
"""
|
||||
from PropertyValues import vText, vDatetime
|
||||
unique = unique or self.rnd_string()
|
||||
return vText('%s-%s@%s' % (vDatetime.today().ical(), unique, host_name))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os.path, doctest, tools
|
||||
# import and test this file
|
||||
doctest.testmod(tools)
|
|
@ -0,0 +1,9 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//My calendar product//mxm.dk//
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20050404T080000
|
||||
SUMMARY:Python meeting about calendaring
|
||||
UID:42
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,744 @@
|
|||
#!/usr/bin/env python2.3
|
||||
#
|
||||
# SchoolTool - common information systems platform for school administration
|
||||
# Copyright (c) 2003 Shuttleworth Foundation
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
"""
|
||||
SchoolTool test runner.
|
||||
|
||||
Syntax: test.py [options] [pathname-regexp [test-regexp]]
|
||||
|
||||
There are two kinds of tests:
|
||||
- unit tests (or programmer tests) test the internal workings of various
|
||||
components of the system
|
||||
- functional tests (acceptance tests, customer tests) test only externaly
|
||||
visible system behaviour
|
||||
|
||||
You can choose to run unit tests (this is the default mode), functional tests
|
||||
(by giving a -f option to test.py) or both (by giving both -u and -f options).
|
||||
|
||||
Test cases are located in the directory tree starting at the location of this
|
||||
script, in subdirectories named 'tests' for unit tests and 'ftests' for
|
||||
functional tests, in Python modules named 'test*.py'. They are then filtered
|
||||
according to pathname and test regexes. Alternatively, packages may just have
|
||||
'tests.py' and 'ftests.py' instead of subpackages 'tests' and 'ftests'
|
||||
respectively.
|
||||
|
||||
A leading "!" in a regexp is stripped and negates the regexp. Pathname
|
||||
regexp is applied to the whole path (package/package/module.py). Test regexp
|
||||
is applied to a full test id (package.package.module.class.test_method).
|
||||
|
||||
Options:
|
||||
-h, --help print this help message
|
||||
-v verbose (print dots for each test run)
|
||||
-vv very verbose (print test names)
|
||||
-q quiet (do not print anything on success)
|
||||
-w enable warnings about omitted test cases
|
||||
-d invoke pdb when an exception occurs
|
||||
-p show progress bar (can be combined with -v or -vv)
|
||||
-u select unit tests (default)
|
||||
-f select functional tests
|
||||
--level n select only tests at level n or lower
|
||||
--all-levels select all tests
|
||||
--list-files list all selected test files
|
||||
--list-tests list all selected test cases
|
||||
--list-hooks list all loaded test hooks
|
||||
--coverage create code coverage reports
|
||||
--search-in dir limit directory tree walk to dir (optimisation)
|
||||
--immediate-errors show errors as soon as they happen (default)
|
||||
--delayed-errors show errors after all unit tests were run
|
||||
"""
|
||||
#
|
||||
# This script borrows ideas from Zope 3's test runner heavily. It is smaller
|
||||
# and cleaner though, at the expense of more limited functionality.
|
||||
#
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import types
|
||||
import getopt
|
||||
import unittest
|
||||
import traceback
|
||||
import linecache
|
||||
import pdb
|
||||
from sets import Set
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class Options:
|
||||
"""Configurable properties of the test runner."""
|
||||
|
||||
# test location
|
||||
basedir = '' # base directory for tests (defaults to
|
||||
# basedir of argv[0] + 'src'), must be absolute
|
||||
search_in = () # list of subdirs to traverse (defaults to
|
||||
# basedir)
|
||||
follow_symlinks = True # should symlinks to subdirectories be
|
||||
# followed? (hardcoded, may cause loops)
|
||||
|
||||
# which tests to run
|
||||
unit_tests = False # unit tests (default if both are false)
|
||||
functional_tests = False # functional tests
|
||||
|
||||
# test filtering
|
||||
level = 1 # run only tests at this or lower level
|
||||
# (if None, runs all tests)
|
||||
pathname_regex = '' # regexp for filtering filenames
|
||||
test_regex = '' # regexp for filtering test cases
|
||||
|
||||
# actions to take
|
||||
list_files = False # --list-files
|
||||
list_tests = False # --list-tests
|
||||
list_hooks = False # --list-hooks
|
||||
run_tests = True # run tests (disabled by --list-foo)
|
||||
postmortem = False # invoke pdb when an exception occurs
|
||||
|
||||
# output verbosity
|
||||
verbosity = 0 # verbosity level (-v)
|
||||
quiet = 0 # do not print anything on success (-q)
|
||||
warn_omitted = False # produce warnings when a test case is
|
||||
# not included in a test suite (-w)
|
||||
print_import_time = True # print time taken to import test modules
|
||||
# (currently hardcoded)
|
||||
progress = False # show running progress (-p)
|
||||
coverage = False # produce coverage reports (--coverage)
|
||||
coverdir = 'coverage' # where to put them (currently hardcoded)
|
||||
immediate_errors = True # show tracebacks twice (--immediate-errors,
|
||||
# --delayed-errors)
|
||||
screen_width = 80 # screen width (autodetected)
|
||||
|
||||
|
||||
def compile_matcher(regex):
|
||||
"""Returns a function that takes one argument and returns True or False.
|
||||
|
||||
Regex is a regular expression. Empty regex matches everything. There
|
||||
is one expression: if the regex starts with "!", the meaning of it is
|
||||
reversed.
|
||||
"""
|
||||
if not regex:
|
||||
return lambda x: True
|
||||
elif regex == '!':
|
||||
return lambda x: False
|
||||
elif regex.startswith('!'):
|
||||
rx = re.compile(regex[1:])
|
||||
return lambda x: rx.search(x) is None
|
||||
else:
|
||||
rx = re.compile(regex)
|
||||
return lambda x: rx.search(x) is not None
|
||||
|
||||
|
||||
def walk_with_symlinks(top, func, arg):
|
||||
"""Like os.path.walk, but follows symlinks on POSIX systems.
|
||||
|
||||
If the symlinks create a loop, this function will never finish.
|
||||
"""
|
||||
try:
|
||||
names = os.listdir(top)
|
||||
except os.error:
|
||||
return
|
||||
func(arg, top, names)
|
||||
exceptions = ('.', '..')
|
||||
for name in names:
|
||||
if name not in exceptions:
|
||||
name = os.path.join(top, name)
|
||||
if os.path.isdir(name):
|
||||
walk_with_symlinks(name, func, arg)
|
||||
|
||||
|
||||
def get_test_files(cfg):
|
||||
"""Returns a list of test module filenames."""
|
||||
matcher = compile_matcher(cfg.pathname_regex)
|
||||
results = []
|
||||
test_names = []
|
||||
if cfg.unit_tests:
|
||||
test_names.append('tests')
|
||||
if cfg.functional_tests:
|
||||
test_names.append('ftests')
|
||||
baselen = len(cfg.basedir) + 1
|
||||
def visit(ignored, dir, files):
|
||||
# Ignore files starting with a dot.
|
||||
# Do not not descend into subdirs containing with a dot.
|
||||
remove = []
|
||||
for idx, file in enumerate(files):
|
||||
if file.startswith('.'):
|
||||
remove.append(idx)
|
||||
elif '.' in file and os.path.isdir(os.path.join(dir, file)):
|
||||
remove.append(idx)
|
||||
remove.reverse()
|
||||
for idx in remove:
|
||||
del files[idx]
|
||||
# Look for tests.py and/or ftests.py
|
||||
if os.path.basename(dir) not in test_names:
|
||||
for name in test_names:
|
||||
if name + '.py' in files:
|
||||
path = os.path.join(dir, name + '.py')
|
||||
if matcher(path[baselen:]):
|
||||
results.append(path)
|
||||
return
|
||||
if '__init__.py' not in files:
|
||||
print >> sys.stderr, "%s is not a package" % dir
|
||||
return
|
||||
for file in files:
|
||||
if file.startswith('test') and file.endswith('.py'):
|
||||
path = os.path.join(dir, file)
|
||||
if matcher(path[baselen:]):
|
||||
results.append(path)
|
||||
if cfg.follow_symlinks:
|
||||
walker = walk_with_symlinks
|
||||
else:
|
||||
walker = os.path.walk
|
||||
for dir in cfg.search_in:
|
||||
walker(dir, visit, None)
|
||||
results.sort()
|
||||
return results
|
||||
|
||||
|
||||
def import_module(filename, cfg, tracer=None):
|
||||
"""Imports and returns a module."""
|
||||
filename = os.path.splitext(filename)[0]
|
||||
modname = filename[len(cfg.basedir):].replace(os.path.sep, '.')
|
||||
if modname.startswith('.'):
|
||||
modname = modname[1:]
|
||||
if tracer is not None:
|
||||
mod = tracer.runfunc(__import__, modname)
|
||||
else:
|
||||
mod = __import__(modname)
|
||||
components = modname.split('.')
|
||||
for comp in components[1:]:
|
||||
mod = getattr(mod, comp)
|
||||
return mod
|
||||
|
||||
|
||||
def filter_testsuite(suite, matcher, level=None):
|
||||
"""Returns a flattened list of test cases that match the given matcher."""
|
||||
if not isinstance(suite, unittest.TestSuite):
|
||||
raise TypeError('not a TestSuite', suite)
|
||||
results = []
|
||||
for test in suite._tests:
|
||||
if level is not None and getattr(test, 'level', 0) > level:
|
||||
continue
|
||||
if isinstance(test, unittest.TestCase):
|
||||
testname = test.id() # package.module.class.method
|
||||
if matcher(testname):
|
||||
results.append(test)
|
||||
else:
|
||||
filtered = filter_testsuite(test, matcher, level)
|
||||
results.extend(filtered)
|
||||
return results
|
||||
|
||||
|
||||
def get_all_test_cases(module):
|
||||
"""Returns a list of all test case classes defined in a given module."""
|
||||
results = []
|
||||
for name in dir(module):
|
||||
if not name.startswith('Test'):
|
||||
continue
|
||||
item = getattr(module, name)
|
||||
if (isinstance(item, (type, types.ClassType)) and
|
||||
issubclass(item, unittest.TestCase)):
|
||||
results.append(item)
|
||||
return results
|
||||
|
||||
|
||||
def get_test_classes_from_testsuite(suite):
|
||||
"""Returns a set of test case classes used in a test suite."""
|
||||
if not isinstance(suite, unittest.TestSuite):
|
||||
raise TypeError('not a TestSuite', suite)
|
||||
results = Set()
|
||||
for test in suite._tests:
|
||||
if isinstance(test, unittest.TestCase):
|
||||
results.add(test.__class__)
|
||||
else:
|
||||
classes = get_test_classes_from_testsuite(test)
|
||||
results.update(classes)
|
||||
return results
|
||||
|
||||
|
||||
def get_test_cases(test_files, cfg, tracer=None):
|
||||
"""Returns a list of test cases from a given list of test modules."""
|
||||
matcher = compile_matcher(cfg.test_regex)
|
||||
results = []
|
||||
startTime = time.time()
|
||||
for file in test_files:
|
||||
module = import_module(file, cfg, tracer=tracer)
|
||||
try:
|
||||
func = module.test_suite
|
||||
except AttributeError:
|
||||
print >> sys.stderr
|
||||
print >> sys.stderr, ("%s: WARNING: there is no test_suite"
|
||||
" function" % file)
|
||||
print >> sys.stderr
|
||||
continue
|
||||
if tracer is not None:
|
||||
test_suite = tracer.runfunc(func)
|
||||
else:
|
||||
test_suite = func()
|
||||
if test_suite is None:
|
||||
continue
|
||||
if cfg.warn_omitted:
|
||||
all_classes = Set(get_all_test_cases(module))
|
||||
classes_in_suite = get_test_classes_from_testsuite(test_suite)
|
||||
difference = all_classes - classes_in_suite
|
||||
for test_class in difference:
|
||||
# surround the warning with blank lines, otherwise it tends
|
||||
# to get lost in the noise
|
||||
print >> sys.stderr
|
||||
print >> sys.stderr, ("%s: WARNING: %s not in test suite"
|
||||
% (file, test_class.__name__))
|
||||
print >> sys.stderr
|
||||
if (cfg.level is not None and
|
||||
getattr(test_suite, 'level', 0) > cfg.level):
|
||||
continue
|
||||
filtered = filter_testsuite(test_suite, matcher, cfg.level)
|
||||
results.extend(filtered)
|
||||
stopTime = time.time()
|
||||
timeTaken = float(stopTime - startTime)
|
||||
if cfg.print_import_time:
|
||||
nmodules = len(test_files)
|
||||
print "Imported %d modules in %.3fs" % (nmodules, timeTaken)
|
||||
print
|
||||
return results
|
||||
|
||||
|
||||
def get_test_hooks(test_files, cfg, tracer=None):
|
||||
"""Returns a list of test hooks from a given list of test modules."""
|
||||
results = []
|
||||
dirs = Set(map(os.path.dirname, test_files))
|
||||
for dir in list(dirs):
|
||||
if os.path.basename(dir) == 'ftests':
|
||||
dirs.add(os.path.join(os.path.dirname(dir), 'tests'))
|
||||
dirs = list(dirs)
|
||||
dirs.sort()
|
||||
for dir in dirs:
|
||||
filename = os.path.join(dir, 'checks.py')
|
||||
if os.path.exists(filename):
|
||||
module = import_module(filename, cfg, tracer=tracer)
|
||||
if tracer is not None:
|
||||
hooks = tracer.runfunc(module.test_hooks)
|
||||
else:
|
||||
hooks = module.test_hooks()
|
||||
results.extend(hooks)
|
||||
return results
|
||||
|
||||
|
||||
def extract_tb(tb, limit=None):
|
||||
"""Improved version of traceback.extract_tb.
|
||||
|
||||
Includes a dict with locals in every stack frame instead of the line.
|
||||
"""
|
||||
list = []
|
||||
while tb is not None and (limit is None or len(list) < limit):
|
||||
frame = tb.tb_frame
|
||||
code = frame.f_code
|
||||
name = code.co_name
|
||||
filename = code.co_filename
|
||||
lineno = tb.tb_lineno
|
||||
locals = frame.f_locals
|
||||
list.append((filename, lineno, name, locals))
|
||||
tb = tb.tb_next
|
||||
return list
|
||||
|
||||
def format_exception(etype, value, tb, limit=None, basedir=None):
|
||||
"""Improved version of traceback.format_exception.
|
||||
|
||||
Includes Zope-specific extra information in tracebacks.
|
||||
"""
|
||||
list = []
|
||||
if tb:
|
||||
list = ['Traceback (most recent call last):\n']
|
||||
w = list.append
|
||||
|
||||
for filename, lineno, name, locals in extract_tb(tb, limit):
|
||||
w(' File "%s", line %s, in %s\n' % (filename, lineno, name))
|
||||
line = linecache.getline(filename, lineno)
|
||||
if line:
|
||||
w(' %s\n' % line.strip())
|
||||
tb_info = locals.get('__traceback_info__')
|
||||
if tb_info is not None:
|
||||
w(' Extra information: %s\n' % repr(tb_info))
|
||||
tb_supplement = locals.get('__traceback_supplement__')
|
||||
if tb_supplement is not None:
|
||||
tb_supplement = tb_supplement[0](*tb_supplement[1:])
|
||||
# XXX these should be hookable
|
||||
from zope.tales.tales import TALESTracebackSupplement
|
||||
from zope.pagetemplate.pagetemplate \
|
||||
import PageTemplateTracebackSupplement
|
||||
if isinstance(tb_supplement, PageTemplateTracebackSupplement):
|
||||
template = tb_supplement.manageable_object.pt_source_file()
|
||||
if template:
|
||||
w(' Template "%s"\n' % template)
|
||||
elif isinstance(tb_supplement, TALESTracebackSupplement):
|
||||
w(' Template "%s", line %s, column %s\n'
|
||||
% (tb_supplement.source_url, tb_supplement.line,
|
||||
tb_supplement.column))
|
||||
line = linecache.getline(tb_supplement.source_url,
|
||||
tb_supplement.line)
|
||||
if line:
|
||||
w(' %s\n' % line.strip())
|
||||
w(' Expression: %s\n' % tb_supplement.expression)
|
||||
else:
|
||||
w(' __traceback_supplement__ = %r\n' % (tb_supplement, ))
|
||||
list += traceback.format_exception_only(etype, value)
|
||||
return list
|
||||
|
||||
|
||||
class CustomTestResult(unittest._TextTestResult):
|
||||
"""Customised TestResult.
|
||||
|
||||
It can show a progress bar, and displays tracebacks for errors and failures
|
||||
as soon as they happen, in addition to listing them all at the end.
|
||||
"""
|
||||
|
||||
__super = unittest._TextTestResult
|
||||
__super_init = __super.__init__
|
||||
__super_startTest = __super.startTest
|
||||
__super_stopTest = __super.stopTest
|
||||
__super_printErrors = __super.printErrors
|
||||
__super_printErrorList = __super.printErrorList
|
||||
|
||||
def __init__(self, stream, descriptions, verbosity, count, cfg, hooks):
|
||||
self.__super_init(stream, descriptions, verbosity)
|
||||
self.count = count
|
||||
self.cfg = cfg
|
||||
self.hooks = hooks
|
||||
if cfg.progress:
|
||||
self.dots = False
|
||||
self._lastWidth = 0
|
||||
self._maxWidth = cfg.screen_width - len("xxxx/xxxx (xxx.x%): ") - 1
|
||||
|
||||
def startTest(self, test):
|
||||
n = self.testsRun + 1
|
||||
if self.cfg.progress:
|
||||
# verbosity == 0: 'xxxx/xxxx (xxx.x%)'
|
||||
# verbosity == 1: 'xxxx/xxxx (xxx.x%): test name'
|
||||
# verbosity >= 2: 'xxxx/xxxx (xxx.x%): test name ... ok'
|
||||
self.stream.write("\r%4d" % n)
|
||||
if self.count:
|
||||
self.stream.write("/%d (%5.1f%%)"
|
||||
% (self.count, n * 100.0 / self.count))
|
||||
if self.showAll: # self.cfg.verbosity == 1
|
||||
self.stream.write(": ")
|
||||
elif self.cfg.verbosity:
|
||||
name = self.getShortDescription(test)
|
||||
width = len(name)
|
||||
if width < self._lastWidth:
|
||||
name += " " * (self._lastWidth - width)
|
||||
self.stream.write(": %s" % name)
|
||||
self._lastWidth = width
|
||||
self.stream.flush()
|
||||
self.__super_startTest(test) # increments testsRun by one and prints
|
||||
self.testsRun = n # override the testsRun calculation
|
||||
for hook in self.hooks:
|
||||
hook.startTest(test)
|
||||
|
||||
def stopTest(self, test):
|
||||
for hook in self.hooks:
|
||||
hook.stopTest(test)
|
||||
self.__super_stopTest(test)
|
||||
|
||||
def getDescription(self, test):
|
||||
return test.id() # package.module.class.method
|
||||
|
||||
def getShortDescription(self, test):
|
||||
s = test.id() # package.module.class.method
|
||||
if len(s) > self._maxWidth:
|
||||
namelen = len(s.split('.')[-1])
|
||||
left = max(0, (self._maxWidth - namelen) / 2 - 1)
|
||||
right = self._maxWidth - left - 3
|
||||
s = "%s...%s" % (s[:left], s[-right:])
|
||||
return s
|
||||
|
||||
def printErrors(self):
|
||||
if self.cfg.progress and not (self.dots or self.showAll):
|
||||
self.stream.writeln()
|
||||
if self.cfg.immediate_errors and (self.errors or self.failures):
|
||||
self.stream.writeln(self.separator1)
|
||||
self.stream.writeln("Tests that failed")
|
||||
self.stream.writeln(self.separator2)
|
||||
self.__super_printErrors()
|
||||
|
||||
def formatError(self, err):
|
||||
return "".join(format_exception(basedir=self.cfg.basedir, *err))
|
||||
|
||||
def printTraceback(self, kind, test, err):
|
||||
self.stream.writeln()
|
||||
self.stream.writeln(self.separator1)
|
||||
self.stream.writeln("%s: %s" % (kind, self.getDescription(test)))
|
||||
self.stream.writeln(self.separator2)
|
||||
self.stream.writeln(self.formatError(err))
|
||||
self.stream.writeln()
|
||||
|
||||
def addFailure(self, test, err):
|
||||
if self.cfg.immediate_errors:
|
||||
self.printTraceback("FAIL", test, err)
|
||||
if self.cfg.postmortem:
|
||||
pdb.post_mortem(sys.exc_info()[2])
|
||||
self.failures.append((test, self.formatError(err)))
|
||||
|
||||
def addError(self, test, err):
|
||||
if self.cfg.immediate_errors:
|
||||
self.printTraceback("ERROR", test, err)
|
||||
if self.cfg.postmortem:
|
||||
pdb.post_mortem(sys.exc_info()[2])
|
||||
self.errors.append((test, self.formatError(err)))
|
||||
|
||||
def printErrorList(self, flavour, errors):
|
||||
if self.cfg.immediate_errors:
|
||||
for test, err in errors:
|
||||
description = self.getDescription(test)
|
||||
self.stream.writeln("%s: %s" % (flavour, description))
|
||||
else:
|
||||
self.__super_printErrorList(flavour, errors)
|
||||
|
||||
|
||||
class CustomTestRunner(unittest.TextTestRunner):
|
||||
"""Customised TestRunner.
|
||||
|
||||
See CustomisedTextResult for a list of extensions.
|
||||
"""
|
||||
|
||||
__super = unittest.TextTestRunner
|
||||
__super_init = __super.__init__
|
||||
__super_run = __super.run
|
||||
|
||||
def __init__(self, cfg, hooks=None, stream=sys.stderr, count=None):
|
||||
self.__super_init(verbosity=cfg.verbosity, stream=stream)
|
||||
self.cfg = cfg
|
||||
if hooks is not None:
|
||||
self.hooks = hooks
|
||||
else:
|
||||
self.hooks = []
|
||||
self.count = count
|
||||
|
||||
def run(self, test):
|
||||
"""Run the given test case or test suite."""
|
||||
if self.count is None:
|
||||
self.count = test.countTestCases()
|
||||
result = self._makeResult()
|
||||
startTime = time.time()
|
||||
test(result)
|
||||
stopTime = time.time()
|
||||
timeTaken = float(stopTime - startTime)
|
||||
result.printErrors()
|
||||
run = result.testsRun
|
||||
if not self.cfg.quiet:
|
||||
self.stream.writeln(result.separator2)
|
||||
self.stream.writeln("Ran %d test%s in %.3fs" %
|
||||
(run, run != 1 and "s" or "", timeTaken))
|
||||
self.stream.writeln()
|
||||
if not result.wasSuccessful():
|
||||
self.stream.write("FAILED (")
|
||||
failed, errored = map(len, (result.failures, result.errors))
|
||||
if failed:
|
||||
self.stream.write("failures=%d" % failed)
|
||||
if errored:
|
||||
if failed: self.stream.write(", ")
|
||||
self.stream.write("errors=%d" % errored)
|
||||
self.stream.writeln(")")
|
||||
elif not self.cfg.quiet:
|
||||
self.stream.writeln("OK")
|
||||
return result
|
||||
|
||||
def _makeResult(self):
|
||||
return CustomTestResult(self.stream, self.descriptions, self.verbosity,
|
||||
cfg=self.cfg, count=self.count,
|
||||
hooks=self.hooks)
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""Main program."""
|
||||
|
||||
# Environment
|
||||
if sys.version_info < (2, 3):
|
||||
print >> sys.stderr, '%s: need Python 2.3 or later' % argv[0]
|
||||
print >> sys.stderr, 'your python is %s' % sys.version
|
||||
return 1
|
||||
|
||||
# Defaults
|
||||
cfg = Options()
|
||||
cfg.basedir = os.path.join(os.path.dirname(argv[0]), 'src')
|
||||
cfg.basedir = os.path.abspath(cfg.basedir)
|
||||
|
||||
# Figure out terminal size
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
curses.setupterm()
|
||||
cols = curses.tigetnum('cols')
|
||||
if cols > 0:
|
||||
cfg.screen_width = cols
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
# Option processing
|
||||
try:
|
||||
opts, args = getopt.gnu_getopt(argv[1:], 'hvpqufwd',
|
||||
['list-files', 'list-tests', 'list-hooks',
|
||||
'level=', 'all-levels', 'coverage',
|
||||
'search-in=', 'immediate-errors',
|
||||
'delayed-errors', 'help'])
|
||||
except getopt.error, e:
|
||||
print >> sys.stderr, '%s: %s' % (argv[0], e)
|
||||
print >> sys.stderr, 'run %s -h for help' % argv[0]
|
||||
return 1
|
||||
for k, v in opts:
|
||||
if k in ['-h', '--help']:
|
||||
print __doc__
|
||||
return 0
|
||||
elif k == '-v':
|
||||
cfg.verbosity += 1
|
||||
cfg.quiet = False
|
||||
elif k == '-p':
|
||||
cfg.progress = True
|
||||
cfg.quiet = False
|
||||
elif k == '-q':
|
||||
cfg.verbosity = 0
|
||||
cfg.progress = False
|
||||
cfg.quiet = True
|
||||
elif k == '-u':
|
||||
cfg.unit_tests = True
|
||||
elif k == '-f':
|
||||
cfg.functional_tests = True
|
||||
elif k == '-d':
|
||||
cfg.postmortem = True
|
||||
elif k == '-w':
|
||||
cfg.warn_omitted = True
|
||||
elif k == '--list-files':
|
||||
cfg.list_files = True
|
||||
cfg.run_tests = False
|
||||
elif k == '--list-tests':
|
||||
cfg.list_tests = True
|
||||
cfg.run_tests = False
|
||||
elif k == '--list-hooks':
|
||||
cfg.list_hooks = True
|
||||
cfg.run_tests = False
|
||||
elif k == '--coverage':
|
||||
cfg.coverage = True
|
||||
elif k == '--level':
|
||||
try:
|
||||
cfg.level = int(v)
|
||||
except ValueError:
|
||||
print >> sys.stderr, '%s: invalid level: %s' % (argv[0], v)
|
||||
print >> sys.stderr, 'run %s -h for help' % argv[0]
|
||||
return 1
|
||||
elif k == '--all-levels':
|
||||
cfg.level = None
|
||||
elif k == '--search-in':
|
||||
dir = os.path.abspath(v)
|
||||
if not dir.startswith(cfg.basedir):
|
||||
print >> sys.stderr, ('%s: argument to --search-in (%s) must'
|
||||
' be a subdir of %s'
|
||||
% (argv[0], v, cfg.basedir))
|
||||
return 1
|
||||
cfg.search_in += (dir, )
|
||||
elif k == '--immediate-errors':
|
||||
cfg.immediate_errors = True
|
||||
elif k == '--delayed-errors':
|
||||
cfg.immediate_errors = False
|
||||
else:
|
||||
print >> sys.stderr, '%s: invalid option: %s' % (argv[0], k)
|
||||
print >> sys.stderr, 'run %s -h for help' % argv[0]
|
||||
return 1
|
||||
if args:
|
||||
cfg.pathname_regex = args[0]
|
||||
if len(args) > 1:
|
||||
cfg.test_regex = args[1]
|
||||
if len(args) > 2:
|
||||
print >> sys.stderr, '%s: too many arguments: %s' % (argv[0], args[2])
|
||||
print >> sys.stderr, 'run %s -h for help' % argv[0]
|
||||
return 1
|
||||
if not cfg.unit_tests and not cfg.functional_tests:
|
||||
cfg.unit_tests = True
|
||||
|
||||
if not cfg.search_in:
|
||||
cfg.search_in = (cfg.basedir, )
|
||||
|
||||
# Do not print "Imported %d modules in %.3fs" if --list-XXX was specified
|
||||
if cfg.list_tests or cfg.list_hooks or cfg.list_files:
|
||||
cfg.print_import_time = False
|
||||
|
||||
# Set up the python path
|
||||
sys.path[0] = cfg.basedir
|
||||
# XXX The following bit is SchoolTool specific: we need the Zope3 tree in
|
||||
# sys.path, in addition to basedir.
|
||||
sys.path.insert(1, os.path.join(os.path.dirname(cfg.basedir),
|
||||
'Zope3', 'src'))
|
||||
|
||||
# Set up tracing before we start importing things
|
||||
tracer = None
|
||||
if cfg.run_tests and cfg.coverage:
|
||||
import trace
|
||||
# trace.py in Python 2.3.1 is buggy:
|
||||
# 1) Despite sys.prefix being in ignoredirs, a lot of system-wide
|
||||
# modules are included in the coverage reports
|
||||
# 2) Some module file names do not have the first two characters,
|
||||
# and in general the prefix used seems to be arbitrary
|
||||
# These bugs are fixed in src/trace.py which should be in PYTHONPATH
|
||||
# before the official one.
|
||||
ignoremods = ['test']
|
||||
ignoredirs = [sys.prefix, sys.exec_prefix]
|
||||
tracer = trace.Trace(count=True, trace=False,
|
||||
ignoremods=ignoremods, ignoredirs=ignoredirs)
|
||||
|
||||
# Finding and importing
|
||||
test_files = get_test_files(cfg)
|
||||
if cfg.list_tests or cfg.run_tests:
|
||||
test_cases = get_test_cases(test_files, cfg, tracer=tracer)
|
||||
if cfg.list_hooks or cfg.run_tests:
|
||||
test_hooks = get_test_hooks(test_files, cfg, tracer=tracer)
|
||||
|
||||
# Configure the logging module
|
||||
import logging
|
||||
logging.basicConfig()
|
||||
logging.root.setLevel(logging.CRITICAL)
|
||||
|
||||
# Running
|
||||
success = True
|
||||
if cfg.list_files:
|
||||
baselen = len(cfg.basedir) + 1
|
||||
print "\n".join([fn[baselen:] for fn in test_files])
|
||||
if cfg.list_tests:
|
||||
print "\n".join([test.id() for test in test_cases])
|
||||
if cfg.list_hooks:
|
||||
print "\n".join([str(hook) for hook in test_hooks])
|
||||
if cfg.run_tests:
|
||||
runner = CustomTestRunner(cfg, test_hooks, count=len(test_cases))
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTests(test_cases)
|
||||
if tracer is not None:
|
||||
success = tracer.runfunc(runner.run, suite).wasSuccessful()
|
||||
results = tracer.results()
|
||||
results.write_results(show_missing=True, coverdir=cfg.coverdir)
|
||||
else:
|
||||
success = runner.run(suite).wasSuccessful()
|
||||
|
||||
# That's all
|
||||
if success:
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exitcode = main(sys.argv)
|
||||
sys.exit(exitcode)
|
|
@ -0,0 +1 @@
|
|||
0.9.4
|
Ładowanie…
Reference in New Issue