summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt39
-rw-r--r--CONTRIBUTORS.txt4
-rw-r--r--HACKING.txt4
-rw-r--r--RELEASING.txt4
-rw-r--r--TODO.txt4
-rw-r--r--appveyor.yml5
-rw-r--r--docs/narr/install.rst2
-rw-r--r--docs/narr/introduction.rst2
-rw-r--r--docs/narr/project.rst20
-rw-r--r--docs/quick_tour.rst4
-rw-r--r--docs/quick_tutorial/requirements.rst6
-rw-r--r--docs/tutorials/wiki/authorization.rst43
-rw-r--r--docs/tutorials/wiki/definingviews.rst1
-rw-r--r--docs/tutorials/wiki/installation.rst9
-rw-r--r--docs/tutorials/wiki/src/authorization/setup.py1
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/security.py17
-rw-r--r--docs/tutorials/wiki/src/authorization/tutorial/views.py4
-rw-r--r--docs/tutorials/wiki/src/tests/setup.py1
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/security.py17
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/tests.py11
-rw-r--r--docs/tutorials/wiki/src/tests/tutorial/views.py4
-rw-r--r--docs/tutorials/wiki2/installation.rst9
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py1
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py12
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py20
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py21
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py67
-rw-r--r--pyramid/asset.py2
-rw-r--r--pyramid/config/predicates.py303
-rw-r--r--pyramid/config/routes.py10
-rw-r--r--pyramid/config/security.py2
-rw-r--r--pyramid/config/settings.py171
-rw-r--r--pyramid/config/tweens.py2
-rw-r--r--pyramid/config/util.py15
-rw-r--r--pyramid/config/views.py6
-rw-r--r--pyramid/predicates.py300
-rw-r--r--pyramid/scripts/pserve.py485
-rw-r--r--pyramid/tests/test_config/test_settings.py39
-rw-r--r--pyramid/tests/test_config/test_util.py11
-rw-r--r--pyramid/tests/test_config/test_views.py4
-rw-r--r--pyramid/tests/test_httpexceptions.py2
-rw-r--r--pyramid/tests/test_predicates.py (renamed from pyramid/tests/test_config/test_predicates.py)22
-rw-r--r--pyramid/tests/test_scripts/test_pserve.py70
-rw-r--r--pyramid/tests/test_traversal.py12
-rw-r--r--pyramid/tests/test_urldispatch.py4
-rw-r--r--pyramid/traversal.py7
-rw-r--r--pyramid/url.py7
-rw-r--r--pyramid/urldispatch.py11
-rw-r--r--pyramid/util.py17
-rw-r--r--setup.py20
50 files changed, 807 insertions, 1047 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 59f77368f..9daddff48 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -35,9 +35,16 @@ Backward Incompatibilities
encoding via ``Accept-Encoding`` request headers.
See https://github.com/Pylons/pyramid/pull/2810
+- Settings are no longer accessible as attributes on the settings object
+ (e.g. ``request.registry.settings.foo``). This was deprecated in Pyramid 1.2.
+ See https://github.com/Pylons/pyramid/pull/2823
+
Features
--------
+- Python 3.6 compatibility.
+ https://github.com/Pylons/pyramid/issues/2835
+
- pcreate learned about --package-name to allow you to create a new project in
an existing folder with a different package name than the project name. See
https://github.com/Pylons/pyramid/pull/2783
@@ -80,6 +87,38 @@ Features
as soon as possible before importing the rest of pyramid.
See https://github.com/Pylons/pyramid/pull/2797
+- Pyramid no longer copies the settings object passed to the
+ ``pyramid.config.Configurator(settings=)``. The original ``dict`` is kept.
+ See https://github.com/Pylons/pyramid/pull/2823
+
+- The csrf trusted origins setting may now be a whitespace-separated list of
+ domains. Previously only a python list was allowed. Also, it can now be set
+ using the ``PYRAMID_CSRF_TRUSTED_ORIGINS`` environment variable similar to
+ other settings. See https://github.com/Pylons/pyramid/pull/2823
+
+- ``pserve --reload`` now uses the
+ `hupper <http://docs.pylonsproject.org/projects/hupper/en/latest/>`
+ library to monitor file changes. This comes with many improvements:
+
+ - If the `watchdog <http://pythonhosted.org/watchdog/>`_ package is
+ installed then monitoring will be done using inotify instead of
+ cpu and disk-intensive polling.
+
+ - The monitor is now a separate process that will not crash and starts up
+ before any of your code.
+
+ - The monitor will not restart the process after a crash until a file is
+ saved.
+
+ - The monitor works on windows.
+
+ - You can now trigger a reload manually from a pyramid view or any other
+ code via ``hupper.get_reloader().trigger_reload()``. Kind of neat.
+
+ - You can trigger a reload by issuing a ``SIGHUP`` to the monitor process.
+
+ See https://github.com/Pylons/pyramid/pull/2805
+
Bug Fixes
---------
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index bb21337e2..b4e30e085 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -284,3 +284,7 @@ Contributors
- Jon Davidson, 2016/07/18
- Keith Yang, 2016/07/22
+
+- Moriyoshi Koizumi, 2016/11/20
+
+- Mikko Ohtamaa, 2016/12/6
diff --git a/HACKING.txt b/HACKING.txt
index f240492e7..bbebb5165 100644
--- a/HACKING.txt
+++ b/HACKING.txt
@@ -118,8 +118,8 @@ In order to add a feature to Pyramid:
- The feature must be documented in both the API and narrative documentation
(in ``docs/``).
-- The feature must work fully on the following CPython versions: 2.7, 3.4,
- and 3.5 on both UNIX and Windows.
+- The feature must work fully on the following CPython versions: 2.7, 3.4, 3.5,
+ and 3.6 on both UNIX and Windows.
- The feature must work on the latest version of PyPy.
diff --git a/RELEASING.txt b/RELEASING.txt
index 4690fbd37..73cf38aa7 100644
--- a/RELEASING.txt
+++ b/RELEASING.txt
@@ -33,8 +33,8 @@ Prepare new release branch
- Run tests on Windows if feasible.
-- Make sure all scaffold tests pass (CPython 2.7, 3.4, and 3.5, and PyPy on
- UNIX; this doesn't work on Windows):
+- Make sure all scaffold tests pass (CPython 2.7, 3.4, 3.5, and 3.6, and PyPy
+ on UNIX; this doesn't work on Windows):
$ ./scaffoldtests.sh
diff --git a/TODO.txt b/TODO.txt
index d62efee5a..b064cd8e8 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -114,10 +114,6 @@ Nice-to-Have
Future
------
-- 1.6: turn ``pyramid.settings.Settings`` into a function that returns the
- original dict (after ``__getattr__`` deprecation period, it was deprecated
- in 1.2).
-
- 1.6: Remove IContextURL and TraversalContextURL.
- 1.9: Remove set_request_property.
diff --git a/appveyor.yml b/appveyor.yml
index e10dbc580..4c684bfc6 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -5,6 +5,11 @@ environment:
- PYTHON: "C:\\Python27"
TOXENV: "py27"
+cache:
+ - '%LOCALAPPDATA%\pip\Cache'
+
+version: '{branch}.{build}'
+
install:
- "%PYTHON%\\python.exe -m pip install tox"
diff --git a/docs/narr/install.rst b/docs/narr/install.rst
index 570cb2285..c3c2ba64c 100644
--- a/docs/narr/install.rst
+++ b/docs/narr/install.rst
@@ -22,7 +22,7 @@ the following sections.
.. sidebar:: Python Versions
As of this writing, :app:`Pyramid` is tested against Python 2.7,
- Python 3.4, Python 3.5, PyPy.
+ Python 3.4, Python 3.5, Python 3.6, and PyPy.
:app:`Pyramid` is known to run on all popular UNIX-like systems such as Linux,
Mac OS X, and FreeBSD, as well as on Windows platforms. It is also known to
diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst
index 47638579b..adad196e4 100644
--- a/docs/narr/introduction.rst
+++ b/docs/narr/introduction.rst
@@ -860,7 +860,7 @@ Every release of Pyramid has 100% statement coverage via unit and integration
tests, as measured by the ``coverage`` tool available on PyPI. It also has
greater than 95% decision/condition coverage as measured by the
``instrumental`` tool available on PyPI. It is automatically tested by Travis,
-and Jenkins on Python 2.7, Python 3.4, Python 3.5, and PyPy
+and Jenkins on Python 2.7, Python 3.4, Python 3.5, Python 3.6, and PyPy
after each commit to its GitHub repository. Official Pyramid add-ons are held
to a similar testing standard. We still find bugs in Pyramid and its official
add-ons, but we've noticed we find a lot more of them while working on other
diff --git a/docs/narr/project.rst b/docs/narr/project.rst
index 71bd176f6..6c42881f4 100644
--- a/docs/narr/project.rst
+++ b/docs/narr/project.rst
@@ -1045,3 +1045,23 @@ Another good production alternative is :term:`Green Unicorn` (aka
mod_wsgi, although it depends, in its default configuration, on having a
buffering HTTP proxy in front of it. It does not, as of this writing, work on
Windows.
+
+Automatically Reloading Your Code
+---------------------------------
+
+During development, it can be really useful to automatically have the
+webserver restart when you make changes. ``pserve`` has a ``--reload`` switch
+to enable this. It uses the
+`hupper <http://docs.pylonsproject.org/projects/hupper/en/latest/>` package
+to enable this behavior. When your code crashes, ``hupper`` will wait for
+another change or the ``SIGHUP`` signal before restarting again.
+
+inotify support
+~~~~~~~~~~~~~~~
+
+By default, ``hupper`` will poll the filesystem for changes to all python
+code. This can be pretty inefficient in larger projects. To be nicer to your
+hard drive, you should install the
+`watchdog <http://pythonhosted.org/watchdog/>` package in development.
+``hupper`` will automatically use ``watchdog`` to more efficiently poll the
+filesystem.
diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst
index 45c706b0d..5dc7d8816 100644
--- a/docs/quick_tour.rst
+++ b/docs/quick_tour.rst
@@ -26,7 +26,7 @@ To save a little bit of typing and to be certain that we use the modules,
scripts, and packages installed in our virtual environment, we'll set an
environment variable, too.
-As an example, for Python 3.5+ on Linux:
+As an example, for Python 3.6+ on Linux:
.. parsed-literal::
@@ -729,7 +729,7 @@ This yields the following output.
collected 1 items
hello_world/tests.py .
- ------------- coverage: platform darwin, python 3.5.0-final-0 -------------
+ ------------- coverage: platform darwin, python 3.6.0-final-0 -------------
Name Stmts Miss Cover Missing
--------------------------------------------------------
hello_world/__init__.py 11 8 27% 11-23
diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst
index afa8ed104..913e08a62 100644
--- a/docs/quick_tutorial/requirements.rst
+++ b/docs/quick_tutorial/requirements.rst
@@ -19,11 +19,11 @@ virtual environment.)
This *Quick Tutorial* is based on:
-* **Python 3.5**. Pyramid fully supports Python 3.4+ and Python 2.7+. This
- tutorial uses **Python 3.5** but runs fine under Python 2.7.
+* **Python 3.6**. Pyramid fully supports Python 3.4+ and Python 2.7+. This
+ tutorial uses **Python 3.6** but runs fine under Python 2.7.
* **venv**. We believe in virtual environments. For this tutorial, we use
- Python 3.5's built-in solution :term:`venv`. For Python 2.7, you can install
+ Python 3.6's built-in solution :term:`venv`. For Python 2.7, you can install
:term:`virtualenv`.
* **pip**. We use :term:`pip` for package management.
diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst
index 44097b35b..523acc53b 100644
--- a/docs/tutorials/wiki/authorization.rst
+++ b/docs/tutorials/wiki/authorization.rst
@@ -18,6 +18,7 @@ require permission, instead of a default "403 Forbidden" page.
We will implement the access control with the following steps:
+* Add password hashing dependencies
* Add users and groups (``security.py``, a new module).
* Add an :term:`ACL` (``models.py``).
* Add an :term:`authentication policy` and an :term:`authorization policy`
@@ -38,6 +39,25 @@ Then we will add the login and logout feature:
Access control
--------------
+Add dependencies
+~~~~~~~~~~~~~~~~
+
+Just like in :ref:`wiki_defining_views` we need a new dependency.
+We need to add the ``bcrypt`` [1]_ package, to our tutorial package's
+``setup.py`` file by assigning this dependency to the ``requires`` parameter
+in the ``setup()`` function.
+
+Open ``setup.py`` and edit it to look like the following:
+
+.. literalinclude:: src/authorization/setup.py
+ :linenos:
+ :emphasize-lines: 21
+ :language: python
+
+Only the highlighted line needs to be added.
+
+Do not forget to run ``pip install -e .`` just like in :ref:`wiki-running-pip-install`.
+
Add users and groups
~~~~~~~~~~~~~~~~~~~~
@@ -61,7 +81,20 @@ request)`` returns ``None``. We will use ``groupfinder()`` as an
:term:`authentication policy` "callback" that will provide the
:term:`principal` or principals for a user.
-In a production system, user and group data will most often come from a
+There are two helper methods that will help us later to authenticate users.
+The first is ``hash_password`` which takes a raw password and transforms it using
+bcrypt_ into an irreversible representation, a process known as "hashing". The
+second method, ``check_password``, will allow us to compare the hashed value of the
+submitted password against the hashed value of the password stored in the user's
+record. If the two hashed values match, then the submitted
+password is valid, and we can authenticate the user.
+
+We hash passwords so that it is impossible to decrypt and use them to
+authenticate in the application. If we stored passwords foolishly in clear text,
+then anyone with access to the database could retrieve any password to authenticate
+as any user.
+
+In a production system, user and group data will most often be saved and come from a
database, but here we use "dummy" data to represent user and groups sources.
Add an ACL
@@ -370,3 +403,11 @@ following URLs, checking that the result is as expected:
the login form with the ``editor`` credentials), we'll see a Logout link in
the upper right hand corner. When we click it, we're logged out, and
redirected back to the front page.
+
+
+.. _bcrypt: https://pypi.python.org/pypi/bcrypt
+
+.. [1] We are using the bcrypt_ package from PyPI to hash our passwords
+ securely. There are other one-way hash algorithms for passwords if
+ bcrypt is an issue on your system. Just make sure that it's an
+ algorithm approved for storing passwords versus a generic one-way hash.
diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst
index ac94d8059..3859d2cad 100644
--- a/docs/tutorials/wiki/definingviews.rst
+++ b/docs/tutorials/wiki/definingviews.rst
@@ -52,6 +52,7 @@ Open ``setup.py`` and edit it to look like the following:
Only the highlighted line needs to be added.
+.. _wiki-running-pip-install:
Running ``pip install -e .``
============================
diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst
index 6172b122b..ec79a4e9c 100644
--- a/docs/tutorials/wiki/installation.rst
+++ b/docs/tutorials/wiki/installation.rst
@@ -66,7 +66,7 @@ Python 2.7:
c:\> c:\Python27\Scripts\virtualenv %VENV%
-Python 3.5:
+Python 3.6:
.. code-block:: doscon
@@ -310,13 +310,13 @@ If successful, you will see output something like this:
.. code-block:: bash
======================== test session starts ========================
- platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ platform Python 3.6.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile:
plugins: cov-2.2.1
collected 1 items
tutorial/tests.py .
- ------------------ coverage: platform Python 3.5.1 ------------------
+ ------------------ coverage: platform Python 3.6.0 ------------------
Name Stmts Miss Cover Missing
----------------------------------------------------
tutorial/__init__.py 12 7 42% 7-8, 14-18
@@ -370,7 +370,8 @@ coverage.
Start the application
---------------------
-Start the application.
+Start the application. See :ref:`what_is_this_pserve_thing` for more
+information on ``pserve``.
On UNIX
^^^^^^^
diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py
index beeed75c9..68e3c0abd 100644
--- a/docs/tutorials/wiki/src/authorization/setup.py
+++ b/docs/tutorials/wiki/src/authorization/setup.py
@@ -18,6 +18,7 @@ requires = [
'ZODB3',
'waitress',
'docutils',
+ 'bcrypt',
]
tests_require = [
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/security.py b/docs/tutorials/wiki/src/authorization/tutorial/security.py
index d88c9c71f..cbb3acd5d 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/security.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/security.py
@@ -1,5 +1,18 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
+import bcrypt
+
+
+def hash_password(pw):
+ hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
+ # return unicode instead of bytes because databases handle it better
+ return hashed_pw.decode('utf-8')
+
+def check_password(expected_hash, pw):
+ if expected_hash is not None:
+ return bcrypt.checkpw(pw.encode('utf-8'), expected_hash.encode('utf-8'))
+ return False
+
+USERS = {'editor': hash_password('editor'),
+ 'viewer': hash_password('viewer')}
GROUPS = {'editor':['group:editors']}
def groupfinder(userid, request):
diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py
index c271d2cc1..e4560dfe1 100644
--- a/docs/tutorials/wiki/src/authorization/tutorial/views.py
+++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py
@@ -14,7 +14,7 @@ from pyramid.security import (
)
-from .security import USERS
+from .security import USERS, check_password
from .models import Page
# regular expression used to find WikiWords
@@ -94,7 +94,7 @@ def login(request):
if 'form.submitted' in request.params:
login = request.params['login']
password = request.params['password']
- if USERS.get(login) == password:
+ if check_password(USERS.get(login), password):
headers = remember(request, login)
return HTTPFound(location=came_from,
headers=headers)
diff --git a/docs/tutorials/wiki/src/tests/setup.py b/docs/tutorials/wiki/src/tests/setup.py
index beeed75c9..68e3c0abd 100644
--- a/docs/tutorials/wiki/src/tests/setup.py
+++ b/docs/tutorials/wiki/src/tests/setup.py
@@ -18,6 +18,7 @@ requires = [
'ZODB3',
'waitress',
'docutils',
+ 'bcrypt',
]
tests_require = [
diff --git a/docs/tutorials/wiki/src/tests/tutorial/security.py b/docs/tutorials/wiki/src/tests/tutorial/security.py
index d88c9c71f..cbb3acd5d 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/security.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/security.py
@@ -1,5 +1,18 @@
-USERS = {'editor':'editor',
- 'viewer':'viewer'}
+import bcrypt
+
+
+def hash_password(pw):
+ hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
+ # return unicode instead of bytes because databases handle it better
+ return hashed_pw.decode('utf-8')
+
+def check_password(expected_hash, pw):
+ if expected_hash is not None:
+ return bcrypt.checkpw(pw.encode('utf-8'), expected_hash.encode('utf-8'))
+ return False
+
+USERS = {'editor': hash_password('editor'),
+ 'viewer': hash_password('viewer')}
GROUPS = {'editor':['group:editors']}
def groupfinder(userid, request):
diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py
index 04beaea44..098e9c1bd 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/tests.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py
@@ -122,6 +122,17 @@ class EditPageTests(unittest.TestCase):
self.assertEqual(response.location, 'http://example.com/')
self.assertEqual(context.data, 'Hello yo!')
+class SecurityTests(unittest.TestCase):
+ def test_hashing(self):
+ from .security import hash_password, check_password
+ password = 'secretpassword'
+ hashed_password = hash_password(password)
+ self.assertTrue(check_password(hashed_password, password))
+
+ self.assertFalse(check_password(hashed_password, 'attackerpassword'))
+
+ self.assertFalse(check_password(None, password))
+
class FunctionalTests(unittest.TestCase):
viewer_login = '/login?login=viewer&password=viewer' \
diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py
index c271d2cc1..e4560dfe1 100644
--- a/docs/tutorials/wiki/src/tests/tutorial/views.py
+++ b/docs/tutorials/wiki/src/tests/tutorial/views.py
@@ -14,7 +14,7 @@ from pyramid.security import (
)
-from .security import USERS
+from .security import USERS, check_password
from .models import Page
# regular expression used to find WikiWords
@@ -94,7 +94,7 @@ def login(request):
if 'form.submitted' in request.params:
login = request.params['login']
password = request.params['password']
- if USERS.get(login) == password:
+ if check_password(USERS.get(login), password):
headers = remember(request, login)
return HTTPFound(location=came_from,
headers=headers)
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 0440c2d1d..fa990fb01 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -66,7 +66,7 @@ Python 2.7:
c:\> c:\Python27\Scripts\virtualenv %VENV%
-Python 3.5:
+Python 3.6:
.. code-block:: doscon
@@ -327,13 +327,13 @@ If successful, you will see output something like this:
.. code-block:: bash
======================== test session starts ========================
- platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
+ platform Python 3.6.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile:
plugins: cov-2.2.1
collected 2 items
tutorial/tests.py ..
- ------------------ coverage: platform Python 3.5.1 ------------------
+ ------------------ coverage: platform Python 3.6.0 ------------------
Name Stmts Miss Cover Missing
----------------------------------------------------------------
tutorial/__init__.py 8 6 25% 7-12
@@ -457,7 +457,8 @@ working directory. This is an SQLite database with a single table defined in it
Start the application
---------------------
-Start the application.
+Start the application. See :ref:`what_is_this_pserve_thing` for more
+information on ``pserve``.
On UNIX
^^^^^^^
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
index f3c0a6fef..c860ef8cf 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
@@ -28,6 +28,7 @@ def usage(argv):
def main(argv=sys.argv):
if len(argv) < 2:
usage(argv)
+ return
config_uri = argv[1]
options = parse_vars(argv[2:])
setup_logging(config_uri)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
index 715768b2e..0250e71c9 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py
@@ -11,6 +11,9 @@ class FunctionalTests(unittest.TestCase):
basic_wrong_login = (
'/login?login=basic&password=incorrect'
'&next=FrontPage&form.submitted=Login')
+ basic_login_no_next = (
+ '/login?login=basic&password=basic'
+ '&form.submitted=Login')
editor_login = (
'/login?login=editor&password=editor'
'&next=FrontPage&form.submitted=Login')
@@ -68,6 +71,10 @@ class FunctionalTests(unittest.TestCase):
res = self.testapp.get(self.basic_login, status=302)
self.assertEqual(res.location, 'http://localhost/FrontPage')
+ def test_successful_log_in_no_next(self):
+ res = self.testapp.get(self.basic_login_no_next, status=302)
+ self.assertEqual(res.location, 'http://localhost/')
+
def test_failed_log_in(self):
res = self.testapp.get(self.basic_wrong_login, status=200)
self.assertTrue(b'login' in res.body)
@@ -120,3 +127,8 @@ class FunctionalTests(unittest.TestCase):
self.testapp.get(self.editor_login, status=302)
res = self.testapp.get('/FrontPage', status=200)
self.assertTrue(b'FrontPage' in res.body)
+
+ def test_redirect_to_edit_for_existing_page(self):
+ self.testapp.get(self.editor_login, status=302)
+ res = self.testapp.get('/add_page/FrontPage', status=302)
+ self.assertTrue(b'FrontPage' in res.body)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py
new file mode 100644
index 000000000..97511d5e8
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py
@@ -0,0 +1,20 @@
+import mock
+import unittest
+
+
+class TestInitializeDB(unittest.TestCase):
+
+ @mock.patch('tutorial.scripts.initializedb.sys')
+ def test_usage(self, mocked_sys):
+ from ..scripts.initializedb import main
+ main(argv=['foo'])
+ mocked_sys.exit.assert_called_with(1)
+
+ @mock.patch('tutorial.scripts.initializedb.get_tm_session')
+ @mock.patch('tutorial.scripts.initializedb.sys')
+ def test_run(self, mocked_sys, mocked_session):
+ from ..scripts.initializedb import main
+ main(argv=['foo', 'development.ini'])
+ mocked_session.assert_called_once()
+
+
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py
new file mode 100644
index 000000000..4c3b72946
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py
@@ -0,0 +1,21 @@
+import mock
+import unittest
+
+
+class TestMyAuthenticationPolicy(unittest.TestCase):
+
+ def test_no_user(self):
+ request = mock.Mock()
+ request.user = None
+
+ from ..security import MyAuthenticationPolicy
+ policy = MyAuthenticationPolicy(None)
+ self.assertEqual(policy.authenticated_userid(request), None)
+
+ def test_authenticated_user(self):
+ request = mock.Mock()
+ request.user.id = 'foo'
+
+ from ..security import MyAuthenticationPolicy
+ policy = MyAuthenticationPolicy(None)
+ self.assertEqual(policy.authenticated_userid(request), 'foo')
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py
new file mode 100644
index 000000000..9490ac990
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py
@@ -0,0 +1,67 @@
+import unittest
+import transaction
+
+from pyramid import testing
+
+
+class BaseTest(unittest.TestCase):
+
+ def setUp(self):
+ from ..models import get_tm_session
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('..models')
+ self.config.include('..routes')
+
+ session_factory = self.config.registry['dbsession_factory']
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ self.init_database()
+
+ def init_database(self):
+ from ..models.meta import Base
+ session_factory = self.config.registry['dbsession_factory']
+ engine = session_factory.kw['bind']
+ Base.metadata.create_all(engine)
+
+ def tearDown(self):
+ testing.tearDown()
+ transaction.abort()
+
+ def makeUser(self, name, role):
+ from ..models import User
+ return User(name=name, role=role)
+
+
+class TestSetPassword(BaseTest):
+
+ def test_password_hash_saved(self):
+ user = self.makeUser(name='foo', role='bar')
+ self.assertFalse(user.password_hash)
+
+ user.set_password('secret')
+ self.assertTrue(user.password_hash)
+
+
+class TestCheckPassword(BaseTest):
+
+ def test_password_hash_not_set(self):
+ user = self.makeUser(name='foo', role='bar')
+ self.assertFalse(user.password_hash)
+
+ self.assertFalse(user.check_password('secret'))
+
+ def test_correct_password(self):
+ user = self.makeUser(name='foo', role='bar')
+ user.set_password('secret')
+ self.assertTrue(user.password_hash)
+
+ self.assertTrue(user.check_password('secret'))
+
+ def test_incorrect_password(self):
+ user = self.makeUser(name='foo', role='bar')
+ user.set_password('secret')
+ self.assertTrue(user.password_hash)
+
+ self.assertFalse(user.check_password('incorrect'))
diff --git a/pyramid/asset.py b/pyramid/asset.py
index e6a145341..9d7a3ee63 100644
--- a/pyramid/asset.py
+++ b/pyramid/asset.py
@@ -33,7 +33,7 @@ def asset_spec_from_abspath(abspath, package):
relpath.replace(os.path.sep, '/'))
return abspath
-# bw compat only; use pyramid.path.AssetDescriptor.abspath() instead
+# bw compat only; use pyramid.path.AssetResolver().resolve(spec).abspath()
def abspath_from_asset_spec(spec, pname='__main__'):
if pname is None:
return spec
diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py
index 0b76bbd70..5d99d6564 100644
--- a/pyramid/config/predicates.py
+++ b/pyramid/config/predicates.py
@@ -1,301 +1,2 @@
-import re
-
-from pyramid.exceptions import ConfigurationError
-
-from pyramid.compat import is_nonstr_iter
-
-from pyramid.traversal import (
- find_interface,
- traversal_path,
- resource_path_tuple
- )
-
-from pyramid.urldispatch import _compile_route
-from pyramid.util import object_description
-from pyramid.session import check_csrf_token
-
-from .util import as_sorted_tuple
-
-_marker = object()
-
-class XHRPredicate(object):
- def __init__(self, val, config):
- self.val = bool(val)
-
- def text(self):
- return 'xhr = %s' % self.val
-
- phash = text
-
- def __call__(self, context, request):
- return bool(request.is_xhr) is self.val
-
-class RequestMethodPredicate(object):
- def __init__(self, val, config):
- request_method = as_sorted_tuple(val)
- if 'GET' in request_method and 'HEAD' not in request_method:
- # GET implies HEAD too
- request_method = as_sorted_tuple(request_method + ('HEAD',))
- self.val = request_method
-
- def text(self):
- return 'request_method = %s' % (','.join(self.val))
-
- phash = text
-
- def __call__(self, context, request):
- return request.method in self.val
-
-class PathInfoPredicate(object):
- def __init__(self, val, config):
- self.orig = val
- try:
- val = re.compile(val)
- except re.error as why:
- raise ConfigurationError(why.args[0])
- self.val = val
-
- def text(self):
- return 'path_info = %s' % (self.orig,)
-
- phash = text
-
- def __call__(self, context, request):
- return self.val.match(request.upath_info) is not None
-
-class RequestParamPredicate(object):
- def __init__(self, val, config):
- val = as_sorted_tuple(val)
- reqs = []
- for p in val:
- k = p
- v = None
- if p.startswith('='):
- if '=' in p[1:]:
- k, v = p[1:].split('=', 1)
- k = '=' + k
- k, v = k.strip(), v.strip()
- elif '=' in p:
- k, v = p.split('=', 1)
- k, v = k.strip(), v.strip()
- reqs.append((k, v))
- self.val = val
- self.reqs = reqs
-
- def text(self):
- return 'request_param %s' % ','.join(
- ['%s=%s' % (x,y) if y else x for x, y in self.reqs]
- )
-
- phash = text
-
- def __call__(self, context, request):
- for k, v in self.reqs:
- actual = request.params.get(k)
- if actual is None:
- return False
- if v is not None and actual != v:
- return False
- return True
-
-class HeaderPredicate(object):
- def __init__(self, val, config):
- name = val
- v = None
- if ':' in name:
- name, val_str = name.split(':', 1)
- try:
- v = re.compile(val_str)
- except re.error as why:
- raise ConfigurationError(why.args[0])
- if v is None:
- self._text = 'header %s' % (name,)
- else:
- self._text = 'header %s=%s' % (name, val_str)
- self.name = name
- self.val = v
-
- def text(self):
- return self._text
-
- phash = text
-
- def __call__(self, context, request):
- if self.val is None:
- return self.name in request.headers
- val = request.headers.get(self.name)
- if val is None:
- return False
- return self.val.match(val) is not None
-
-class AcceptPredicate(object):
- def __init__(self, val, config):
- self.val = val
-
- def text(self):
- return 'accept = %s' % (self.val,)
-
- phash = text
-
- def __call__(self, context, request):
- return self.val in request.accept
-
-class ContainmentPredicate(object):
- def __init__(self, val, config):
- self.val = config.maybe_dotted(val)
-
- def text(self):
- return 'containment = %s' % (self.val,)
-
- phash = text
-
- def __call__(self, context, request):
- ctx = getattr(request, 'context', context)
- return find_interface(ctx, self.val) is not None
-
-class RequestTypePredicate(object):
- def __init__(self, val, config):
- self.val = val
-
- def text(self):
- return 'request_type = %s' % (self.val,)
-
- phash = text
-
- def __call__(self, context, request):
- return self.val.providedBy(request)
-
-class MatchParamPredicate(object):
- def __init__(self, val, config):
- val = as_sorted_tuple(val)
- self.val = val
- reqs = [ p.split('=', 1) for p in val ]
- self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ]
-
- def text(self):
- return 'match_param %s' % ','.join(
- ['%s=%s' % (x,y) for x, y in self.reqs]
- )
-
- phash = text
-
- def __call__(self, context, request):
- if not request.matchdict:
- # might be None
- return False
- for k, v in self.reqs:
- if request.matchdict.get(k) != v:
- return False
- return True
-
-class CustomPredicate(object):
- def __init__(self, func, config):
- self.func = func
-
- def text(self):
- return getattr(
- self.func,
- '__text__',
- 'custom predicate: %s' % object_description(self.func)
- )
-
- def phash(self):
- # using hash() here rather than id() is intentional: we
- # want to allow custom predicates that are part of
- # frameworks to be able to define custom __hash__
- # functions for custom predicates, so that the hash output
- # of predicate instances which are "logically the same"
- # may compare equal.
- return 'custom:%r' % hash(self.func)
-
- def __call__(self, context, request):
- return self.func(context, request)
-
-
-class TraversePredicate(object):
- # Can only be used as a *route* "predicate"; it adds 'traverse' to the
- # matchdict if it's specified in the routing args. This causes the
- # ResourceTreeTraverser to use the resolved traverse pattern as the
- # traversal path.
- def __init__(self, val, config):
- _, self.tgenerate = _compile_route(val)
- self.val = val
-
- def text(self):
- return 'traverse matchdict pseudo-predicate'
-
- def phash(self):
- # This isn't actually a predicate, it's just a infodict modifier that
- # injects ``traverse`` into the matchdict. As a result, we don't
- # need to update the hash.
- return ''
-
- def __call__(self, context, request):
- if 'traverse' in context:
- return True
- m = context['match']
- tvalue = self.tgenerate(m) # tvalue will be urlquoted string
- m['traverse'] = traversal_path(tvalue)
- # This isn't actually a predicate, it's just a infodict modifier that
- # injects ``traverse`` into the matchdict. As a result, we just
- # return True.
- return True
-
-class CheckCSRFTokenPredicate(object):
-
- check_csrf_token = staticmethod(check_csrf_token) # testing
-
- def __init__(self, val, config):
- self.val = val
-
- def text(self):
- return 'check_csrf = %s' % (self.val,)
-
- phash = text
-
- def __call__(self, context, request):
- val = self.val
- if val:
- if val is True:
- val = 'csrf_token'
- return self.check_csrf_token(request, val, raises=False)
- return True
-
-class PhysicalPathPredicate(object):
- def __init__(self, val, config):
- if is_nonstr_iter(val):
- self.val = tuple(val)
- else:
- val = tuple(filter(None, val.split('/')))
- self.val = ('',) + val
-
- def text(self):
- return 'physical_path = %s' % (self.val,)
-
- phash = text
-
- def __call__(self, context, request):
- if getattr(context, '__name__', _marker) is not _marker:
- return resource_path_tuple(context) == self.val
- return False
-
-class EffectivePrincipalsPredicate(object):
- def __init__(self, val, config):
- if is_nonstr_iter(val):
- self.val = set(val)
- else:
- self.val = set((val,))
-
- def text(self):
- return 'effective_principals = %s' % sorted(list(self.val))
-
- phash = text
-
- def __call__(self, context, request):
- req_principals = request.effective_principals
- if is_nonstr_iter(req_principals):
- rpset = set(req_principals)
- if self.val.issubset(rpset):
- return True
- return False
-
+import zope.deprecation
+zope.deprecation.moved('pyramid.predicates', 'Pyramid 2.0')
diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py
index 90d4d47d2..203baa128 100644
--- a/pyramid/config/routes.py
+++ b/pyramid/config/routes.py
@@ -13,12 +13,10 @@ from pyramid.registry import predvalseq
from pyramid.request import route_request_iface
from pyramid.urldispatch import RoutesMapper
-from pyramid.config.util import (
- action_method,
- as_sorted_tuple,
- )
+from pyramid.config.util import action_method
+from pyramid.util import as_sorted_tuple
-import pyramid.config.predicates
+import pyramid.predicates
class RoutesConfiguratorMixin(object):
@action_method
@@ -446,7 +444,7 @@ class RoutesConfiguratorMixin(object):
)
def add_default_route_predicates(self):
- p = pyramid.config.predicates
+ p = pyramid.predicates
for (name, factory) in (
('xhr', p.XHRPredicate),
('request_method', p.RequestMethodPredicate),
diff --git a/pyramid/config/security.py b/pyramid/config/security.py
index 02732c042..33593376b 100644
--- a/pyramid/config/security.py
+++ b/pyramid/config/security.py
@@ -9,9 +9,9 @@ from pyramid.interfaces import (
PHASE2_CONFIG,
)
-from pyramid.config.util import as_sorted_tuple
from pyramid.exceptions import ConfigurationError
from pyramid.util import action_method
+from pyramid.util import as_sorted_tuple
class SecurityConfiguratorMixin(object):
@action_method
diff --git a/pyramid/config/settings.py b/pyramid/config/settings.py
index f9dbd752e..26eb48951 100644
--- a/pyramid/config/settings.py
+++ b/pyramid/config/settings.py
@@ -1,15 +1,10 @@
import os
-import warnings
-from zope.interface import implementer
-
-from pyramid.interfaces import ISettings
-
-from pyramid.settings import asbool
+from pyramid.settings import asbool, aslist
class SettingsConfiguratorMixin(object):
def _set_settings(self, mapping):
- if not mapping:
+ if mapping is None:
mapping = {}
settings = Settings(mapping)
self.registry.settings = settings
@@ -54,118 +49,58 @@ class SettingsConfiguratorMixin(object):
return self.registry.settings
-@implementer(ISettings)
-class Settings(dict):
+def Settings(d=None, _environ_=os.environ, **kw):
""" Deployment settings. Update application settings (usually
from PasteDeploy keywords) with framework-specific key/value pairs
(e.g. find ``PYRAMID_DEBUG_AUTHORIZATION`` in os.environ and jam into
keyword args)."""
- # _environ_ is dep inj for testing
- def __init__(self, d=None, _environ_=os.environ, **kw):
- if d is None:
- d = {}
- dict.__init__(self, d, **kw)
- eget = _environ_.get
- config_debug_all = self.get('debug_all', '')
- config_debug_all = self.get('pyramid.debug_all', config_debug_all)
- eff_debug_all = asbool(eget('PYRAMID_DEBUG_ALL', config_debug_all))
- config_reload_all = self.get('reload_all', '')
- config_reload_all = self.get('pyramid.reload_all', config_reload_all)
- eff_reload_all = asbool(eget('PYRAMID_RELOAD_ALL', config_reload_all))
- config_debug_auth = self.get('debug_authorization', '')
- config_debug_auth = self.get('pyramid.debug_authorization',
- config_debug_auth)
- eff_debug_auth = asbool(eget('PYRAMID_DEBUG_AUTHORIZATION',
- config_debug_auth))
- config_debug_notfound = self.get('debug_notfound', '')
- config_debug_notfound = self.get('pyramid.debug_notfound',
- config_debug_notfound)
- eff_debug_notfound = asbool(eget('PYRAMID_DEBUG_NOTFOUND',
- config_debug_notfound))
- config_debug_routematch = self.get('debug_routematch', '')
- config_debug_routematch = self.get('pyramid.debug_routematch',
- config_debug_routematch)
- eff_debug_routematch = asbool(eget('PYRAMID_DEBUG_ROUTEMATCH',
- config_debug_routematch))
- config_debug_templates = self.get('debug_templates', '')
- config_debug_templates = self.get('pyramid.debug_templates',
- config_debug_templates)
- eff_debug_templates = asbool(eget('PYRAMID_DEBUG_TEMPLATES',
- config_debug_templates))
- config_reload_templates = self.get('reload_templates', '')
- config_reload_templates = self.get('pyramid.reload_templates',
- config_reload_templates)
- eff_reload_templates = asbool(eget('PYRAMID_RELOAD_TEMPLATES',
- config_reload_templates))
- config_reload_assets = self.get('reload_assets', '')
- config_reload_assets = self.get('pyramid.reload_assets',
- config_reload_assets)
- reload_assets = asbool(eget('PYRAMID_RELOAD_ASSETS',
- config_reload_assets))
- config_reload_resources = self.get('reload_resources', '')
- config_reload_resources = self.get('pyramid.reload_resources',
- config_reload_resources)
- reload_resources = asbool(eget('PYRAMID_RELOAD_RESOURCES',
- config_reload_resources))
- # reload_resources is an older alias for reload_assets
- eff_reload_assets = reload_assets or reload_resources
- locale_name = self.get('default_locale_name', 'en')
- locale_name = self.get('pyramid.default_locale_name', locale_name)
- eff_locale_name = eget('PYRAMID_DEFAULT_LOCALE_NAME', locale_name)
- config_prevent_http_cache = self.get('prevent_http_cache', '')
- config_prevent_http_cache = self.get('pyramid.prevent_http_cache',
- config_prevent_http_cache)
- eff_prevent_http_cache = asbool(eget('PYRAMID_PREVENT_HTTP_CACHE',
- config_prevent_http_cache))
- config_prevent_cachebust = self.get('prevent_cachebust', '')
- config_prevent_cachebust = self.get('pyramid.prevent_cachebust',
- config_prevent_cachebust)
- eff_prevent_cachebust = asbool(eget('PYRAMID_PREVENT_CACHEBUST',
- config_prevent_cachebust))
- csrf_trusted_origins = self.get("pyramid.csrf_trusted_origins", [])
- eff_csrf_trusted_origins = csrf_trusted_origins
-
- update = {
- 'debug_authorization': eff_debug_all or eff_debug_auth,
- 'debug_notfound': eff_debug_all or eff_debug_notfound,
- 'debug_routematch': eff_debug_all or eff_debug_routematch,
- 'debug_templates': eff_debug_all or eff_debug_templates,
- 'reload_templates': eff_reload_all or eff_reload_templates,
- 'reload_resources':eff_reload_all or eff_reload_assets,
- 'reload_assets':eff_reload_all or eff_reload_assets,
- 'default_locale_name':eff_locale_name,
- 'prevent_http_cache':eff_prevent_http_cache,
- 'prevent_cachebust':eff_prevent_cachebust,
- 'csrf_trusted_origins':eff_csrf_trusted_origins,
-
- 'pyramid.debug_authorization': eff_debug_all or eff_debug_auth,
- 'pyramid.debug_notfound': eff_debug_all or eff_debug_notfound,
- 'pyramid.debug_routematch': eff_debug_all or eff_debug_routematch,
- 'pyramid.debug_templates': eff_debug_all or eff_debug_templates,
- 'pyramid.reload_templates': eff_reload_all or eff_reload_templates,
- 'pyramid.reload_resources':eff_reload_all or eff_reload_assets,
- 'pyramid.reload_assets':eff_reload_all or eff_reload_assets,
- 'pyramid.default_locale_name':eff_locale_name,
- 'pyramid.prevent_http_cache':eff_prevent_http_cache,
- 'pyramid.prevent_cachebust':eff_prevent_cachebust,
- 'pyramid.csrf_trusted_origins':eff_csrf_trusted_origins,
- }
-
- self.update(update)
-
- def __getattr__(self, name):
- try:
- val = self[name]
- # only deprecate on success; a probing getattr/hasattr should not
- # print this warning
- warnings.warn(
- 'Obtaining settings via attributes of the settings dictionary '
- 'is deprecated as of Pyramid 1.2; use settings["foo"] instead '
- 'of settings.foo',
- DeprecationWarning,
- 2
- )
- return val
- except KeyError:
- raise AttributeError(name)
-
+ if d is None:
+ d = {}
+ d.update(**kw)
+
+ eget = _environ_.get
+ def expand_key(key):
+ keys = [key]
+ if not key.startswith('pyramid.'):
+ keys.append('pyramid.' + key)
+ return keys
+ def S(settings_key, env_key=None, type_=str, default=False):
+ value = default
+ keys = expand_key(settings_key)
+ for key in keys:
+ value = d.get(key, value)
+ if env_key:
+ value = eget(env_key, value)
+ value = type_(value)
+ d.update({k: value for k in keys})
+ def O(settings_key, override_key):
+ for key in expand_key(settings_key):
+ d[key] = d[key] or d[override_key]
+
+ S('debug_all', 'PYRAMID_DEBUG_ALL', asbool)
+ S('debug_authorization', 'PYRAMID_DEBUG_AUTHORIZATION', asbool)
+ O('debug_authorization', 'debug_all')
+ S('debug_notfound', 'PYRAMID_DEBUG_NOTFOUND', asbool)
+ O('debug_notfound', 'debug_all')
+ S('debug_routematch', 'PYRAMID_DEBUG_ROUTEMATCH', asbool)
+ O('debug_routematch', 'debug_all')
+ S('debug_templates', 'PYRAMID_DEBUG_TEMPLATES', asbool)
+ O('debug_templates', 'debug_all')
+
+ S('reload_all', 'PYRAMID_RELOAD_ALL', asbool)
+ S('reload_templates', 'PYRAMID_RELOAD_TEMPLATES', asbool)
+ O('reload_templates', 'reload_all')
+ S('reload_assets', 'PYRAMID_RELOAD_ASSETS', asbool)
+ O('reload_assets', 'reload_all')
+ S('reload_resources', 'PYRAMID_RELOAD_RESOURCES', asbool)
+ O('reload_resources', 'reload_all')
+ # reload_resources is an older alias for reload_assets
+ for k in expand_key('reload_assets') + expand_key('reload_resources'):
+ d[k] = d['reload_assets'] or d['reload_resources']
+
+ S('default_locale_name', 'PYRAMID_DEFAULT_LOCALE_NAME', str, 'en')
+ S('prevent_http_cache', 'PYRAMID_PREVENT_HTTP_CACHE', asbool)
+ S('prevent_cachebust', 'PYRAMID_PREVENT_CACHEBUST', asbool)
+ S('csrf_trusted_origins', 'PYRAMID_CSRF_TRUSTED_ORIGINS', aslist, [])
+
+ return d
diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py
index 0aeb01fe3..16712ab16 100644
--- a/pyramid/config/tweens.py
+++ b/pyramid/config/tweens.py
@@ -17,9 +17,9 @@ from pyramid.tweens import (
from pyramid.config.util import (
action_method,
- is_string_or_iterable,
TopologicalSorter,
)
+from pyramid.util import is_string_or_iterable
class TweensConfiguratorMixin(object):
def add_tween(self, tween_factory, under=None, over=None):
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index 626e8d5fe..67bba9593 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -4,8 +4,7 @@ import inspect
from pyramid.compat import (
bytes_,
getargspec,
- is_nonstr_iter,
- string_types,
+ is_nonstr_iter
)
from pyramid.compat import im_func
@@ -24,18 +23,6 @@ ActionInfo = ActionInfo # support bw compat imports
MAX_ORDER = 1 << 30
DEFAULT_PHASH = md5().hexdigest()
-def is_string_or_iterable(v):
- if isinstance(v, string_types):
- return True
- if hasattr(v, '__iter__'):
- return True
-
-def as_sorted_tuple(val):
- if not is_nonstr_iter(val):
- val = (val,)
- val = tuple(sorted(val))
- return val
-
class not_(object):
"""
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 6082d8b48..65c9da585 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -70,10 +70,11 @@ import pyramid.util
from pyramid.util import (
viewdefaults,
action_method,
+ as_sorted_tuple,
TopologicalSorter,
)
-import pyramid.config.predicates
+import pyramid.predicates
import pyramid.viewderivers
from pyramid.viewderivers import (
@@ -89,7 +90,6 @@ from pyramid.viewderivers import (
from pyramid.config.util import (
DEFAULT_PHASH,
MAX_ORDER,
- as_sorted_tuple,
)
urljoin = urlparse.urljoin
@@ -1143,7 +1143,7 @@ class ViewsConfiguratorMixin(object):
)
def add_default_view_predicates(self):
- p = pyramid.config.predicates
+ p = pyramid.predicates
for (name, factory) in (
('xhr', p.XHRPredicate),
('request_method', p.RequestMethodPredicate),
diff --git a/pyramid/predicates.py b/pyramid/predicates.py
new file mode 100644
index 000000000..7c3a778ca
--- /dev/null
+++ b/pyramid/predicates.py
@@ -0,0 +1,300 @@
+import re
+
+from pyramid.exceptions import ConfigurationError
+
+from pyramid.compat import is_nonstr_iter
+
+from pyramid.session import check_csrf_token
+from pyramid.traversal import (
+ find_interface,
+ traversal_path,
+ resource_path_tuple
+ )
+
+from pyramid.urldispatch import _compile_route
+from pyramid.util import object_description
+from pyramid.util import as_sorted_tuple
+
+_marker = object()
+
+class XHRPredicate(object):
+ def __init__(self, val, config):
+ self.val = bool(val)
+
+ def text(self):
+ return 'xhr = %s' % self.val
+
+ phash = text
+
+ def __call__(self, context, request):
+ return bool(request.is_xhr) is self.val
+
+class RequestMethodPredicate(object):
+ def __init__(self, val, config):
+ request_method = as_sorted_tuple(val)
+ if 'GET' in request_method and 'HEAD' not in request_method:
+ # GET implies HEAD too
+ request_method = as_sorted_tuple(request_method + ('HEAD',))
+ self.val = request_method
+
+ def text(self):
+ return 'request_method = %s' % (','.join(self.val))
+
+ phash = text
+
+ def __call__(self, context, request):
+ return request.method in self.val
+
+class PathInfoPredicate(object):
+ def __init__(self, val, config):
+ self.orig = val
+ try:
+ val = re.compile(val)
+ except re.error as why:
+ raise ConfigurationError(why.args[0])
+ self.val = val
+
+ def text(self):
+ return 'path_info = %s' % (self.orig,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ return self.val.match(request.upath_info) is not None
+
+class RequestParamPredicate(object):
+ def __init__(self, val, config):
+ val = as_sorted_tuple(val)
+ reqs = []
+ for p in val:
+ k = p
+ v = None
+ if p.startswith('='):
+ if '=' in p[1:]:
+ k, v = p[1:].split('=', 1)
+ k = '=' + k
+ k, v = k.strip(), v.strip()
+ elif '=' in p:
+ k, v = p.split('=', 1)
+ k, v = k.strip(), v.strip()
+ reqs.append((k, v))
+ self.val = val
+ self.reqs = reqs
+
+ def text(self):
+ return 'request_param %s' % ','.join(
+ ['%s=%s' % (x,y) if y else x for x, y in self.reqs]
+ )
+
+ phash = text
+
+ def __call__(self, context, request):
+ for k, v in self.reqs:
+ actual = request.params.get(k)
+ if actual is None:
+ return False
+ if v is not None and actual != v:
+ return False
+ return True
+
+class HeaderPredicate(object):
+ def __init__(self, val, config):
+ name = val
+ v = None
+ if ':' in name:
+ name, val_str = name.split(':', 1)
+ try:
+ v = re.compile(val_str)
+ except re.error as why:
+ raise ConfigurationError(why.args[0])
+ if v is None:
+ self._text = 'header %s' % (name,)
+ else:
+ self._text = 'header %s=%s' % (name, val_str)
+ self.name = name
+ self.val = v
+
+ def text(self):
+ return self._text
+
+ phash = text
+
+ def __call__(self, context, request):
+ if self.val is None:
+ return self.name in request.headers
+ val = request.headers.get(self.name)
+ if val is None:
+ return False
+ return self.val.match(val) is not None
+
+class AcceptPredicate(object):
+ def __init__(self, val, config):
+ self.val = val
+
+ def text(self):
+ return 'accept = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ return self.val in request.accept
+
+class ContainmentPredicate(object):
+ def __init__(self, val, config):
+ self.val = config.maybe_dotted(val)
+
+ def text(self):
+ return 'containment = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ ctx = getattr(request, 'context', context)
+ return find_interface(ctx, self.val) is not None
+
+class RequestTypePredicate(object):
+ def __init__(self, val, config):
+ self.val = val
+
+ def text(self):
+ return 'request_type = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ return self.val.providedBy(request)
+
+class MatchParamPredicate(object):
+ def __init__(self, val, config):
+ val = as_sorted_tuple(val)
+ self.val = val
+ reqs = [ p.split('=', 1) for p in val ]
+ self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ]
+
+ def text(self):
+ return 'match_param %s' % ','.join(
+ ['%s=%s' % (x,y) for x, y in self.reqs]
+ )
+
+ phash = text
+
+ def __call__(self, context, request):
+ if not request.matchdict:
+ # might be None
+ return False
+ for k, v in self.reqs:
+ if request.matchdict.get(k) != v:
+ return False
+ return True
+
+class CustomPredicate(object):
+ def __init__(self, func, config):
+ self.func = func
+
+ def text(self):
+ return getattr(
+ self.func,
+ '__text__',
+ 'custom predicate: %s' % object_description(self.func)
+ )
+
+ def phash(self):
+ # using hash() here rather than id() is intentional: we
+ # want to allow custom predicates that are part of
+ # frameworks to be able to define custom __hash__
+ # functions for custom predicates, so that the hash output
+ # of predicate instances which are "logically the same"
+ # may compare equal.
+ return 'custom:%r' % hash(self.func)
+
+ def __call__(self, context, request):
+ return self.func(context, request)
+
+
+class TraversePredicate(object):
+ # Can only be used as a *route* "predicate"; it adds 'traverse' to the
+ # matchdict if it's specified in the routing args. This causes the
+ # ResourceTreeTraverser to use the resolved traverse pattern as the
+ # traversal path.
+ def __init__(self, val, config):
+ _, self.tgenerate = _compile_route(val)
+ self.val = val
+
+ def text(self):
+ return 'traverse matchdict pseudo-predicate'
+
+ def phash(self):
+ # This isn't actually a predicate, it's just a infodict modifier that
+ # injects ``traverse`` into the matchdict. As a result, we don't
+ # need to update the hash.
+ return ''
+
+ def __call__(self, context, request):
+ if 'traverse' in context:
+ return True
+ m = context['match']
+ tvalue = self.tgenerate(m) # tvalue will be urlquoted string
+ m['traverse'] = traversal_path(tvalue)
+ # This isn't actually a predicate, it's just a infodict modifier that
+ # injects ``traverse`` into the matchdict. As a result, we just
+ # return True.
+ return True
+
+class CheckCSRFTokenPredicate(object):
+
+ check_csrf_token = staticmethod(check_csrf_token) # testing
+
+ def __init__(self, val, config):
+ self.val = val
+
+ def text(self):
+ return 'check_csrf = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ val = self.val
+ if val:
+ if val is True:
+ val = 'csrf_token'
+ return self.check_csrf_token(request, val, raises=False)
+ return True
+
+class PhysicalPathPredicate(object):
+ def __init__(self, val, config):
+ if is_nonstr_iter(val):
+ self.val = tuple(val)
+ else:
+ val = tuple(filter(None, val.split('/')))
+ self.val = ('',) + val
+
+ def text(self):
+ return 'physical_path = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ if getattr(context, '__name__', _marker) is not _marker:
+ return resource_path_tuple(context) == self.val
+ return False
+
+class EffectivePrincipalsPredicate(object):
+ def __init__(self, val, config):
+ if is_nonstr_iter(val):
+ self.val = set(val)
+ else:
+ self.val = set((val,))
+
+ def text(self):
+ return 'effective_principals = %s' % sorted(list(self.val))
+
+ phash = text
+
+ def __call__(self, context, request):
+ req_principals = request.effective_principals
+ if is_nonstr_iter(req_principals):
+ rpset = set(req_principals)
+ if self.val.issubset(rpset):
+ return True
+ return False
+
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index 0d22c9f3f..969bc07f1 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -8,50 +8,30 @@
# Code taken also from QP: http://www.mems-exchange.org/software/qp/ From
# lib/site.py
-import atexit
-import ctypes
import optparse
import os
-import py_compile
import re
-import subprocess
import sys
-import tempfile
import textwrap
import threading
import time
-import traceback
import webbrowser
-from paste.deploy import loadserver
-from paste.deploy import loadapp
-from paste.deploy.loadwsgi import loadcontext, SERVER
+import hupper
+from paste.deploy import (
+ loadapp,
+ loadserver,
+)
+from paste.deploy.loadwsgi import (
+ SERVER,
+ loadcontext,
+)
from pyramid.compat import PY2
-from pyramid.compat import WIN
from pyramid.scripts.common import parse_vars
from pyramid.scripts.common import setup_logging
-MAXFD = 1024
-
-try:
- import termios
-except ImportError: # pragma: no cover
- termios = None
-
-if WIN and not hasattr(os, 'kill'): # pragma: no cover
- # py 2.6 on windows
- def kill(pid, sig=None):
- """kill function for Win32"""
- # signal is ignored, semibogus raise message
- kernel32 = ctypes.windll.kernel32
- handle = kernel32.OpenProcess(1, 0, pid)
- if (0 == kernel32.TerminateProcess(handle, 0)):
- raise OSError('No such process %s' % pid)
-else:
- kill = os.kill
-
def main(argv=sys.argv, quiet=False):
command = PServeCommand(argv, quiet=quiet)
return command.run()
@@ -119,9 +99,6 @@ class PServeCommand(object):
_scheme_re = re.compile(r'^[a-z][a-z]+:', re.I)
- _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
- _monitor_environ_key = 'PASTE_MONITOR_SHOULD_RUN'
-
def __init__(self, argv, quiet=False):
self.options, self.args = self.parser.parse_args(argv[1:])
if quiet:
@@ -141,19 +118,8 @@ class PServeCommand(object):
return 2
app_spec = self.args[0]
- if self.options.reload:
- if os.environ.get(self._reloader_environ_key):
- if self.options.verbose > 1:
- self.out('Running reloading file monitor')
- install_reloader(int(self.options.reload_interval), [app_spec])
- # if self.requires_config_file:
- # watch_file(self.args[0])
- else:
- return self.restart_with_reloader()
-
- app_name = self.options.app_name
-
vars = self.get_options()
+ app_name = self.options.app_name
if not self._scheme_re.search(app_spec):
app_spec = 'config:' + app_spec
@@ -166,6 +132,34 @@ class PServeCommand(object):
server_spec = app_spec
base = os.getcwd()
+ # do not open the browser on each reload so check hupper first
+ if self.options.browser and not hupper.is_active():
+ def open_browser():
+ context = loadcontext(
+ SERVER, app_spec, name=server_name, relative_to=base,
+ global_conf=vars)
+ url = 'http://127.0.0.1:{port}/'.format(**context.config())
+ time.sleep(1)
+ webbrowser.open(url)
+ t = threading.Thread(target=open_browser)
+ t.setDaemon(True)
+ t.start()
+
+ if self.options.reload and not hupper.is_active():
+ if self.options.verbose > 1:
+ self.out('Running reloading file monitor')
+ hupper.start_reloader(
+ 'pyramid.scripts.pserve.main',
+ reload_interval=int(self.options.reload_interval),
+ verbose=self.options.verbose,
+ )
+ return 0
+
+ if hupper.is_active():
+ reloader = hupper.get_reloader()
+ if app_spec.startswith('config:'):
+ reloader.watch_files([app_spec[len('config:'):]])
+
log_fn = app_spec
if log_fn.startswith('config:'):
log_fn = app_spec[len('config:'):]
@@ -178,8 +172,8 @@ class PServeCommand(object):
server = self.loadserver(server_spec, name=server_name,
relative_to=base, global_conf=vars)
- app = self.loadapp(app_spec, name=app_name, relative_to=base,
- global_conf=vars)
+ app = self.loadapp(
+ app_spec, name=app_name, relative_to=base, global_conf=vars)
if self.options.verbose > 0:
if hasattr(os, 'getpid'):
@@ -200,17 +194,6 @@ class PServeCommand(object):
msg = ''
self.out('Exiting%s (-v to see traceback)' % msg)
- if self.options.browser:
- def open_browser():
- context = loadcontext(SERVER, app_spec, name=server_name, relative_to=base,
- global_conf=vars)
- url = 'http://127.0.0.1:{port}/'.format(**context.config())
- time.sleep(1)
- webbrowser.open(url)
- t = threading.Thread(target=open_browser)
- t.setDaemon(True)
- t.start()
-
serve()
def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover
@@ -220,394 +203,6 @@ class PServeCommand(object):
return loadserver(
server_spec, name=name, relative_to=relative_to, **kw)
- def quote_first_command_arg(self, arg): # pragma: no cover
- """
- There's a bug in Windows when running an executable that's
- located inside a path with a space in it. This method handles
- that case, or on non-Windows systems or an executable with no
- spaces, it just leaves well enough alone.
- """
- if (sys.platform != 'win32' or ' ' not in arg):
- # Problem does not apply:
- return arg
- try:
- import win32api
- except ImportError:
- raise ValueError(
- "The executable %r contains a space, and in order to "
- "handle this issue you must have the win32api module "
- "installed" % arg)
- arg = win32api.GetShortPathName(arg)
- return arg
-
- def find_script_path(self, name): # pragma: no cover
- """
- Return the path to the script being invoked by the python interpreter.
-
- There's an issue on Windows when running the executable from
- a console_script causing the script name (sys.argv[0]) to
- not end with .exe or .py and thus cannot be run via popen.
- """
- if sys.platform == 'win32':
- if not name.endswith('.exe') and not name.endswith('.py'):
- name += '.exe'
- return name
-
- def restart_with_reloader(self): # pragma: no cover
- self.restart_with_monitor(reloader=True)
-
- def restart_with_monitor(self, reloader=False): # pragma: no cover
- if self.options.verbose > 0:
- if reloader:
- self.out('Starting subprocess with file monitor')
- else:
- self.out('Starting subprocess with monitor parent')
- while 1:
- args = [
- self.quote_first_command_arg(sys.executable),
- self.find_script_path(sys.argv[0]),
- ] + sys.argv[1:]
- new_environ = os.environ.copy()
- if reloader:
- new_environ[self._reloader_environ_key] = 'true'
- else:
- new_environ[self._monitor_environ_key] = 'true'
- proc = None
- try:
- try:
- _turn_sigterm_into_systemexit()
- proc = subprocess.Popen(args, env=new_environ)
- exit_code = proc.wait()
- proc = None
- except KeyboardInterrupt:
- self.out('^C caught in monitor process')
- if self.options.verbose > 1:
- raise
- return 1
- finally:
- if proc is not None:
- import signal
- try:
- kill(proc.pid, signal.SIGTERM)
- except (OSError, IOError):
- pass
-
- if reloader:
- # Reloader always exits with code 3; but if we are
- # a monitor, any exit code will restart
- if exit_code != 3:
- return exit_code
- if self.options.verbose > 0:
- self.out('%s %s %s' % ('-' * 20, 'Restarting', '-' * 20))
-
-class LazyWriter(object):
-
- """
- File-like object that opens a file lazily when it is first written
- to.
- """
-
- def __init__(self, filename, mode='w'):
- self.filename = filename
- self.fileobj = None
- self.lock = threading.Lock()
- self.mode = mode
-
- def open(self):
- if self.fileobj is None:
- with self.lock:
- self.fileobj = open(self.filename, self.mode)
- return self.fileobj
-
- def close(self):
- fileobj = self.fileobj
- if fileobj is not None:
- fileobj.close()
-
- def __del__(self):
- self.close()
-
- def write(self, text):
- fileobj = self.open()
- fileobj.write(text)
- fileobj.flush()
-
- def writelines(self, text):
- fileobj = self.open()
- fileobj.writelines(text)
- fileobj.flush()
-
- def flush(self):
- self.open().flush()
-
-def ensure_port_cleanup(
- bound_addresses, maxtries=30, sleeptime=2): # pragma: no cover
- """
- This makes sure any open ports are closed.
-
- Does this by connecting to them until they give connection
- refused. Servers should call like::
-
- ensure_port_cleanup([80, 443])
- """
- atexit.register(_cleanup_ports, bound_addresses, maxtries=maxtries,
- sleeptime=sleeptime)
-
-def _cleanup_ports(
- bound_addresses, maxtries=30, sleeptime=2): # pragma: no cover
- # Wait for the server to bind to the port.
- import socket
- import errno
- for bound_address in bound_addresses:
- for attempt in range(maxtries):
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- try:
- sock.connect(bound_address)
- except socket.error as e:
- if e.args[0] != errno.ECONNREFUSED:
- raise
- break
- else:
- time.sleep(sleeptime)
- else:
- raise SystemExit('Timeout waiting for port.')
- sock.close()
-
-def _turn_sigterm_into_systemexit(): # pragma: no cover
- """
- Attempts to turn a SIGTERM exception into a SystemExit exception.
- """
- try:
- import signal
- except ImportError:
- return
- def handle_term(signo, frame):
- raise SystemExit
- signal.signal(signal.SIGTERM, handle_term)
-
-def ensure_echo_on(): # pragma: no cover
- if termios:
- fd = sys.stdin
- if fd.isatty():
- attr_list = termios.tcgetattr(fd)
- if not attr_list[3] & termios.ECHO:
- attr_list[3] |= termios.ECHO
- termios.tcsetattr(fd, termios.TCSANOW, attr_list)
-
-def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover
- """
- Install the reloading monitor.
-
- On some platforms server threads may not terminate when the main
- thread does, causing ports to remain open/locked.
- """
- ensure_echo_on()
- mon = Monitor(poll_interval=poll_interval)
- if extra_files is None:
- extra_files = []
- mon.extra_files.extend(extra_files)
- t = threading.Thread(target=mon.periodic_reload)
- t.setDaemon(True)
- t.start()
-
-class classinstancemethod(object):
- """
- Acts like a class method when called from a class, like an
- instance method when called by an instance. The method should
- take two arguments, 'self' and 'cls'; one of these will be None
- depending on how the method was called.
- """
-
- def __init__(self, func):
- self.func = func
- self.__doc__ = func.__doc__
-
- def __get__(self, obj, type=None):
- return _methodwrapper(self.func, obj=obj, type=type)
-
-class _methodwrapper(object):
-
- def __init__(self, func, obj, type):
- self.func = func
- self.obj = obj
- self.type = type
-
- def __call__(self, *args, **kw):
- assert 'self' not in kw and 'cls' not in kw, (
- "You cannot use 'self' or 'cls' arguments to a "
- "classinstancemethod")
- return self.func(*((self.obj, self.type) + args), **kw)
-
-
-class Monitor(object): # pragma: no cover
- """
- A file monitor and server restarter.
-
- Use this like:
-
- ..code-block:: Python
-
- install_reloader()
-
- Then make sure your server is installed with a shell script like::
-
- err=3
- while test "$err" -eq 3 ; do
- python server.py
- err="$?"
- done
-
- or is run from this .bat file (if you use Windows)::
-
- @echo off
- :repeat
- python server.py
- if %errorlevel% == 3 goto repeat
-
- or run a monitoring process in Python (``pserve --reload`` does
- this).
-
- Use the ``watch_file(filename)`` function to cause a reload/restart for
- other non-Python files (e.g., configuration files). If you have
- a dynamic set of files that grows over time you can use something like::
-
- def watch_config_files():
- return CONFIG_FILE_CACHE.keys()
- add_file_callback(watch_config_files)
-
- Then every time the reloader polls files it will call
- ``watch_config_files`` and check all the filenames it returns.
- """
- instances = []
- global_extra_files = []
- global_file_callbacks = []
-
- def __init__(self, poll_interval):
- self.module_mtimes = {}
- self.keep_running = True
- self.poll_interval = poll_interval
- self.extra_files = list(self.global_extra_files)
- self.instances.append(self)
- self.syntax_error_files = set()
- self.pending_reload = False
- self.file_callbacks = list(self.global_file_callbacks)
- temp_pyc_fp = tempfile.NamedTemporaryFile(delete=False)
- self.temp_pyc = temp_pyc_fp.name
- temp_pyc_fp.close()
-
- def _exit(self):
- try:
- os.unlink(self.temp_pyc)
- except IOError:
- # not worried if the tempfile can't be removed
- pass
- # use os._exit() here and not sys.exit() since within a
- # thread sys.exit() just closes the given thread and
- # won't kill the process; note os._exit does not call
- # any atexit callbacks, nor does it do finally blocks,
- # flush open files, etc. In otherwords, it is rude.
- os._exit(3)
-
- def periodic_reload(self):
- while True:
- if not self.check_reload():
- self._exit()
- break
- time.sleep(self.poll_interval)
-
- def check_reload(self):
- filenames = list(self.extra_files)
- for file_callback in self.file_callbacks:
- try:
- filenames.extend(file_callback())
- except:
- print(
- "Error calling reloader callback %r:" % file_callback)
- traceback.print_exc()
- for module in list(sys.modules.values()):
- try:
- filename = module.__file__
- except (AttributeError, ImportError):
- continue
- if filename is not None:
- filenames.append(filename)
- new_changes = False
- for filename in filenames:
- try:
- stat = os.stat(filename)
- if stat:
- mtime = stat.st_mtime
- else:
- mtime = 0
- except (OSError, IOError):
- continue
- if filename.endswith('.pyc') and os.path.exists(filename[:-1]):
- mtime = max(os.stat(filename[:-1]).st_mtime, mtime)
- pyc = True
- else:
- pyc = False
- old_mtime = self.module_mtimes.get(filename)
- self.module_mtimes[filename] = mtime
- if old_mtime is not None and old_mtime < mtime:
- new_changes = True
- if pyc:
- filename = filename[:-1]
- is_valid = True
- if filename.endswith('.py'):
- is_valid = self.check_syntax(filename)
- if is_valid:
- print("%s changed ..." % filename)
- if new_changes:
- self.pending_reload = True
- if self.syntax_error_files:
- for filename in sorted(self.syntax_error_files):
- print("%s has a SyntaxError; NOT reloading." % filename)
- if self.pending_reload and not self.syntax_error_files:
- self.pending_reload = False
- return False
- return True
-
- def check_syntax(self, filename):
- # check if a file has syntax errors.
- # If so, track it until it's fixed.
- try:
- py_compile.compile(filename, cfile=self.temp_pyc, doraise=True)
- except py_compile.PyCompileError as ex:
- print(ex.msg)
- self.syntax_error_files.add(filename)
- return False
- else:
- if filename in self.syntax_error_files:
- self.syntax_error_files.remove(filename)
- return True
-
- def watch_file(self, cls, filename):
- """Watch the named file for changes"""
- filename = os.path.abspath(filename)
- if self is None:
- for instance in cls.instances:
- instance.watch_file(filename)
- cls.global_extra_files.append(filename)
- else:
- self.extra_files.append(filename)
-
- watch_file = classinstancemethod(watch_file)
-
- def add_file_callback(self, cls, callback):
- """Add a callback -- a function that takes no parameters -- that will
- return a list of filenames to watch for changes."""
- if self is None:
- for instance in cls.instances:
- instance.add_file_callback(callback)
- cls.global_file_callbacks.append(callback)
- else:
- self.file_callbacks.append(callback)
-
- add_file_callback = classinstancemethod(add_file_callback)
-
-watch_file = Monitor.watch_file
-add_file_callback = Monitor.add_file_callback
-
# For paste.deploy server instantiation (egg:pyramid#wsgiref)
def wsgiref_server_runner(wsgi_app, global_conf, **kw): # pragma: no cover
from wsgiref.simple_server import make_server
diff --git a/pyramid/tests/test_config/test_settings.py b/pyramid/tests/test_config/test_settings.py
index d2a98b347..2dbe9b1bb 100644
--- a/pyramid/tests/test_config/test_settings.py
+++ b/pyramid/tests/test_config/test_settings.py
@@ -11,6 +11,13 @@ class TestSettingsConfiguratorMixin(unittest.TestCase):
settings = config._set_settings(None)
self.assertTrue(settings)
+ def test__set_settings_uses_original_dict(self):
+ config = self._makeOne()
+ dummy = {}
+ result = config._set_settings(dummy)
+ self.assertTrue(dummy is result)
+ self.assertEqual(dummy['pyramid.debug_all'], False)
+
def test__set_settings_as_dictwithvalues(self):
config = self._makeOne()
settings = config._set_settings({'a':'1'})
@@ -68,26 +75,6 @@ class TestSettings(unittest.TestCase):
klass = self._getTargetClass()
return klass(d, _environ_=environ)
- def test_getattr_success(self):
- import warnings
- with warnings.catch_warnings(record=True) as w:
- warnings.filterwarnings('always')
- settings = self._makeOne({'reload_templates':False})
- self.assertEqual(settings.reload_templates, False)
- self.assertEqual(len(w), 1)
-
- def test_getattr_fail(self):
- import warnings
- with warnings.catch_warnings(record=True) as w:
- warnings.filterwarnings('always')
- settings = self._makeOne({})
- self.assertRaises(AttributeError, settings.__getattr__, 'wontexist')
- self.assertEqual(len(w), 0)
-
- def test_getattr_raises_attribute_error(self):
- settings = self._makeOne()
- self.assertRaises(AttributeError, settings.__getattr__, 'mykey')
-
def test_noargs(self):
settings = self._makeOne()
self.assertEqual(settings['debug_authorization'], False)
@@ -557,6 +544,18 @@ class TestSettings(unittest.TestCase):
self.assertEqual(result['default_locale_name'], 'abc')
self.assertEqual(result['pyramid.default_locale_name'], 'abc')
+ def test_csrf_trusted_origins(self):
+ result = self._makeOne({})
+ self.assertEqual(result['pyramid.csrf_trusted_origins'], [])
+ result = self._makeOne({'pyramid.csrf_trusted_origins': 'example.com'})
+ self.assertEqual(result['pyramid.csrf_trusted_origins'], ['example.com'])
+ result = self._makeOne({'pyramid.csrf_trusted_origins': ['example.com']})
+ self.assertEqual(result['pyramid.csrf_trusted_origins'], ['example.com'])
+ result = self._makeOne({'pyramid.csrf_trusted_origins': (
+ 'example.com foo.example.com\nasdf.example.com')})
+ self.assertEqual(result['pyramid.csrf_trusted_origins'], [
+ 'example.com', 'foo.example.com', 'asdf.example.com'])
+
def test_originals_kept(self):
result = self._makeOne({'a':'i am so a'})
self.assertEqual(result['a'], 'i am so a')
diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py
index ccf7fa260..398b6fba8 100644
--- a/pyramid/tests/test_config/test_util.py
+++ b/pyramid/tests/test_config/test_util.py
@@ -5,7 +5,7 @@ class TestPredicateList(unittest.TestCase):
def _makeOne(self):
from pyramid.config.util import PredicateList
- from pyramid.config import predicates
+ from pyramid import predicates
inst = PredicateList()
for name, factory in (
('xhr', predicates.XHRPredicate),
@@ -594,6 +594,15 @@ class TestNotted(unittest.TestCase):
self.assertEqual(inst.phash(), '')
self.assertEqual(inst(None, None), True)
+
+class TestDeprecatedPredicates(unittest.TestCase):
+ def test_it(self):
+ import warnings
+ with warnings.catch_warnings(record=True) as w:
+ warnings.filterwarnings('always')
+ from pyramid.config.predicates import XHRPredicate
+ self.assertEqual(len(w), 1)
+
class DummyPredicate(object):
def __init__(self, result):
self.result = result
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index f020485de..211632730 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -2309,9 +2309,9 @@ class TestViewsConfigurationMixin(unittest.TestCase):
# Since Python 3 has to be all cool and fancy and different...
def _assertBody(self, response, value):
from pyramid.compat import text_type
- if isinstance(value, text_type): # pragma: nocover
+ if isinstance(value, text_type): # pragma: no cover
self.assertEqual(response.text, value)
- else: # pragma: nocover
+ else: # pragma: no cover
self.assertEqual(response.body, value)
def test_add_notfound_view_with_renderer(self):
diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py
index 6c6e16d55..224fa4cf0 100644
--- a/pyramid/tests/test_httpexceptions.py
+++ b/pyramid/tests/test_httpexceptions.py
@@ -348,7 +348,7 @@ class TestHTTPException(unittest.TestCase):
exc = cls(body_template='${REQUEST_METHOD}')
environ = _makeEnviron()
class Choke(object):
- def __str__(self): # pragma nocover
+ def __str__(self): # pragma no cover
raise ValueError
environ['gardentheory.user'] = Choke()
start_response = DummyStartResponse()
diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_predicates.py
index 9cd8f2734..8a002c24e 100644
--- a/pyramid/tests/test_config/test_predicates.py
+++ b/pyramid/tests/test_predicates.py
@@ -6,7 +6,7 @@ from pyramid.compat import text_
class TestXHRPredicate(unittest.TestCase):
def _makeOne(self, val):
- from pyramid.config.predicates import XHRPredicate
+ from pyramid.predicates import XHRPredicate
return XHRPredicate(val, None)
def test___call___true(self):
@@ -33,7 +33,7 @@ class TestXHRPredicate(unittest.TestCase):
class TestRequestMethodPredicate(unittest.TestCase):
def _makeOne(self, val):
- from pyramid.config.predicates import RequestMethodPredicate
+ from pyramid.predicates import RequestMethodPredicate
return RequestMethodPredicate(val, None)
def test_ctor_get_but_no_head(self):
@@ -71,7 +71,7 @@ class TestRequestMethodPredicate(unittest.TestCase):
class TestPathInfoPredicate(unittest.TestCase):
def _makeOne(self, val):
- from pyramid.config.predicates import PathInfoPredicate
+ from pyramid.predicates import PathInfoPredicate
return PathInfoPredicate(val, None)
def test_ctor_compilefail(self):
@@ -102,7 +102,7 @@ class TestPathInfoPredicate(unittest.TestCase):
class TestRequestParamPredicate(unittest.TestCase):
def _makeOne(self, val):
- from pyramid.config.predicates import RequestParamPredicate
+ from pyramid.predicates import RequestParamPredicate
return RequestParamPredicate(val, None)
def test___call___true_exists(self):
@@ -174,7 +174,7 @@ class TestRequestParamPredicate(unittest.TestCase):
class TestMatchParamPredicate(unittest.TestCase):
def _makeOne(self, val):
- from pyramid.config.predicates import MatchParamPredicate
+ from pyramid.predicates import MatchParamPredicate
return MatchParamPredicate(val, None)
def test___call___true_single(self):
@@ -216,7 +216,7 @@ class TestMatchParamPredicate(unittest.TestCase):
class TestCustomPredicate(unittest.TestCase):
def _makeOne(self, val):
- from pyramid.config.predicates import CustomPredicate
+ from pyramid.predicates import CustomPredicate
return CustomPredicate(val, None)
def test___call___true(self):
@@ -255,7 +255,7 @@ class TestCustomPredicate(unittest.TestCase):
class TestTraversePredicate(unittest.TestCase):
def _makeOne(self, val):
- from pyramid.config.predicates import TraversePredicate
+ from pyramid.predicates import TraversePredicate
return TraversePredicate(val, None)
def test___call__traverse_has_remainder_already(self):
@@ -297,7 +297,7 @@ class TestTraversePredicate(unittest.TestCase):
class Test_CheckCSRFTokenPredicate(unittest.TestCase):
def _makeOne(self, val, config):
- from pyramid.config.predicates import CheckCSRFTokenPredicate
+ from pyramid.predicates import CheckCSRFTokenPredicate
return CheckCSRFTokenPredicate(val, config)
def test_text(self):
@@ -340,7 +340,7 @@ class Test_CheckCSRFTokenPredicate(unittest.TestCase):
class TestHeaderPredicate(unittest.TestCase):
def _makeOne(self, val):
- from pyramid.config.predicates import HeaderPredicate
+ from pyramid.predicates import HeaderPredicate
return HeaderPredicate(val, None)
def test___call___true_exists(self):
@@ -404,7 +404,7 @@ class TestHeaderPredicate(unittest.TestCase):
class Test_PhysicalPathPredicate(unittest.TestCase):
def _makeOne(self, val, config):
- from pyramid.config.predicates import PhysicalPathPredicate
+ from pyramid.predicates import PhysicalPathPredicate
return PhysicalPathPredicate(val, config)
def test_text(self):
@@ -468,7 +468,7 @@ class Test_EffectivePrincipalsPredicate(unittest.TestCase):
testing.tearDown()
def _makeOne(self, val, config):
- from pyramid.config.predicates import EffectivePrincipalsPredicate
+ from pyramid.predicates import EffectivePrincipalsPredicate
return EffectivePrincipalsPredicate(val, config)
def test_text(self):
diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py
index bf4763602..e84de92d4 100644
--- a/pyramid/tests/test_scripts/test_pserve.py
+++ b/pyramid/tests/test_scripts/test_pserve.py
@@ -1,5 +1,3 @@
-import os
-import tempfile
import unittest
class TestPServeCommand(unittest.TestCase):
@@ -69,71 +67,3 @@ class Test_main(unittest.TestCase):
def test_it(self):
result = self._callFUT(['pserve'])
self.assertEqual(result, 2)
-
-class TestLazyWriter(unittest.TestCase):
- def _makeOne(self, filename, mode='w'):
- from pyramid.scripts.pserve import LazyWriter
- return LazyWriter(filename, mode)
-
- def test_open(self):
- filename = tempfile.mktemp()
- try:
- inst = self._makeOne(filename)
- fp = inst.open()
- self.assertEqual(fp.name, filename)
- finally:
- fp.close()
- os.remove(filename)
-
- def test_write(self):
- filename = tempfile.mktemp()
- try:
- inst = self._makeOne(filename)
- inst.write('hello')
- finally:
- with open(filename) as f:
- data = f.read()
- self.assertEqual(data, 'hello')
- inst.close()
- os.remove(filename)
-
- def test_writeline(self):
- filename = tempfile.mktemp()
- try:
- inst = self._makeOne(filename)
- inst.writelines('hello')
- finally:
- with open(filename) as f:
- data = f.read()
- self.assertEqual(data, 'hello')
- inst.close()
- os.remove(filename)
-
- def test_flush(self):
- filename = tempfile.mktemp()
- try:
- inst = self._makeOne(filename)
- inst.flush()
- fp = inst.fileobj
- self.assertEqual(fp.name, filename)
- finally:
- fp.close()
- os.remove(filename)
-
-class Test__methodwrapper(unittest.TestCase):
- def _makeOne(self, func, obj, type):
- from pyramid.scripts.pserve import _methodwrapper
- return _methodwrapper(func, obj, type)
-
- def test___call__succeed(self):
- def foo(self, cls, a=1): return 1
- class Bar(object): pass
- wrapper = self._makeOne(foo, Bar, None)
- result = wrapper(a=1)
- self.assertEqual(result, 1)
-
- def test___call__fail(self):
- def foo(self, cls, a=1): return 1
- class Bar(object): pass
- wrapper = self._makeOne(foo, Bar, None)
- self.assertRaises(AssertionError, wrapper, cls=1)
diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py
index 0decd04d6..5fc878a32 100644
--- a/pyramid/tests/test_traversal.py
+++ b/pyramid/tests/test_traversal.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
import unittest
import warnings
@@ -839,7 +840,7 @@ class QuotePathSegmentTests(unittest.TestCase):
def test_string(self):
s = '/ hello!'
result = self._callFUT(s)
- self.assertEqual(result, '%2F%20hello%21')
+ self.assertEqual(result, '%2F%20hello!')
def test_int(self):
s = 12345
@@ -1299,6 +1300,15 @@ class Test__join_path_tuple(unittest.TestCase):
result = self._callFUT(('x',))
self.assertEqual(result, 'x')
+ def test_segments_with_unsafes(self):
+ safe_segments = tuple(u"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~!$&'()*+,;=:@")
+ result = self._callFUT(safe_segments)
+ self.assertEqual(result, u'/'.join(safe_segments))
+ unsafe_segments = tuple(chr(i) for i in range(0x20, 0x80) if not chr(i) in safe_segments) + (u'あ',)
+ result = self._callFUT(unsafe_segments)
+ self.assertEqual(result, u'/'.join(''.join('%%%02X' % (ord(c) if isinstance(c, str) else c) for c in unsafe_segment.encode('utf-8')) for unsafe_segment in unsafe_segments))
+
+
def make_traverser(result):
class DummyTraverser(object):
def __init__(self, context):
diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py
index 2d20b24c3..06f4ad793 100644
--- a/pyramid/tests/test_urldispatch.py
+++ b/pyramid/tests/test_urldispatch.py
@@ -485,11 +485,15 @@ class TestCompileRouteFunctional(unittest.TestCase):
def test_generator_functional_newstyle(self):
self.generates('/{x}', {'x':''}, '/')
self.generates('/{x}', {'x':'a'}, '/a')
+ self.generates('/{x}', {'x':'a/b/c'}, '/a/b/c')
+ self.generates('/{x}', {'x':':@&+$,'}, '/:@&+$,')
self.generates('zzz/{x}', {'x':'abc'}, '/zzz/abc')
self.generates('zzz/{x}*traverse', {'x':'abc', 'traverse':''},
'/zzz/abc')
self.generates('zzz/{x}*traverse', {'x':'abc', 'traverse':'/def/g'},
'/zzz/abc/def/g')
+ self.generates('zzz/{x}*traverse', {'x':':@&+$,', 'traverse':'/:@&+$,'},
+ '/zzz/:@&+$,/:@&+$,')
self.generates('/{x}', {'x':text_(b'/La Pe\xc3\xb1a', 'utf-8')},
'//La%20Pe%C3%B1a')
self.generates('/{x}*y', {'x':text_(b'/La Pe\xc3\xb1a', 'utf-8'),
diff --git a/pyramid/traversal.py b/pyramid/traversal.py
index 963a76bb5..1ca52692a 100644
--- a/pyramid/traversal.py
+++ b/pyramid/traversal.py
@@ -35,6 +35,9 @@ with warnings.catch_warnings():
warnings.filterwarnings('ignore')
from pyramid.interfaces import IContextURL
+PATH_SEGMENT_SAFE = "~!$&'()*+,;=:@" # from webob
+PATH_SAFE = PATH_SEGMENT_SAFE + "/"
+
empty = text_('')
def find_root(resource):
@@ -577,7 +580,7 @@ the ``safe`` argument to this function. This corresponds to the
if PY2:
# special-case on Python 2 for speed? unchecked
- def quote_path_segment(segment, safe=''):
+ def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE):
""" %s """ % quote_path_segment_doc
# The bit of this code that deals with ``_segment_cache`` is an
# optimization: we cache all the computation of URL path segments
@@ -596,7 +599,7 @@ if PY2:
_segment_cache[(segment, safe)] = result
return result
else:
- def quote_path_segment(segment, safe=''):
+ def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE):
""" %s """ % quote_path_segment_doc
# The bit of this code that deals with ``_segment_cache`` is an
# optimization: we cache all the computation of URL path segments
diff --git a/pyramid/url.py b/pyramid/url.py
index 646cc857d..d6587e783 100644
--- a/pyramid/url.py
+++ b/pyramid/url.py
@@ -25,10 +25,11 @@ from pyramid.threadlocal import get_current_registry
from pyramid.traversal import (
ResourceURL,
quote_path_segment,
+ PATH_SAFE,
+ PATH_SEGMENT_SAFE,
)
-PATH_SAFE = '/:@&+$,' # from webob
-QUERY_SAFE = '/?:@!$&\'()*+,;=' # RFC 3986
+QUERY_SAFE = "/?:@!$&'()*+,;=" # RFC 3986
ANCHOR_SAFE = QUERY_SAFE
def parse_url_overrides(kw):
@@ -947,4 +948,4 @@ def current_route_path(request, *elements, **kw):
@lru_cache(1000)
def _join_elements(elements):
- return '/'.join([quote_path_segment(s, safe=':@&+$,') for s in elements])
+ return '/'.join([quote_path_segment(s, safe=PATH_SEGMENT_SAFE) for s in elements])
diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py
index c88ad9590..a61071845 100644
--- a/pyramid/urldispatch.py
+++ b/pyramid/urldispatch.py
@@ -22,6 +22,7 @@ from pyramid.exceptions import URLDecodeError
from pyramid.traversal import (
quote_path_segment,
split_path_info,
+ PATH_SAFE,
)
_marker = object()
@@ -207,6 +208,10 @@ def _compile_route(route):
return d
gen = ''.join(gen)
+
+ def q(v):
+ return quote_path_segment(v, safe=PATH_SAFE)
+
def generator(dict):
newdict = {}
for k, v in dict.items():
@@ -223,17 +228,17 @@ def _compile_route(route):
# a stararg argument
if is_nonstr_iter(v):
v = '/'.join(
- [quote_path_segment(x, safe='/') for x in v]
+ [q(x) for x in v]
) # native
else:
if v.__class__ not in string_types:
v = str(v)
- v = quote_path_segment(v, safe='/')
+ v = q(v)
else:
if v.__class__ not in string_types:
v = str(v)
# v may be bytes (py2) or native string (py3)
- v = quote_path_segment(v, safe='/')
+ v = q(v)
# at this point, the value will be a native string
newdict[k] = v
diff --git a/pyramid/util.py b/pyramid/util.py
index 4936dcb24..3337d410d 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -3,7 +3,7 @@ import functools
try:
# py2.7.7+ and py3.3+ have native comparison support
from hmac import compare_digest
-except ImportError: # pragma: nocover
+except ImportError: # pragma: no cover
compare_digest = None
import inspect
import traceback
@@ -28,13 +28,24 @@ from pyramid.compat import (
from pyramid.interfaces import IActionInfo
from pyramid.path import DottedNameResolver as _DottedNameResolver
+_marker = object()
+
class DottedNameResolver(_DottedNameResolver):
def __init__(self, package=None): # default to package = None for bw compat
_DottedNameResolver.__init__(self, package)
-_marker = object()
-
+def is_string_or_iterable(v):
+ if isinstance(v, string_types):
+ return True
+ if hasattr(v, '__iter__'):
+ return True
+
+def as_sorted_tuple(val):
+ if not is_nonstr_iter(val):
+ val = (val,)
+ val = tuple(sorted(val))
+ return val
class InstancePropertyHelper(object):
"""A helper object for assigning properties and descriptors to instances.
diff --git a/setup.py b/setup.py
index f738ee623..a6dbc2824 100644
--- a/setup.py
+++ b/setup.py
@@ -14,19 +14,21 @@
import os
import sys
+import warnings
from setuptools import setup, find_packages
py_version = sys.version_info[:2]
-PY3 = py_version[0] == 3
+PY2 = py_version[0] == 2
-if PY3:
- if py_version < (3, 4):
- raise RuntimeError('On Python 3, Pyramid requires Python 3.4 or better')
-else:
- if py_version < (2, 7):
- raise RuntimeError('On Python 2, Pyramid requires Python 2.7 or better')
+if (3, 0) <= py_version < (3, 4):
+ warnings.warn(
+ 'On Python 3, Pyramid only supports Python 3.4 or better',
+ UserWarning,
+ )
+elif py_version < (2, 7):
+ raise RuntimeError('On Python 2, Pyramid requires Python 2.7 or better')
here = os.path.abspath(os.path.dirname(__file__))
try:
@@ -46,13 +48,14 @@ install_requires = [
'venusian >= 1.0a3', # ``ignore``
'translationstring >= 0.4', # py3 compat
'PasteDeploy >= 1.5.0', # py3 compat
+ 'hupper',
]
tests_require = [
'WebTest >= 1.3.1', # py3 compat
]
-if not PY3:
+if PY2:
tests_require.append('zope.component>=3.11.0')
docs_extras = [
@@ -82,6 +85,7 @@ setup(name='pyramid',
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Framework :: Pyramid",