summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-02-14 04:13:58 -0500
committerChris McDonough <chrism@plope.com>2012-02-14 04:13:58 -0500
commit9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9 (patch)
treed5df37904835cfaac437acaf8d886fdb7c2072ce
parentc08f7656c1427cf5ebacf18356a5f55bd20ecfbd (diff)
parentaf24f7d5f69a74f9887ca6df622ef67c69075988 (diff)
downloadpyramid-9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9.tar.gz
pyramid-9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9.tar.bz2
pyramid-9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9.zip
Merge branch 'wwitzel3-ww/415'
-rw-r--r--CHANGES.txt38
-rw-r--r--docs/api/interfaces.rst3
-rw-r--r--docs/conf.py2
-rw-r--r--docs/narr/project.rst2
-rw-r--r--docs/narr/urldispatch.rst4
-rw-r--r--docs/whatsnew-1.3.rst15
-rw-r--r--pyramid/config/__init__.py25
-rw-r--r--pyramid/config/util.py15
-rw-r--r--pyramid/config/views.py40
-rw-r--r--pyramid/path.py2
-rw-r--r--pyramid/tests/test_config/test_init.py22
-rw-r--r--pyramid/tests/test_config/test_util.py16
-rw-r--r--pyramid/tests/test_config/test_views.py125
-rw-r--r--setup.py4
-rw-r--r--tox.ini3
15 files changed, 288 insertions, 28 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 9edcbdbe8..411681d81 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -4,9 +4,40 @@ Next release
Features
--------
+- The ``scan`` method of a ``Configurator`` can be passed an ``ignore``
+ argument, which can be a string, a callable, or a list consisting of
+ strings and/or callables. This feature allows submodules, subpackages, and
+ global objects from being scanned. See
+ http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for
+ more information about how to use the ``ignore`` argument to ``scan``.
+
+- Better error messages when a view callable returns a value that cannot be
+ converted to a response (for example, when a view callable returns a
+ dictionary without a renderer defined, or doesn't return any value at all).
+ The error message now contains information about the view callable itself
+ as well as the result of calling it.
+
+Dependencies
+------------
+
+- Depend on ``venusian`` >= 1.0a3 to provide scan ``ignore`` support.
+
+1.3a7 (2012-02-07)
+==================
+
+Features
+--------
+
- More informative error message when a ``config.include`` cannot find an
``includeme``. See https://github.com/Pylons/pyramid/pull/392.
+- Internal: catch unhashable discriminators early (raise an error instead of
+ allowing them to find their way into resolveConflicts).
+
+- The `match_param` view predicate now accepts a string or a tuple.
+ This replaces the broken behavior of accepting a dict. See
+ https://github.com/Pylons/pyramid/issues/425 for more information.
+
Bug Fixes
---------
@@ -18,6 +49,13 @@ Bug Fixes
- The ``prequest`` script would fail when used against URLs which did not
return HTML or text. See https://github.com/Pylons/pyramid/issues/381
+Backwards Incompatibilities
+---------------------------
+
+- The `match_param` view predicate no longer accepts a dict. This will
+ have no negative affect because the implementation was broken for
+ dict-based arguments.
+
Documentation
-------------
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index 5b190b53b..11cd8cf7e 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -76,3 +76,6 @@ Other Interfaces
.. autointerface:: IActionInfo
:members:
+
+ .. autointerface:: IAssetDescriptor
+ :members:
diff --git a/docs/conf.py b/docs/conf.py
index 3496bd38c..0c56f56e7 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -80,7 +80,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year
# other places throughout the built documents.
#
# The short X.Y version.
-version = '1.3a6'
+version = '1.3a7'
# The full version, including alpha/beta/rc tags.
release = version
diff --git a/docs/narr/project.rst b/docs/narr/project.rst
index ea0045ca7..d69f0cf13 100644
--- a/docs/narr/project.rst
+++ b/docs/narr/project.rst
@@ -98,7 +98,7 @@ Or on Windows:
.. code-block:: text
- $ Scripts\pcreate alchemy MyProject
+ $ Scripts\pcreate -s alchemy MyProject
Here's sample output from a run of ``pcreate`` on UNIX for a project we name
``MyProject``:
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index 6d9dfdd92..a7bf74786 100644
--- a/docs/narr/urldispatch.rst
+++ b/docs/narr/urldispatch.rst
@@ -340,8 +340,8 @@ The above pattern will match these URLs, generating the following matchdicts:
.. code-block:: text
- foo/1/2/ -> {'baz':u'1', 'bar':u'2', 'fizzle':()}
- foo/abc/def/a/b/c -> {'baz':u'abc', 'bar':u'def', 'fizzle': u'a/b/c')}
+ foo/1/2/ -> {'baz':u'1', 'bar':u'2', 'fizzle':u''}
+ foo/abc/def/a/b/c -> {'baz':u'abc', 'bar':u'def', 'fizzle': u'a/b/c'}
This occurs because the default regular expression for a marker is ``[^/]+``
which will match everything up to the first ``/``, while ``{fizzle:.*}`` will
diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst
index ed7024f62..b6cfde039 100644
--- a/docs/whatsnew-1.3.rst
+++ b/docs/whatsnew-1.3.rst
@@ -15,7 +15,9 @@ The major feature additions in Pyramid 1.3 follow.
Python 3 Compatibility
~~~~~~~~~~~~~~~~~~~~~~
-Pyramid is now Python 3 compatible. Python 3.2 or better is required.
+In addition to running on Python 2 (version 2.6 or 2.7 required), Pyramid is
+now Python 3 compatible. For Python 3 compatibility, Python 3.2 or better
+is required.
.. warning::
@@ -250,6 +252,13 @@ Minor Feature Additions
http://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling)
when one is provided by the web server.
+- The :meth:`pyramid.config.Configurator.scan` method can be passed an
+ ``ignore`` argument, which can be a string, a callable, or a list
+ consisting of strings and/or callables. This feature allows submodules,
+ subpackages, and global objects from being scanned. See
+ http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for
+ more information about how to use the ``ignore`` argument to ``scan``.
+
Backwards Incompatibilities
---------------------------
@@ -315,6 +324,10 @@ Backwards Incompatibilities
(indeterminate value based on Python 3 vs. Python 2). This has to be done
to normalize matching on Python 2 and Python 3.
+- The ``match_param`` view predicate no longer accepts a dict. This will have
+ no negative affect because the implementation was broken for dict-based
+ arguments.
+
Documentation Enhancements
--------------------------
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index 57ab7e13a..1656b5410 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -533,6 +533,10 @@ class Configurator(
``extra`` provides a facility for inserting extra keys and values
into an action dictionary.
"""
+ # catch nonhashable discriminators here; most unit tests use
+ # autocommit=False, which won't catch unhashable discriminators
+ assert hash(discriminator)
+
if kw is None:
kw = {}
@@ -847,7 +851,8 @@ class Configurator(
return self.manager.pop()
# this is *not* an action method (uses caller_package)
- def scan(self, package=None, categories=None, onerror=None, **kw):
+ def scan(self, package=None, categories=None, onerror=None, ignore=None,
+ **kw):
"""Scan a Python package and any of its subpackages for objects
marked with :term:`configuration decoration` such as
:class:`pyramid.view.view_config`. Any decorated object found will
@@ -875,6 +880,20 @@ class Configurator(
:term:`Venusian` documentation for more information about ``onerror``
callbacks.
+ The ``ignore`` argument, if provided, should be a Venusian ``ignore``
+ value. Providing an ``ignore`` argument allows the scan to ignore
+ particular modules, packages, or global objects during a scan.
+ ``ignore`` can be a string or a callable, or a list containing
+ strings or callables. The simplest usage of ``ignore`` is to provide
+ a module or package by providing a full path to its dotted name. For
+ example: ``config.scan(ignore='my.module.subpackage')`` would ignore
+ the ``my.module.subpackage`` package during a scan, which would
+ prevent the subpackage and any of its submodules from being imported
+ and scanned. See the :term:`Venusian` documentation for more
+ information about the ``ignore`` argument.
+
+ .. note:: the ``ignore`` argument is new in Pyramid 1.3.
+
To perform a ``scan``, Pyramid creates a Venusian ``Scanner`` object.
The ``kw`` argument represents a set of keyword arguments to pass to
the Venusian ``Scanner`` object's constructor. See the
@@ -896,7 +915,9 @@ class Configurator(
ctorkw.update(kw)
scanner = self.venusian.Scanner(**ctorkw)
- scanner.scan(package, categories=categories, onerror=onerror)
+
+ scanner.scan(package, categories=categories, onerror=onerror,
+ ignore=ignore)
def make_wsgi_app(self):
""" Commits any pending configuration statements, sends a
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index 5f0dd98ac..4c7ecd359 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -6,7 +6,6 @@ from zope.interface import implementer
from pyramid.interfaces import IActionInfo
from pyramid.compat import (
- string_types,
bytes_,
is_nonstr_iter,
)
@@ -221,19 +220,21 @@ def make_predicates(xhr=None, request_method=None, path_info=None,
h.update(bytes_('request_type:%r' % hash(request_type)))
if match_param is not None:
- if isinstance(match_param, string_types):
- match_param, match_param_val = match_param.split('=', 1)
- match_param = {match_param: match_param_val}
- text = "match_param %s" % match_param
+ if not is_nonstr_iter(match_param):
+ match_param = (match_param,)
+ match_param = sorted(match_param)
+ text = "match_param %s" % repr(match_param)
+ reqs = [p.split('=', 1) for p in match_param]
def match_param_predicate(context, request):
- for k, v in match_param.items():
+ for k, v in reqs:
if request.matchdict.get(k) != v:
return False
return True
match_param_predicate.__text__ = text
weights.append(1 << 9)
predicates.append(match_param_predicate)
- h.update(bytes_('match_param:%r' % match_param))
+ for p in match_param:
+ h.update(bytes_('match_param:%r' % p))
if custom:
for num, predicate in enumerate(custom):
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 0359c46f7..1988b532b 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -55,6 +55,7 @@ from pyramid.security import NO_PERMISSION_REQUIRED
from pyramid.static import static_view
from pyramid.threadlocal import get_current_registry
from pyramid.view import render_view_to_response
+from pyramid.util import object_description
from pyramid.config.util import (
DEFAULT_PHASH,
@@ -67,6 +68,12 @@ from pyramid.config.util import (
urljoin = urlparse.urljoin
url_parse = urlparse.urlparse
+def view_description(view):
+ try:
+ return view.__text__
+ except AttributeError:
+ return object_description(view)
+
def wraps_view(wrapper):
def inner(self, view):
wrapper_view = wrapper(self, view)
@@ -99,7 +106,7 @@ def preserve_view_attrs(view, wrapper):
# "wrapped view"
for attr in ('__permitted__', '__call_permissive__', '__permission__',
'__predicated__', '__predicates__', '__accept__',
- '__order__'):
+ '__order__', '__text__'):
try:
setattr(wrapper, attr, getattr(view, attr))
except AttributeError:
@@ -343,9 +350,19 @@ class ViewDeriver(object):
result = view(context, request)
response = registry.queryAdapterOrSelf(result, IResponse)
if response is None:
- raise ValueError(
- 'Could not convert view return value "%s" into a '
- 'response object' % (result,))
+ if result is None:
+ append = (' You may have forgotten to return a value from '
+ 'the view callable.')
+ elif isinstance(result, dict):
+ append = (' You may have forgotten to define a renderer in '
+ 'the view configuration.')
+ else:
+ append = ''
+ msg = ('Could not convert return value of the view callable %s '
+ 'into a response object. '
+ 'The value returned was %r.' + append)
+
+ raise ValueError(msg % (view_description(view), result))
return response
return viewresult_to_response
@@ -376,6 +393,8 @@ class DefaultViewMapper(object):
mapped_view = self.map_class_requestonly(view)
else:
mapped_view = self.map_class_native(view)
+ mapped_view.__text__ = 'method %s of %s' % (
+ self.attr or '__call__', object_description(view))
return mapped_view
def map_nonclass(self, view):
@@ -388,6 +407,11 @@ class DefaultViewMapper(object):
mapped_view = self.map_nonclass_requestonly(view)
elif self.attr:
mapped_view = self.map_nonclass_attr(view)
+ if self.attr is not None:
+ mapped_view.__text__ = 'attr %s of %s' % (
+ self.attr, object_description(view))
+ else:
+ mapped_view.__text__ = object_description(view)
return mapped_view
def map_class_requestonly(self, view):
@@ -843,18 +867,18 @@ class ViewsConfiguratorMixin(object):
.. note:: This feature is new as of :app:`Pyramid` 1.2.
- This param may be either a single string of the format "key=value"
- or a dict of key/value pairs.
+ This value can be a string of the format "key=value" or a tuple
+ containing one or more of these strings.
A view declaration with this argument ensures that the view will
only be called when the :term:`request` has key/value pairs in its
:term:`matchdict` that equal those supplied in the predicate.
e.g. ``match_param="action=edit" would require the ``action``
- parameter in the :term:`matchdict` match the right hande side of
+ parameter in the :term:`matchdict` match the right hand side of
the expression (``edit``) for the view to "match" the current
request.
- If the ``match_param`` is a dict, every key/value pair must match
+ If the ``match_param`` is a tuple, every key/value pair must match
for the predicate to pass.
containment
diff --git a/pyramid/path.py b/pyramid/path.py
index 8a8898174..d1c3b6d31 100644
--- a/pyramid/path.py
+++ b/pyramid/path.py
@@ -165,7 +165,7 @@ class AssetResolver(Resolver):
"""
Resolve the asset spec named as ``spec`` to an object that has the
attributes and methods described in
- `pyramid.interfaces.IAssetDescriptor`.
+ :class:`pyramid.interfaces.IAssetDescriptor`.
If ``spec`` is an absolute filename
(e.g. ``/path/to/myproject/templates/foo.pt``) or an absolute asset
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index a866bed55..d237b3fe8 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -929,6 +929,28 @@ pyramid.tests.test_config.dummy_include2""",
result = render_view_to_response(ctx, req, 'pod_notinit')
self.assertEqual(result, None)
+ def test_scan_integration_with_ignore(self):
+ from zope.interface import alsoProvides
+ from pyramid.interfaces import IRequest
+ from pyramid.view import render_view_to_response
+ import pyramid.tests.test_config.pkgs.scannable as package
+ config = self._makeOne(autocommit=True)
+ config.scan(package,
+ ignore='pyramid.tests.test_config.pkgs.scannable.another')
+
+ ctx = DummyContext()
+ req = DummyRequest()
+ alsoProvides(req, IRequest)
+ req.registry = config.registry
+
+ req.method = 'GET'
+ result = render_view_to_response(ctx, req, '')
+ self.assertEqual(result, 'grokked')
+
+ # ignored
+ v = render_view_to_response(ctx, req, 'another_stacked_class2')
+ self.assertEqual(v, None)
+
def test_scan_integration_dottedname_package(self):
from zope.interface import alsoProvides
from pyramid.interfaces import IRequest
diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py
index ebf308929..1ad1fb3c1 100644
--- a/pyramid/tests/test_config/test_util.py
+++ b/pyramid/tests/test_config/test_util.py
@@ -281,7 +281,7 @@ class Test__make_predicates(unittest.TestCase):
self.assertEqual(predicates[5].__text__, 'accept = accept')
self.assertEqual(predicates[6].__text__, 'containment = containment')
self.assertEqual(predicates[7].__text__, 'request_type = request_type')
- self.assertEqual(predicates[8].__text__, "match_param {'foo': 'bar'}")
+ self.assertEqual(predicates[8].__text__, "match_param ['foo=bar']")
self.assertEqual(predicates[9].__text__, 'custom predicate')
self.assertEqual(predicates[10].__text__, 'classmethod predicate')
self.assertEqual(predicates[11].__text__, '<unknown custom predicate>')
@@ -299,13 +299,13 @@ class Test__make_predicates(unittest.TestCase):
self.assertFalse(predicates[0](Dummy(), request))
def test_match_param_from_dict(self):
- _, predicates, _ = self._callFUT(match_param={'foo':'bar','baz':'bum'})
+ _, predicates, _ = self._callFUT(match_param=('foo=bar','baz=bum'))
request = DummyRequest()
request.matchdict = {'foo':'bar', 'baz':'bum'}
self.assertTrue(predicates[0](Dummy(), request))
def test_match_param_from_dict_fails(self):
- _, predicates, _ = self._callFUT(match_param={'foo':'bar','baz':'bum'})
+ _, predicates, _ = self._callFUT(match_param=('foo=bar','baz=bum'))
request = DummyRequest()
request.matchdict = {'foo':'bar', 'baz':'foo'}
self.assertFalse(predicates[0](Dummy(), request))
@@ -328,6 +328,16 @@ class Test__make_predicates(unittest.TestCase):
hash2, _, __= self._callFUT(request_method='GET')
self.assertEqual(hash1, hash2)
+ def test_match_param_hashable(self):
+ # https://github.com/Pylons/pyramid/issues/425
+ import pyramid.testing
+ def view(request): pass
+ config = pyramid.testing.setUp(autocommit=False)
+ config.add_route('foo', '/foo/{a}/{b}')
+ config.add_view(view, route_name='foo', match_param='a=bar')
+ config.add_view(view, route_name='foo', match_param=('a=bar', 'b=baz'))
+ config.commit()
+
class TestActionInfo(unittest.TestCase):
def _getTargetClass(self):
from pyramid.config.util import ActionInfo
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index 4af29325a..eb18d5c84 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -2282,6 +2282,113 @@ class TestViewDeriver(unittest.TestCase):
self.config.registry.registerUtility(policy, IAuthenticationPolicy)
self.config.registry.registerUtility(policy, IAuthorizationPolicy)
+ def test_function_returns_non_adaptable(self):
+ def view(request):
+ return None
+ deriver = self._makeOne()
+ result = deriver(view)
+ self.assertFalse(result is view)
+ try:
+ result(None, None)
+ except ValueError as e:
+ self.assertEqual(
+ e.args[0],
+ 'Could not convert return value of the view callable function '
+ 'pyramid.tests.test_config.test_views.view into a response '
+ 'object. The value returned was None. You may have forgotten '
+ 'to return a value from the view callable.'
+ )
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_function_returns_non_adaptable_dict(self):
+ def view(request):
+ return {'a':1}
+ deriver = self._makeOne()
+ result = deriver(view)
+ self.assertFalse(result is view)
+ try:
+ result(None, None)
+ except ValueError as e:
+ self.assertEqual(
+ e.args[0],
+ "Could not convert return value of the view callable function "
+ "pyramid.tests.test_config.test_views.view into a response "
+ "object. The value returned was {'a': 1}. You may have "
+ "forgotten to define a renderer in the view configuration."
+ )
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_instance_returns_non_adaptable(self):
+ class AView(object):
+ def __call__(self, request):
+ return None
+ view = AView()
+ deriver = self._makeOne()
+ result = deriver(view)
+ self.assertFalse(result is view)
+ try:
+ result(None, None)
+ except ValueError as e:
+ msg = e.args[0]
+ self.assertTrue(msg.startswith(
+ 'Could not convert return value of the view callable object '
+ '<pyramid.tests.test_config.test_views.AView object at'))
+ self.assertTrue(msg.endswith(
+ '> into a response object. The value returned was None. You '
+ 'may have forgotten to return a value from the view callable.'))
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_requestonly_default_method_returns_non_adaptable(self):
+ request = DummyRequest()
+ class AView(object):
+ def __init__(self, request):
+ pass
+ def __call__(self):
+ return None
+ deriver = self._makeOne()
+ result = deriver(AView)
+ self.assertFalse(result is AView)
+ try:
+ result(None, request)
+ except ValueError as e:
+ self.assertEqual(
+ e.args[0],
+ 'Could not convert return value of the view callable '
+ 'method __call__ of '
+ 'class pyramid.tests.test_config.test_views.AView into a '
+ 'response object. The value returned was None. You may have '
+ 'forgotten to return a value from the view callable.'
+ )
+ else: # pragma: no cover
+ raise AssertionError
+
+ def test_requestonly_nondefault_method_returns_non_adaptable(self):
+ request = DummyRequest()
+ class AView(object):
+ def __init__(self, request):
+ pass
+ def theviewmethod(self):
+ return None
+ deriver = self._makeOne(attr='theviewmethod')
+ result = deriver(AView)
+ self.assertFalse(result is AView)
+ try:
+ result(None, request)
+ except ValueError as e:
+ self.assertEqual(
+ e.args[0],
+ 'Could not convert return value of the view callable '
+ 'method theviewmethod of '
+ 'class pyramid.tests.test_config.test_views.AView into a '
+ 'response object. The value returned was None. You may have '
+ 'forgotten to return a value from the view callable.'
+ )
+ else: # pragma: no cover
+ raise AssertionError
+
def test_requestonly_function(self):
response = DummyResponse()
def view(request):
@@ -3689,6 +3796,24 @@ class TestStaticURLInfo(unittest.TestCase):
view_attr='attr')
self.assertEqual(config.view_kw['attr'], 'attr')
+class Test_view_description(unittest.TestCase):
+ def _callFUT(self, view):
+ from pyramid.config.views import view_description
+ return view_description(view)
+
+ def test_with_text(self):
+ def view(): pass
+ view.__text__ = 'some text'
+ result = self._callFUT(view)
+ self.assertEqual(result, 'some text')
+
+ def test_without_text(self):
+ def view(): pass
+ result = self._callFUT(view)
+ self.assertEqual(result,
+ 'function pyramid.tests.test_config.test_views.view')
+
+
class DummyRegistry:
pass
diff --git a/setup.py b/setup.py
index 64c0ad419..a6cfa1480 100644
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,7 @@ install_requires=[
'repoze.lru >= 0.4', # py3 compat
'zope.interface >= 3.8.0', # has zope.interface.registry
'zope.deprecation >= 3.5.0', # py3 compat
- 'venusian >= 1.0a1', # ``onerror``
+ 'venusian >= 1.0a3', # ``ignore``
'translationstring >= 0.4', # py3 compat
'PasteDeploy >= 1.5.0', # py3 compat
]
@@ -56,7 +56,7 @@ if not PY3:
])
setup(name='pyramid',
- version='1.3a6',
+ version='1.3a7',
description=('The Pyramid web application development framework, a '
'Pylons project'),
long_description=README + '\n\n' + CHANGES,
diff --git a/tox.ini b/tox.ini
index 79728bc18..1e7223886 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,6 +11,7 @@ deps =
repoze.sphinx.autointerface
WebTest
virtualenv
+ venusian>=1.0a3
[testenv:py32]
commands =
@@ -18,6 +19,7 @@ commands =
deps =
WebTest
virtualenv
+ venusian>=1.0a3
[testenv:cover]
basepython =
@@ -30,6 +32,7 @@ deps =
WebTest
repoze.sphinx.autointerface
virtualenv
+ venusian>=1.0a3
nose
coverage==3.4
nosexcover