Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
  • 15 commits
  • 22 files changed
  • 0 commit comments
  • 1 contributor
Commits on Sep 22, 2012
@kfdm Updates to .gitignore 758f52e
Commits on Oct 13, 2012
@kfdm Add pypy to tox for testing 9cca9e6
Commits on Oct 22, 2012
@kfdm Make sure we open binary files as "rb"
Fixes #31
d639fa2
@kfdm Initial socket error test 4f6b61e
Commits on Dec 08, 2012
@kfdm Remove code duplication from gntp.config
By taking advantage of **kwargs we can remove a lot of the code
duplication. To support this, we add a notifierFactory method to our
mini method though we have to put the mini method at the bottom of the
source file now, to ensure GrowlNotifier is defined.

Fixes #33. Thanks @xdgc
08576fd
Commits on Dec 09, 2012
@kfdm Remove code duplication from gntp.config
By taking advantage of **kwargs we can remove a lot of the code
duplication. To support this, we add a notifierFactory method to our
mini method though we have to put the mini method at the bottom of the
source file now, to ensure GrowlNotifier is defined.

Fixes #33. Thanks @xdgc
6f37531
@kfdm Move Exceptions into their own module 68a3c7d
@kfdm Catch socket errors and rethrow as a GNTP Error c39f66f
@kfdm Swallow Exceptions when using the "mini" growl function 7c3401a
@kfdm Update egg 37abc91
Commits on Jan 13, 2013
@kfdm Move __version__ to it's own module 980c0e2
Commits on Jan 21, 2013
@kfdm Switch to using GNTP errors for everything
The mini function is supposed to be "fire and forget" so we will always
swallow errors (but log them). For the notifier functions we'll always
return a gntp.errors.BaseError to make it easier for implementing
programs to have a single error they can test against.

Merge branch 'socket-error-tests'

Conflicts:
	gntp/__init__.py
	gntp/notifier.py
