Initial import of iCalendar, by Max M.

pull/6/head
Martijn Faassen 2005-03-23 13:49:29 +00:00
commit ee533bd510
22 zmienionych plików z 3983 dodań i 0 usunięć

125
CHANGES.txt 100644
Wyświetl plik

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

91
README.txt 100644
Wyświetl plik

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

5
TODO.txt 100644
Wyświetl plik

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

16
doc/example.ics 100644
Wyświetl plik

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

307
doc/example.py 100644
Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

25
doc/test.ics 100644
Wyświetl plik

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

35
doc/test.py 100644
Wyświetl plik

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

16
example.ics 100644
Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1 @@
# this is a package

Wyświetl plik

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

Wyświetl plik

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

9
test.ics 100644
Wyświetl plik

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

744
test.py 100755
Wyświetl plik

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

1
version.txt 100644
Wyświetl plik

@ -0,0 +1 @@
0.9.4