summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2014-04-08 09:08:02 -0400
committerChris McDonough <chrism@plope.com>2014-04-08 09:08:02 -0400
commitecb376a132034bd4ccfe1daa69d4828699e9028d (patch)
tree912b74c4dc61370bb46487f5c52fc705c70e150e
parente012af904feb15f20a134f22eaeeb61bd9d56cc8 (diff)
parentbcf18220be9c21ff4b1af50f45b90aadfa7820f5 (diff)
downloadpyramid-ecb376a132034bd4ccfe1daa69d4828699e9028d.tar.gz
pyramid-ecb376a132034bd4ccfe1daa69d4828699e9028d.tar.bz2
pyramid-ecb376a132034bd4ccfe1daa69d4828699e9028d.zip
fix merge conflict while merging master to 1.5 branch
-rw-r--r--CHANGES.txt6
-rw-r--r--docs/quick_tour.rst44
-rw-r--r--docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt4
-rw-r--r--docs/quick_tutorial/scaffolds.rst8
-rw-r--r--docs/tutorials/wiki2/authorization.rst15
-rw-r--r--pyramid/config/views.py14
-rw-r--r--pyramid/security.py12
-rw-r--r--pyramid/session.py64
-rw-r--r--pyramid/tests/test_integration.py42
-rw-r--r--pyramid/tests/test_session.py25
-rw-r--r--setup.py1
-rw-r--r--tox.ini2
12 files changed, 168 insertions, 69 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 6372c904d..fdf2ac644 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -7,6 +7,12 @@ Unreleased
- ``UnencryptedCookieSessionFactoryConfig`` failed if the secret contained
higher order characters. See https://github.com/Pylons/pyramid/issues/1246
+- Fixed a bug in ``UnencryptedCookieSessionFactoryConfig`` and
+ ``SignedCookieSessionFactory`` where ``timeout=None`` would cause a new
+ session to always be created. Also in ``SignedCookieSessionFactory`` a
+ ``reissue_time=None`` would cause an exception when modifying the session.
+ See https://github.com/Pylons/pyramid/issues/1247
+
1.5b1 (2014-02-08)
==================
diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst
index 2d4e679f8..4ab39bb11 100644
--- a/docs/quick_tour.rst
+++ b/docs/quick_tour.rst
@@ -73,14 +73,14 @@ This simple example is easy to run. Save this as ``app.py`` and run it:
Next, open `http://localhost:6543/ <http://localhost:6543/>`_ in a
browser and you will see the ``Hello World!`` message.
-New to Python web programming? If so, some lines in module merit
+New to Python web programming? If so, some lines in the module merit
explanation:
#. *Line 10*. The ``if __name__ == '__main__':`` is Python's way of
saying "Start here when running from the command line".
#. *Lines 11-13*. Use Pyramid's :term:`configurator` to connect
- :term:`view` code to particular URL :term:`route`.
+ :term:`view` code to a particular URL :term:`route`.
#. *Lines 6-7*. Implement the view code that generates the
:term:`response`.
@@ -148,15 +148,15 @@ So far our examples place everything in one file:
- the WSGI application launcher
Let's move the views out to their own ``views.py`` module and change
-the ``app.py`` to scan that module, looking for decorators that setup
+the ``app.py`` to scan that module, looking for decorators that set up
the views. First, our revised ``app.py``:
.. literalinclude:: quick_tour/views/app.py
:linenos:
We added some more routes, but we also removed the view code.
-Our views, and their registrations (via decorators) are now in a module
-``views.py`` which is scanned via ``config.scan('views')``.
+Our views and their registrations (via decorators) are now in a module
+``views.py``, which is scanned via ``config.scan('views')``.
We now have a ``views.py`` module that is focused on handling requests
and responses:
@@ -167,7 +167,7 @@ and responses:
We have 4 views, each leading to the other. If you start at
``http://localhost:6543/``, you get a response with a link to the next
view. The ``hello_view`` (available at the URL ``/howdy``) has a link
-to the ``redirect_view``, which shows issuing a redirect to the final
+to the ``redirect_view``, which issues a redirect to the final
view.
Earlier we saw ``config.add_view`` as one way to configure a view. This
@@ -267,7 +267,7 @@ Now lets change our views.py file:
Ahh, that looks better. We have a view that is focused on Python code.
Our ``@view_config`` decorator specifies a :term:`renderer` that points
-our template file. Our view then simply returns data which is then
+to our template file. Our view then simply returns data which is then
supplied to our template:
.. literalinclude:: quick_tour/templating/hello_world.pt
@@ -303,7 +303,7 @@ our configuration:
config.include('pyramid_jinja2')
-The only change in our view...point the renderer at the ``.jinja2`` file:
+The only change in our view is to point the renderer at the ``.jinja2`` file:
.. literalinclude:: quick_tour/jinja2/views.py
:start-after: Start View 1
@@ -356,8 +356,8 @@ template:
This link presumes that our CSS is at a URL starting with ``/static/``.
What if the site is later moved under ``/somesite/static/``? Or perhaps
-web developer changes the arrangement on disk? Pyramid gives a helper
-that provides flexibility on URL generation:
+a web developer changes the arrangement on disk? Pyramid provides a helper
+to allow flexibility on URL generation:
.. literalinclude:: quick_tour/static_assets/hello_world.pt
:language: html
@@ -454,7 +454,7 @@ have much more to offer:
Quick Project Startup with Scaffolds
====================================
-So far we have done all of our *Quick Glance* as a single Python file.
+So far we have done all of our *Quick Tour* as a single Python file.
No Python packages, no structure. Most Pyramid projects, though,
aren't developed this way.
@@ -479,7 +479,7 @@ let's use that scaffold to make our project:
$ pcreate --scaffold pyramid_jinja2_starter hello_world
-We next use the normal Python development to setup our package for
+We next use the normal Python command to set up our package for
development:
.. code-block:: bash
@@ -534,7 +534,7 @@ take a look at this configuration file.
Configuration with ``.ini`` Files
=================================
-Earlier in *Quick Glance* we first met Pyramid's configuration system.
+Earlier in *Quick Tour* we first met Pyramid's configuration system.
At that point we did all configuration in Python code. For example,
the port number chosen for our HTTP server was right there in Python
code. Our scaffold has moved this decision, and more, into the
@@ -556,8 +556,8 @@ into sections:
We have a few decisions made for us in this configuration:
-#. *Choice of web server*. The ``use = egg:pyramid#wsgiref`` tell
- ``pserve`` to the ``wsgiref`` server that is wrapped in the Pyramid
+#. *Choice of web server*. The ``use = egg:pyramid#wsgiref`` tells
+ ``pserve`` to use the ``wsgiref`` server that is wrapped in the Pyramid
package.
#. *Port number*. ``port = 6543`` tells ``wsgiref`` to listen on port
@@ -574,7 +574,7 @@ We have a few decisions made for us in this configuration:
Additionally, the ``development.ini`` generated by this scaffold wired
up Python's standard logging. We'll now see in the console, for example,
-a log on every request that comes in, as well traceback information.
+a log on every request that comes in, as well as traceback information.
.. seealso:: See also:
:ref:`Quick Tutorial Application Configuration <qtut_ini>`,
@@ -585,7 +585,7 @@ a log on every request that comes in, as well traceback information.
Easier Development with ``debugtoolbar``
========================================
-As we introduce the basics we also want to show how to be productive in
+As we introduce the basics, we also want to show how to be productive in
development and debugging. For example, we just discussed template
reloading and earlier we showed ``--reload`` for application reloading.
@@ -700,12 +700,12 @@ we might need to detect situations when other people use the site. We
need *logging*.
Fortunately Pyramid uses the normal Python approach to logging. The
-scaffold generated, in your ``development.ini``, a number of lines that
+scaffold generated in your ``development.ini`` a number of lines that
configure the logging for you to some reasonable defaults. You then see
-messages sent by Pyramid (for example, when a new request comes in.)
+messages sent by Pyramid (for example, when a new request comes in).
Maybe you would like to log messages in your code? In your Python
-module, import and setup the logging:
+module, import and set up the logging:
.. literalinclude:: quick_tour/package/hello_world/views.py
:start-after: Start Logging 1
@@ -726,7 +726,7 @@ controls that? These sections in the configuration file:
:start-after: Start Sphinx Include
:end-before: End Sphinx Include
-Our application, a package named ``hello_world``, is setup as a logger
+Our application, a package named ``hello_world``, is set up as a logger
and configured to log messages at a ``DEBUG`` or higher level. When you
visit ``http://localhost:6543`` your console will now show::
@@ -789,7 +789,7 @@ Databases
=========
Web applications mean data. Data means databases. Frequently SQL
-databases. SQL Databases frequently mean an "ORM"
+databases. SQL databases frequently mean an "ORM"
(object-relational mapper.) In Python, ORM usually leads to the
mega-quality *SQLAlchemy*, a Python package that greatly eases working
with databases.
diff --git a/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt
index d1fea0d7f..3292dfd90 100644
--- a/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt
+++ b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt
@@ -4,10 +4,10 @@
<title>WikiPage: Add/Edit</title>
<tal:block tal:repeat="reqt view.reqts['css']">
<link rel="stylesheet" type="text/css"
- href="${request.static_url('deform:static/' + reqt)}"/>
+ href="${request.static_url(reqt)}"/>
</tal:block>
<tal:block tal:repeat="reqt view.reqts['js']">
- <script src="${request.static_url('deform:static/' + reqt)}"
+ <script src="${request.static_url(reqt)}"
type="text/javascript"></script>
</tal:block>
</head>
diff --git a/docs/quick_tutorial/scaffolds.rst b/docs/quick_tutorial/scaffolds.rst
index 8ca2d27df..4f2694100 100644
--- a/docs/quick_tutorial/scaffolds.rst
+++ b/docs/quick_tutorial/scaffolds.rst
@@ -63,11 +63,11 @@ Steps
On startup, ``pserve`` logs some output:
- .. code-block:: bash
+ .. code-block:: bash
- Starting subprocess with file monitor
- Starting server in PID 72213.
- Starting HTTP server on http://0.0.0.0:6543
+ Starting subprocess with file monitor
+ Starting server in PID 72213.
+ Starting HTTP server on http://0.0.0.0:6543
#. Open http://localhost:6543/ in your browser.
diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst
index 1e5d0dcbf..2e35574fd 100644
--- a/docs/tutorials/wiki2/authorization.rst
+++ b/docs/tutorials/wiki2/authorization.rst
@@ -207,6 +207,21 @@ routes:
:linenos:
:language: python
+.. note:: The preceding lines must be added *before* the following
+ ``view_page`` route definition:
+
+ .. literalinclude:: src/authorization/tutorial/__init__.py
+ :lines: 32
+ :linenos:
+ :language: python
+
+ This is because ``view_page``'s route definition uses a catch-all
+ "replacement marker" ``/{pagename}`` (see :ref:`route_pattern_syntax`)
+ which will catch any route that was not already caught by any
+ route listed above it in ``__init__.py``. Hence, for ``login`` and
+ ``logout`` views to have the opportunity of being matched
+ (or "caught"), they must be above ``/{pagename}``.
+
Add Login and Logout Views
~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 1ef4efdb3..2f6c758ab 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -879,13 +879,13 @@ class ViewsConfiguratorMixin(object):
request_method
- This value can be one of the strings ``GET``, ``POST``, ``PUT``,
- ``DELETE``, or ``HEAD`` representing an HTTP ``REQUEST_METHOD``, 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 request's ``method`` attribute (aka the
- ``REQUEST_METHOD`` of the WSGI environment) string matches a
- supplied value. Note that use of ``GET`` also implies that the
+ This value can be either a strings (such as ``GET``, ``POST``,
+ ``PUT``, ``DELETE``, or ``HEAD``) representing an HTTP
+ ``REQUEST_METHOD``, 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 ``method`` attribute of the
+ request (aka the ``REQUEST_METHOD`` of the WSGI environment) matches
+ a supplied value. Note that use of ``GET`` also implies that the
view will respond to ``HEAD`` as of Pyramid 1.4.
.. versionchanged:: 1.2
diff --git a/pyramid/security.py b/pyramid/security.py
index 848574233..c98d4e6cc 100644
--- a/pyramid/security.py
+++ b/pyramid/security.py
@@ -160,9 +160,6 @@ def forget(request):
If no :term:`authentication policy` is in use, this function will
always return an empty sequence.
-
- .. deprecated:: 1.5
- Use :meth:`pyramid.request.Request.get_logout_headers` instead.
"""
policy = _get_authentication_policy(request)
if policy is None:
@@ -343,10 +340,9 @@ class AuthenticationAPIMixin(object):
@property
def effective_principals(self):
""" Return the list of 'effective' :term:`principal` identifiers
- for the ``request``. This will include the userid of the
- currently authenticated user if a user is currently
- authenticated. If no :term:`authentication policy` is in effect,
- this will return an empty sequence.
+ for the ``request``. If no :term:`authentication policy` is in effect,
+ this will return a one-element list containing the
+ :data:`pyramid.security.Everyone` principal.
.. versionadded:: 1.5
"""
@@ -354,7 +350,7 @@ class AuthenticationAPIMixin(object):
if policy is None:
return [Everyone]
return policy.effective_principals(self)
-
+
class AuthorizationAPIMixin(object):
def has_permission(self, permission, context=None):
diff --git a/pyramid/session.py b/pyramid/session.py
index 4dc7bda74..a95c3f258 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -27,8 +27,9 @@ def manage_accessed(wrapped):
method is called."""
def accessed(session, *arg, **kw):
session.accessed = now = int(time.time())
- if now - session.renewed > session._reissue_time:
- session.changed()
+ if session._reissue_time is not None:
+ if now - session.renewed > session._reissue_time:
+ session.changed()
return wrapped(session, *arg, **kw)
accessed.__doc__ = wrapped.__doc__
return accessed
@@ -175,10 +176,10 @@ def BaseCookieSessionFactory(
Parameters:
``serializer``
- An object with two methods: ``loads`` and ``dumps``. The ``loads`` method
- should accept bytes and return a Python object. The ``dumps`` method
- should accept a Python object and return bytes. A ``ValueError`` should
- be raised for malformed inputs.
+ An object with two methods: ``loads`` and ``dumps``. The ``loads``
+ method should accept bytes and return a Python object. The ``dumps``
+ method should accept a Python object and return bytes. A ``ValueError``
+ should be raised for malformed inputs.
``cookie_name``
The name of the cookie used for sessioning. Default: ``'session'``.
@@ -202,14 +203,17 @@ def BaseCookieSessionFactory(
``timeout``
A number of seconds of inactivity before a session times out. If
- ``None`` then the cookie never expires. Default: 1200.
+ ``None`` then the cookie never expires. This lifetime only applies
+ to the *value* within the cookie. Meaning that if the cookie expires
+ due to a lower ``max_age``, then this setting has no effect.
+ Default: ``1200``.
``reissue_time``
The number of seconds that must pass before the cookie is automatically
reissued as the result of a request which accesses the session. The
duration is measured as the number of seconds since the last session
cookie was issued and 'now'. If this value is ``0``, a new cookie
- will be reissued on every request accesses the session. If ``None``
+ will be reissued on every request accessing the session. If ``None``
then the cookie's lifetime will never be extended.
A good rule of thumb: if you want auto-expired cookies based on
@@ -263,17 +267,24 @@ def BaseCookieSessionFactory(
if value is not None:
try:
- renewed, created, state = value
+ # since the value is not necessarily signed, we have
+ # to unpack it a little carefully
+ rval, cval, sval = value
+ renewed = float(rval)
+ created = float(cval)
+ state = sval
new = False
- if now - renewed > self._timeout:
- # expire the session because it was not renewed
- # before the timeout threshold
- state = {}
- except TypeError:
+ except (TypeError, ValueError):
# value failed to unpack properly or renewed was not
# a numeric type so we'll fail deserialization here
state = {}
+ if self._timeout is not None:
+ if now - renewed > self._timeout:
+ # expire the session because it was not renewed
+ # before the timeout threshold
+ state = {}
+
self.created = created
self.accessed = renewed
self.renewed = renewed
@@ -480,8 +491,8 @@ def UnencryptedCookieSessionFactoryConfig(
deprecated(
'UnencryptedCookieSessionFactoryConfig',
'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of '
- 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead. '
- 'Caveat: Cookies generated using SignedCookieSessionFactory are not '
+ 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead.'
+ ' Caveat: Cookies generated using SignedCookieSessionFactory are not '
'compatible with cookies generated using UnencryptedCookieSessionFactory, '
'so existing user session data will be destroyed if you switch to it.'
)
@@ -558,14 +569,17 @@ def SignedCookieSessionFactory(
``timeout``
A number of seconds of inactivity before a session times out. If
- ``None`` then the cookie never expires. Default: 1200.
+ ``None`` then the cookie never expires. This lifetime only applies
+ to the *value* within the cookie. Meaning that if the cookie expires
+ due to a lower ``max_age``, then this setting has no effect.
+ Default: ``1200``.
``reissue_time``
The number of seconds that must pass before the cookie is automatically
- reissued as the result of a request which accesses the session. The
+ reissued as the result of accessing the session. The
duration is measured as the number of seconds since the last session
cookie was issued and 'now'. If this value is ``0``, a new cookie
- will be reissued on every request accesses the session. If ``None``
+ will be reissued on every request accessing the session. If ``None``
then the cookie's lifetime will never be extended.
A good rule of thumb: if you want auto-expired cookies based on
@@ -582,11 +596,11 @@ def SignedCookieSessionFactory(
while rendering a view. Default: ``True``.
``serializer``
- An object with two methods: ``loads`` and ``dumps``. The ``loads`` method
- should accept bytes and return a Python object. The ``dumps`` method
- should accept a Python object and return bytes. A ``ValueError`` should
- be raised for malformed inputs. If a serializer is not passed, the
- :class:`pyramid.session.PickleSerializer` serializer will be used.
+ An object with two methods: ``loads`` and ``dumps``. The ``loads``
+ method should accept bytes and return a Python object. The ``dumps``
+ method should accept a Python object and return bytes. A ``ValueError``
+ should be raised for malformed inputs. If a serializer is not passed,
+ the :class:`pyramid.session.PickleSerializer` serializer will be used.
.. versionadded: 1.5a3
"""
@@ -595,7 +609,7 @@ def SignedCookieSessionFactory(
signed_serializer = SignedSerializer(
secret,
- salt,
+ salt,
hashalg,
serializer=serializer,
)
diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py
index 9d3a9e004..35648ed38 100644
--- a/pyramid/tests/test_integration.py
+++ b/pyramid/tests/test_integration.py
@@ -640,6 +640,48 @@ class RendererScanAppTest(IntegrationBase, unittest.TestCase):
res = testapp.get('/two', status=200)
self.assertTrue(b'Two!' in res.body)
+class UnicodeInURLTest(unittest.TestCase):
+ def _makeConfig(self):
+ from pyramid.config import Configurator
+ config = Configurator()
+ return config
+
+ def _makeTestApp(self, config):
+ from webtest import TestApp
+ app = config.make_wsgi_app()
+ return TestApp(app)
+
+ def test_unicode_in_url_404(self):
+ request_path = '/avalia%C3%A7%C3%A3o_participante'
+ request_path_unicode = b'/avalia\xc3\xa7\xc3\xa3o_participante'.decode('utf-8')
+
+ config = self._makeConfig()
+ testapp = self._makeTestApp(config)
+
+ res = testapp.get(request_path, status=404)
+
+ # Pyramid default 404 handler outputs:
+ # u'404 Not Found\n\nThe resource could not be found.\n\n\n'
+ # u'/avalia\xe7\xe3o_participante\n\n'
+ self.assertTrue(request_path_unicode in res.text)
+
+ def test_unicode_in_url_200(self):
+ request_path = '/avalia%C3%A7%C3%A3o_participante'
+ request_path_unicode = b'/avalia\xc3\xa7\xc3\xa3o_participante'.decode('utf-8')
+
+ def myview(request):
+ return 'XXX'
+
+ config = self._makeConfig()
+ config.add_route('myroute', request_path_unicode)
+ config.add_view(myview, route_name='myroute', renderer='json')
+ testapp = self._makeTestApp(config)
+
+ res = testapp.get(request_path, status=200)
+
+ self.assertEqual(res.text, '"XXX"')
+
+
class AcceptContentTypeTest(unittest.TestCase):
def setUp(self):
def hello_view(request):
diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py
index f1b1e2296..35c234e99 100644
--- a/pyramid/tests/test_session.py
+++ b/pyramid/tests/test_session.py
@@ -52,6 +52,15 @@ class SharedCookieSessionTests(object):
session = self._makeOne(request, timeout=1)
self.assertEqual(dict(session), {})
+ def test_timeout_never(self):
+ import time
+ request = testing.DummyRequest()
+ LONG_TIME = 31536000
+ cookieval = self._serialize((time.time() + LONG_TIME, 0, {'state': 1}))
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, timeout=None)
+ self.assertEqual(dict(session), {'state': 1})
+
def test_changed(self):
request = testing.DummyRequest()
session = self._makeOne(request)
@@ -279,6 +288,14 @@ class TestBaseCookieSession(SharedCookieSessionTests, unittest.TestCase):
self.assertEqual(session['state'], 1)
self.assertFalse(session._dirty)
+ def test_reissue_never(self):
+ request = testing.DummyRequest()
+ cookieval = self._serialize((0, 0, {'state': 1}))
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, reissue_time=None, timeout=None)
+ self.assertEqual(session['state'], 1)
+ self.assertFalse(session._dirty)
+
class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase):
def _makeOne(self, request, **kw):
from pyramid.session import SignedCookieSessionFactory
@@ -305,6 +322,14 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase):
self.assertEqual(session['state'], 1)
self.assertFalse(session._dirty)
+ def test_reissue_never(self):
+ request = testing.DummyRequest()
+ cookieval = self._serialize((0, 0, {'state': 1}))
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, reissue_time=None, timeout=None)
+ self.assertEqual(session['state'], 1)
+ self.assertFalse(session._dirty)
+
def test_custom_salt(self):
import time
request = testing.DummyRequest()
diff --git a/setup.py b/setup.py
index 3c17af20d..bb443330d 100644
--- a/setup.py
+++ b/setup.py
@@ -79,6 +79,7 @@ setup(name='pyramid',
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Framework :: Pyramid",
diff --git a/tox.ini b/tox.ini
index b50e56544..2bf213ca4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
envlist =
- py26,py27,py32,py33,pypy,cover
+ py26,py27,py32,py33,py34,pypy,cover
[testenv]
commands =