398abaf
Commits on Jan 24, 2013
@kfdm Fix building docs d6f7ac5
Commits on Jan 25, 2013
@kfdm Remove egg data
Though I have not been able to find a source that stated as clearly as
I would prefer, it seems like this information is NOT required to
support installing from pip with a git repo
f344ccb
@kfdm Update version to 0.9 97220f3
View
4 .gitignore
@@ -2,8 +2,10 @@ dist/*
build/*
MANIFEST
README.html
+*.egg-info/*
# Testing files
.tox/*
.coverage
-htmlcov
+cover/*
+htmlcov/*
View
8 README.rst
@@ -70,7 +70,7 @@ You can send the image along with the notification to get around this.
::
- image = open('/path/to/image.png').read()
+ image = open('/path/to/image.png', 'rb').read()
growl.notify(
noteType = "New Messages",
title = "You have a new message",
@@ -88,6 +88,12 @@ Bugs
Changelog
---------
+`v0.9 <https://github.com/kfdm/gntp/compare/v0.8...v0.9>`_
+ - Remove duplicate code from gntp.config
+ - Catch all errors and rethrow them as gntp.errors to make it easier for
+ other programs to deal with errors from the gntp library.
+ - Ensure that we open resource files as "rb" and update the documentation
+
`v0.8 <https://github.com/kfdm/gntp/compare/v0.7...v0.8>`_
- Fix a bug where resource sections were missing a CRLF
- Fix a bug where the cli client was using config values over options
View
5 docs/conf.py
@@ -11,9 +11,10 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys, os
+import sys
+import os
sys.path.insert(0, os.path.abspath('..'))
-from gntp import __version__ as gntpversion
+from gntp.version import __version__ as gntpversion
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
View
6 docs/core.rst
@@ -5,11 +5,11 @@ Lower level classes for those who want more control in sending messages
Exceptions
----------
-.. autoexception:: gntp.AuthError
+.. autoexception:: gntp.errors.AuthError
-.. autoexception:: gntp.ParseError
+.. autoexception:: gntp.errors.ParseError
-.. autoexception:: gntp.UnsupportedError
+.. autoexception:: gntp.errors.UnsupportedError
GNTP Messages
-------------
View
2  docs/index.rst
@@ -82,7 +82,7 @@ Complete Example
)
# Send the image with the growl notification
- image = open('/path/to/icon.png').read()
+ image = open('/path/to/icon.png', 'rb').read()
growl.notify(
noteType = "New Messages",
title = "Now with icons",
View
121 gntp.egg-info/PKG-INFO
@@ -1,121 +0,0 @@
-Metadata-Version: 1.0
-Name: gntp
-Version: 0.8
-Summary: Growl Notification Transport Protocol for Python
-Home-page: http://github.com/kfdm/gntp/
-Author: Paul Traylor
-Author-email: UNKNOWN
-License: UNKNOWN
-Description: GNTP
- ====
-
- This is a Python library for working with the `Growl Notification
- Transport Protocol <http://www.growlforwindows.com/gfw/help/gntp.aspx>`_
-
- It should work as a dropin replacement for the older Python bindings
-
- Installation
- ------------
-
- You can install with pip
-
- ::
-
- $ pip install gntp
-
- then test the module
-
- ::
-
- $ python -m gntp.notifier
-
- Simple Usage
- ------------
-
- ::
-
- import gntp.notifier
-
- # Simple "fire and forget" notification
- gntp.notifier.mini("Here's a quick message")
-
- # More complete example
- growl = gntp.notifier.GrowlNotifier(
- applicationName = "My Application Name",
- notifications = ["New Updates","New Messages"],
- defaultNotifications = ["New Messages"],
- # hostname = "computer.example.com", # Defaults to localhost
- # password = "abc123" # Defaults to a blank password
- )
- growl.register()
-
- # Send one message
- growl.notify(
- noteType = "New Messages",
- title = "You have a new message",
- description = "A longer message description",
- icon = "http://example.com/icon.png",
- sticky = False,
- priority = 1,
- )
-
- # Try to send a different type of message
- # This one may fail since it is not in our list
- # of defaultNotifications
- growl.notify(
- noteType = "New Updates",
- title = "There is a new update to download",
- description = "A longer message description",
- icon = "http://example.com/icon.png",
- sticky = False,
- priority = -1,
- )
-
-
- URL based images do not work in the OSX version of
- `growl <http://code.google.com/p/growl/issues/detail?id=423>`_ 1.4
- You can send the image along with the notification to get around this.
-
- ::
-
- image = open('/path/to/image.png').read()
- growl.notify(
- noteType = "New Messages",
- title = "You have a new message",
- description = "This time we embed the image",
- icon = image,
- )
-
-
- Bugs
- ----
-
- `GitHub issue tracker <https://github.com/kfdm/gntp/issues>`_
-
-
- Changelog
- ---------
-
- `v0.8 <https://github.com/kfdm/gntp/compare/v0.7...v0.8>`_
- - Fix a bug where resource sections were missing a CRLF
- - Fix a bug where the cli client was using config values over options
- - Add support for coalescing
-
- `v0.7 <https://github.com/kfdm/gntp/compare/0.6...v0.7>`_
- - Support for images
- - Better test coverage support
-
- `0.6 <https://github.com/kfdm/gntp/compare/0.5...0.6>`_
- - ConfigParser aware GrowlNotifier that reads settings from ~/.gntp
-
-
-
-Platform: UNKNOWN
-Classifier: Development Status :: 3 - Alpha
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Natural Language :: English
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python :: 2.5
-Classifier: Programming Language :: Python :: 2.6
-Classifier: Programming Language :: Python :: 2.7
View
12 gntp.egg-info/SOURCES.txt
@@ -1,12 +0,0 @@
-MANIFEST.in
-README.rst
-setup.py
-gntp/__init__.py
-gntp/cli.py
-gntp/config.py
-gntp/notifier.py
-gntp.egg-info/PKG-INFO
-gntp.egg-info/SOURCES.txt
-gntp.egg-info/dependency_links.txt
-gntp.egg-info/entry_points.txt
-gntp.egg-info/top_level.txt
View
1  gntp.egg-info/dependency_links.txt
@@ -1 +0,0 @@
-
View
3  gntp.egg-info/entry_points.txt
@@ -1,3 +0,0 @@
-[console_scripts]
-gntp = gntp.cli:main
-
View
1  gntp.egg-info/top_level.txt
@@ -1 +0,0 @@
-gntp
View
47 gntp/__init__.py
@@ -3,7 +3,7 @@
import time
import StringIO
-__version__ = '0.8'
+import gntp.errors as errors
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
GNTP_INFO_LINE = re.compile(
@@ -23,27 +23,6 @@
GNTP_EOL = '\r\n'
-class BaseError(Exception):
- def gntp_error(self):
- error = GNTPError(self.errorcode, self.errordesc)
- return error.encode()
-
-
-class ParseError(BaseError):
- errorcode = 500
- errordesc = 'Error parsing the message'
-
-
-class AuthError(BaseError):
- errorcode = 400
- errordesc = 'Error with authorization'
-
-
-class UnsupportedError(BaseError):
- errorcode = 500
- errordesc = 'Currently unsupported by gntp.py'
-
-
class _GNTPBuffer(StringIO.StringIO):
"""GNTP Buffer class"""
def writefmt(self, message="", *args):
@@ -81,7 +60,7 @@ def _parse_info(self, data):
match = GNTP_INFO_LINE.match(data)
if not match:
- raise ParseError('ERROR_PARSING_INFO_LINE')
+ raise errors.ParseError('ERROR_PARSING_INFO_LINE')
info = match.groupdict()
if info['encryptionAlgorithmID'] == 'NONE':
@@ -109,7 +88,7 @@ def set_password(self, password, encryptAlgo='MD5'):
self.info['keyHashAlgorithm'] = None
return
if not self.encryptAlgo in hash.keys():
- raise UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
+ raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
hashfunction = hash.get(self.encryptAlgo)
@@ -144,21 +123,21 @@ def _decode_binary(self, rawIdentifier, identifier):
pointerEnd = pointerStart + dataLength
data = self.raw[pointerStart:pointerEnd]
if not len(data) == dataLength:
- raise ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
+ raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
return data
def _validate_password(self, password):
"""Validate GNTP Message against stored password"""
self.password = password
if password == None:
- raise AuthError('Missing password')
+ raise errors.AuthError('Missing password')
keyHash = self.info.get('keyHash', None)
if keyHash is None and self.password is None:
return True
if keyHash is None:
- raise AuthError('Invalid keyHash')
+ raise errors.AuthError('Invalid keyHash')
if self.password is None:
- raise AuthError('Missing password')
+ raise errors.AuthError('Missing password')
password = self.password.encode('utf8')
saltHash = self._decode_hex(self.info['salt'])
@@ -168,14 +147,14 @@ def _validate_password(self, password):
keyHash = hashlib.md5(key).hexdigest()
if not keyHash.upper() == self.info['keyHash'].upper():
- raise AuthError('Invalid Hash')
+ raise errors.AuthError('Invalid Hash')
return True
def validate(self):
"""Verify required headers"""
for header in self._requiredHeaders:
if not self.headers.get(header, False):
- raise ParseError('Missing Notification Header: ' + header)
+ raise errors.ParseError('Missing Notification Header: ' + header)
def _format_info(self):
"""Generate info line for GNTP Message
@@ -300,11 +279,11 @@ def validate(self):
'''Validate required headers and validate notification headers'''
for header in self._requiredHeaders:
if not self.headers.get(header, False):
- raise ParseError('Missing Registration Header: ' + header)
+ raise errors.ParseError('Missing Registration Header: ' + header)
for notice in self.notifications:
for header in self._requiredNotificationHeaders:
if not notice.get(header, False):
- raise ParseError('Missing Notification Header: ' + header)
+ raise errors.ParseError('Missing Notification Header: ' + header)
def decode(self, data, password):
"""Decode existing GNTP Registration message
@@ -494,7 +473,7 @@ def parse_gntp(data, password=None):
"""
match = GNTP_INFO_LINE_SHORT.match(data)
if not match:
- raise ParseError('INVALID_GNTP_INFO')
+ raise errors.ParseError('INVALID_GNTP_INFO')
info = match.groupdict()
if info['messagetype'] == 'REGISTER':
return GNTPRegister(data, password=password)
@@ -506,4 +485,4 @@ def parse_gntp(data, password=None):
return GNTPOK(data)
elif info['messagetype'] == '-ERROR':
return GNTPError(data)
- raise ParseError('INVALID_GNTP_MESSAGE')
+ raise errors.ParseError('INVALID_GNTP_MESSAGE')
View
2  gntp/cli.py
@@ -3,7 +3,7 @@
import sys
import os
import logging
-from gntp import __version__
+from gntp.version import __version__
from gntp.notifier import GrowlNotifier
from optparse import OptionParser, OptionGroup
from ConfigParser import RawConfigParser
View
77 gntp/config.py
@@ -3,10 +3,6 @@
advantage of the ConfigParser module to allow us to setup some default values
(such as hostname, password, and port) in a more global way to be shared among
programs using gntp
-
-Code duplication is bad, but for right now I have copied the mini() function
-from the gntp.notifier class since I do not know of an easy way to reuse the
-code yet fire using the copy of GrowlNotifier in this module
"""
import os
import ConfigParser
@@ -21,41 +17,6 @@
logger = logging.getLogger(__name__)
-def mini(description, applicationName='PythonMini', noteType="Message",
- title="Mini Message", applicationIcon=None, hostname='localhost',
- password=None, port=23053, sticky=False, priority=None,
- callback=None, notificationIcon=None, identifier=None):
- """Single notification function
-
- Simple notification function in one line. Has only one required parameter
- and attempts to use reasonable defaults for everything else
- :param string description: Notification message
- """
- growl = GrowlNotifier(
- applicationName=applicationName,
- notifications=[noteType],
- defaultNotifications=[noteType],
- applicationIcon=applicationIcon,
- hostname=hostname,
- password=password,
- port=port,
- )
- result = growl.register()
- if result is not True:
- return result
-
- return growl.notify(
- noteType=noteType,
- title=title,
- description=description,
- icon=notificationIcon,
- sticky=sticky,
- priority=priority,
- callback=callback,
- identifier=identifier,
- )
-
-
class GrowlNotifier(gntp.notifier.GrowlNotifier):
"""
ConfigParser enhanced GrowlNotifier object
@@ -70,13 +31,11 @@ class GrowlNotifier(gntp.notifier.GrowlNotifier):
password = ?
port = ?
"""
- def __init__(self, applicationName='Python GNTP', notifications=[],
- defaultNotifications=None, applicationIcon=None, hostname='localhost',
- password=None, port=23053):
+ def __init__(self, *args, **kwargs):
config = ConfigParser.RawConfigParser({
- 'hostname': hostname,
- 'password': password,
- 'port': port,
+ 'hostname': kwargs.get('hostname', 'localhost'),
+ 'password': kwargs.get('password'),
+ 'port': kwargs.get('port', 23053),
})
config.read([os.path.expanduser('~/.gntp')])
@@ -89,17 +48,23 @@ def __init__(self, applicationName='Python GNTP', notifications=[],
logger.info('Error reading ~/.gntp config file')
config.add_section('gntp')
- self.applicationName = applicationName
- self.notifications = list(notifications)
- if defaultNotifications:
- self.defaultNotifications = list(defaultNotifications)
- else:
- self.defaultNotifications = self.notifications
- self.applicationIcon = applicationIcon
-
- self.password = config.get('gntp', 'password')
- self.hostname = config.get('gntp', 'hostname')
- self.port = config.getint('gntp', 'port')
+ kwargs['password'] = config.get('gntp', 'password')
+ kwargs['hostname'] = config.get('gntp', 'hostname')
+ kwargs['port'] = config.getint('gntp', 'port')
+
+ super(GrowlNotifier, self).__init__(*args, **kwargs)
+
+
+def mini(description, **kwargs):
+ """Single notification function
+
+ Simple notification function in one line. Has only one required parameter
+ and attempts to use reasonable defaults for everything else
+ :param string description: Notification message
+ """
+ kwargs['notifierFactory'] = GrowlNotifier
+ gntp.notifier.mini(description, **kwargs)
+
if __name__ == '__main__':
# If we're running this module directly we're likely running it as a test
View
22 gntp/errors.py
@@ -0,0 +1,22 @@
+class BaseError(Exception):
+ pass
+
+
+class ParseError(BaseError):
+ errorcode = 500
+ errordesc = 'Error parsing the message'
+
+
+class AuthError(BaseError):
+ errorcode = 400
+ errordesc = 'Error with authorization'
+
+
+class UnsupportedError(BaseError):
+ errorcode = 500
+ errordesc = 'Currently unsupported by gntp.py'
+
+
+class NetworkError(BaseError):
+ errorcode = 500
+ errordesc = "Error connecting to growl server"
View
103 gntp/notifier.py
@@ -14,6 +14,9 @@
import logging
import platform
+from gntp.version import __version__
+import gntp.errors as errors
+
__all__ = [
'mini',
'GrowlNotifier',
@@ -22,45 +25,6 @@
logger = logging.getLogger(__name__)
-def mini(description, applicationName='PythonMini', noteType="Message",
- title="Mini Message", applicationIcon=None, hostname='localhost',
- password=None, port=23053, sticky=False, priority=None,
- callback=None, notificationIcon=None, identifier=None):
- """Single notification function
-
- Simple notification function in one line. Has only one required parameter
- and attempts to use reasonable defaults for everything else
- :param string description: Notification message
-
- .. warning::
- For now, only URL callbacks are supported. In the future, the
- callback argument will also support a function
- """
- growl = GrowlNotifier(
- applicationName=applicationName,
- notifications=[noteType],
- defaultNotifications=[noteType],
- applicationIcon=applicationIcon,
- hostname=hostname,
- password=password,
- port=port,
- )
- result = growl.register()
- if result is not True:
- return result
-
- return growl.notify(
- noteType=noteType,
- title=title,
- description=description,
- icon=notificationIcon,
- sticky=sticky,
- priority=priority,
- callback=callback,
- identifier=identifier,
- )
-
-
class GrowlNotifier(object):
"""Helper class to simplfy sending Growl messages
@@ -195,7 +159,7 @@ def add_origin_info(self, packet):
"""Add optional Origin headers to message"""
packet.add_header('Origin-Machine-Name', platform.node())
packet.add_header('Origin-Software-Name', 'gntp.py')
- packet.add_header('Origin-Software-Version', gntp.__version__)
+ packet.add_header('Origin-Software-Version', __version__)
packet.add_header('Origin-Platform-Name', platform.system())
packet.add_header('Origin-Platform-Version', platform.platform())
@@ -218,11 +182,15 @@ def _send(self, messagetype, packet):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(self.socketTimeout)
- s.connect((self.hostname, self.port))
- s.send(data)
- recv_data = s.recv(1024)
- while not recv_data.endswith("\r\n\r\n"):
- recv_data += s.recv(1024)
+ try:
+ s.connect((self.hostname, self.port))
+ s.send(data)
+ recv_data = s.recv(1024)
+ while not recv_data.endswith("\r\n\r\n"):
+ recv_data += s.recv(1024)
+ except socket.error, e:
+ raise errors.NetworkError(e.message)
+
response = gntp.parse_gntp(recv_data)
s.close()
@@ -233,6 +201,51 @@ def _send(self, messagetype, packet):
logger.error('Invalid response: %s', response.error())
return response.error()
+
+def mini(description, applicationName='PythonMini', noteType="Message",
+ title="Mini Message", applicationIcon=None, hostname='localhost',
+ password=None, port=23053, sticky=False, priority=None,
+ callback=None, notificationIcon=None, identifier=None,
+ notifierFactory=GrowlNotifier):
+ """Single notification function
+
+ Simple notification function in one line. Has only one required parameter
+ and attempts to use reasonable defaults for everything else
+ :param string description: Notification message
+
+ .. warning::
+ For now, only URL callbacks are supported. In the future, the
+ callback argument will also support a function
+ """
+ try:
+ growl = notifierFactory(
+ applicationName=applicationName,
+ notifications=[noteType],
+ defaultNotifications=[noteType],
+ applicationIcon=applicationIcon,
+ hostname=hostname,
+ password=password,
+ port=port,
+ )
+ result = growl.register()
+ if result is not True:
+ return result
+
+ return growl.notify(
+ noteType=noteType,
+ title=title,
+ description=description,
+ icon=notificationIcon,
+ sticky=sticky,
+ priority=priority,
+ callback=callback,
+ identifier=identifier,
+ )
+ except Exception:
+ # We want the "mini" function to be simple and swallow Exceptions
+ # in order to be less invasive
+ logging.exception("Growl error")
+
if __name__ == '__main__':
# If we're running this module directly we're likely running it as a test
# so extra debugging is useful
View
5 gntp/test/basic_tests.py
@@ -10,6 +10,7 @@
import gntp
import gntp.config
import gntp.notifier
+import gntp.errors as errors
ICON_URL = "https://www.google.com/intl/en_com/images/srpr/logo3w.png"
ICON_FILE = os.path.join(os.path.dirname(__file__), "growl-icon.png")
@@ -50,7 +51,7 @@ def test_unknown_note(self):
self.assertRaises(AssertionError, self._notify, noteType='Unknown')
def test_parse_error(self):
- self.assertRaises(gntp.ParseError, gntp.parse_gntp, 'Invalid GNTP Packet')
+ self.assertRaises(errors.ParseError, gntp.parse_gntp, 'Invalid GNTP Packet')
def test_url_icon(self):
self.assertIsTrue(self._notify(
@@ -60,7 +61,7 @@ def test_url_icon(self):
def test_file_icon(self):
self.assertIsTrue(self._notify(
- icon=open(ICON_FILE).read(),
+ icon=open(ICON_FILE, 'rb').read(),
description='test_file_icon',
))
View
20 gntp/test/test_errors.py
@@ -0,0 +1,20 @@
+"""
+Test the various error condtions
+
+This test runs with the gntp.config module so that we can
+get away without having to hardcode our password in a test
+script. Please fill out your ~/.gntp config before running
+"""
+
+from gntp.test import GNTPTestCase
+import gntp.errors as errors
+
+
+class TestErrors(GNTPTestCase):
+ def test_connection_error(self):
+ #self.growl.hostname = '0.0.0.0'
+ # Port 9 would be the discard protocol. We just want a "null port"
+ # for testing
+ # http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
+ self.growl.port = 9
+ self.assertRaises(errors.NetworkError, self._notify, description='Connection Error')
View
2  gntp/test/test_hash.py
@@ -8,7 +8,7 @@
"""
import os
from gntp.test import GNTPTestCase
-from gntp import UnsupportedError
+from gntp.errors import UnsupportedError
class TestHash(GNTPTestCase):
View
2  gntp/test/test_resources.py
@@ -6,7 +6,7 @@
import gntp
ICON_FILE = os.path.join(os.path.dirname(__file__), "growl-icon.png")
-ICON_DATA = open(ICON_FILE).read()
+ICON_DATA = open(ICON_FILE, 'rb').read()
FILE_DATA = open(__file__).read()
View
1  gntp/version.py
@@ -0,0 +1 @@
+__version__ = '0.9'
View
4 setup.py
@@ -5,7 +5,7 @@
except ImportError:
from distutils.core import setup
-import gntp
+from gntp.version import __version__
setup(
name='gntp',
@@ -13,7 +13,7 @@
long_description=open('README.rst').read(),
author='Paul Traylor',
url='http://github.com/kfdm/gntp/',
- version=gntp.__version__,
+ version=__version__,
packages=['gntp'],
# http://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
View
2  tox.ini
@@ -1,6 +1,6 @@
# content of: tox.ini , put in same dir as setup.py
[tox]
-envlist = py25,py26,py27
+envlist = py25,py26,py27,pypy
[testenv]
deps=nose # install pytest in the venvs
commands=nosetests # or 'nosetests' or ...

No commit comments for this range

Something went wrong with that request. Please try again.