summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2018-08-10 10:28:04 -0500
committerGitHub <noreply@github.com>2018-08-10 10:28:04 -0500
commit3ee04cc62205b10eb9041b0df5e156936765202f (patch)
tree666862c069fce963e4fac454b767f08586687686
parent0ad05afc020d2790048d4ca85d936b4ea79eae13 (diff)
parent0760eba8fd5a0d8f0424c329ce92e9fb8d003f11 (diff)
downloadpyramid-3ee04cc62205b10eb9041b0df5e156936765202f.tar.gz
pyramid-3ee04cc62205b10eb9041b0df5e156936765202f.tar.bz2
pyramid-3ee04cc62205b10eb9041b0df5e156936765202f.zip
Merge branch 'master' into feature/more-samesite-work
-rw-r--r--.travis.yml5
-rw-r--r--CHANGES.rst21
-rw-r--r--docs/glossary.rst3
-rw-r--r--docs/narr/commandline.rst59
-rw-r--r--docs/quick_tutorial/json/tutorial/__init__.py4
-rw-r--r--docs/tutorials/wiki2/authentication.rst8
-rw-r--r--docs/tutorials/wiki2/definingmodels.rst208
-rw-r--r--docs/tutorials/wiki2/definingviews.rst2
-rw-r--r--docs/tutorials/wiki2/design.rst2
-rw-r--r--docs/tutorials/wiki2/installation.rst243
-rw-r--r--docs/tutorials/wiki2/src/authentication/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/authentication/README.txt12
-rw-r--r--docs/tutorials/wiki2/src/authentication/development.ini6
-rw-r--r--docs/tutorials/wiki2/src/authentication/production.ini6
-rw-r--r--docs/tutorials/wiki2/src/authentication/pytest.ini2
-rw-r--r--docs/tutorials/wiki2/src/authentication/setup.py5
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/scripts/initialize_db.py56
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py57
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja22
-rw-r--r--docs/tutorials/wiki2/src/authentication/tutorial/views/default.py12
-rw-r--r--docs/tutorials/wiki2/src/authorization/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/authorization/README.txt12
-rw-r--r--docs/tutorials/wiki2/src/authorization/development.ini6
-rw-r--r--docs/tutorials/wiki2/src/authorization/production.ini6
-rw-r--r--docs/tutorials/wiki2/src/authorization/pytest.ini2
-rw-r--r--docs/tutorials/wiki2/src/authorization/setup.py5
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/routes.py6
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py56
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py57
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/security.py4
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja22
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/tests.py2
-rw-r--r--docs/tutorials/wiki2/src/authorization/tutorial/views/default.py6
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/README.txt12
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/development.ini6
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/production.ini6
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/pytest.ini2
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/setup.py5
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initialize_db.py44
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py45
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja22
-rw-r--r--docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py11
-rw-r--r--docs/tutorials/wiki2/src/installation/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/installation/README.txt12
-rw-r--r--docs/tutorials/wiki2/src/installation/development.ini6
-rw-r--r--docs/tutorials/wiki2/src/installation/production.ini6
-rw-r--r--docs/tutorials/wiki2/src/installation/pytest.ini2
-rw-r--r--docs/tutorials/wiki2/src/installation/setup.py5
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/scripts/initialize_db.py44
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py45
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja22
-rw-r--r--docs/tutorials/wiki2/src/installation/tutorial/views/default.py11
-rw-r--r--docs/tutorials/wiki2/src/models/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/models/README.txt12
-rw-r--r--docs/tutorials/wiki2/src/models/development.ini6
-rw-r--r--docs/tutorials/wiki2/src/models/production.ini6
-rw-r--r--docs/tutorials/wiki2/src/models/pytest.ini2
-rw-r--r--docs/tutorials/wiki2/src/models/setup.py5
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py56
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py57
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja22
-rw-r--r--docs/tutorials/wiki2/src/models/tutorial/views/default.py11
-rw-r--r--docs/tutorials/wiki2/src/tests/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/tests/README.txt12
-rw-r--r--docs/tutorials/wiki2/src/tests/development.ini6
-rw-r--r--docs/tutorials/wiki2/src/tests/production.ini6
-rw-r--r--docs/tutorials/wiki2/src/tests/pytest.ini2
-rw-r--r--docs/tutorials/wiki2/src/tests/setup.py5
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/routes.py6
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/scripts/initialize_db.py56
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py58
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/security.py4
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja22
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py4
-rw-r--r--docs/tutorials/wiki2/src/tests/tutorial/views/default.py6
-rw-r--r--docs/tutorials/wiki2/src/views/.gitignore21
-rw-r--r--docs/tutorials/wiki2/src/views/README.txt12
-rw-r--r--docs/tutorials/wiki2/src/views/development.ini6
-rw-r--r--docs/tutorials/wiki2/src/views/production.ini6
-rw-r--r--docs/tutorials/wiki2/src/views/pytest.ini2
-rw-r--r--docs/tutorials/wiki2/src/views/setup.py5
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/alembic/env.py58
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/alembic/script.py.mako24
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/alembic/versions/README.txt1
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/models/__init__.py4
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py56
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py57
-rw-r--r--docs/tutorials/wiki2/src/views/tutorial/views/default.py14
-rw-r--r--pyramid/authentication.py11
-rw-r--r--pyramid/config/__init__.py14
-rw-r--r--pyramid/config/adapters.py7
-rw-r--r--pyramid/config/assets.py2
-rw-r--r--pyramid/config/factories.py2
-rw-r--r--pyramid/config/i18n.py3
-rw-r--r--pyramid/config/predicates.py2
-rw-r--r--pyramid/config/rendering.py2
-rw-r--r--pyramid/config/routes.py7
-rw-r--r--pyramid/config/security.py2
-rw-r--r--pyramid/config/testing.py2
-rw-r--r--pyramid/config/tweens.py7
-rw-r--r--pyramid/config/util.py146
-rw-r--r--pyramid/config/views.py79
-rw-r--r--pyramid/csrf.py5
-rw-r--r--pyramid/predicates.py30
-rw-r--r--pyramid/registry.py2
-rw-r--r--pyramid/scripts/pshell.py110
-rw-r--r--pyramid/tests/test_authentication.py15
-rw-r--r--pyramid/tests/test_config/test_init.py2
-rw-r--r--pyramid/tests/test_config/test_util.py264
-rw-r--r--pyramid/tests/test_config/test_views.py16
-rw-r--r--pyramid/tests/test_decorator.py7
-rw-r--r--pyramid/tests/test_events.py1
-rw-r--r--pyramid/tests/test_predicates.py38
-rw-r--r--pyramid/tests/test_scripts/dummy.py2
-rw-r--r--pyramid/tests/test_scripts/test_pshell.py29
-rw-r--r--pyramid/tests/test_testing.py2
-rw-r--r--pyramid/tests/test_util.py282
-rw-r--r--pyramid/util.py153
-rw-r--r--pyramid/viewderivers.py55
-rw-r--r--tox.ini19
143 files changed, 2444 insertions, 1247 deletions
diff --git a/.travis.yml b/.travis.yml
index 9e296f987..9884c3d19 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,9 +12,10 @@ matrix:
env: TOXENV=py35
- python: 3.6
env: TOXENV=py36
- # aws s3 ls s3://travis-python-archives/binaries/ubuntu/14.04/x86_64/ | grep 'pypy.*bz2$'
- - python: pypy-5.4.1
+ - python: pypy
env: TOXENV=pypy
+ - python: pypy3
+ env: TOXENV=pypy3
- python: 3.5
env: TOXENV=py2-cover,py3-cover,coverage
- python: 3.5
diff --git a/CHANGES.rst b/CHANGES.rst
index cd4647e82..91dadfa79 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -30,8 +30,8 @@ Features
route_prefix for ``include`` and ``add_route`` calls inside the context.
See https://github.com/Pylons/pyramid/pull/3279
-- Modify the builtin session implementations to support SameSite options on
- cookies and set the default to ``'Lax'``. This affects
+- Modify the builtin session implementations to support ``SameSite`` options
+ on cookies and set the default to ``'Lax'``. This affects
``pyramid.session.BaseCookieSessionFactory``,
``pyramid.session.SignedCookieSessionFactory``, and
``pyramid.session.UnencryptedCookieSessionFactoryConfig``.
@@ -46,6 +46,14 @@ Features
exception/response object for a HTTP 308 redirect.
See https://github.com/Pylons/pyramid/pull/3302
+- Within ``pshell``, allow the user-defined ``setup`` function to be a
+ generator, in which case it may wrap the command's lifecycle.
+ See https://github.com/Pylons/pyramid/pull/3318
+
+- Within ``pshell``, variables defined by the ``[pshell]`` settings are
+ available within the user-defined ``setup`` function.
+ See https://github.com/Pylons/pyramid/pull/3318
+
Bug Fixes
---------
@@ -81,9 +89,18 @@ Backward Incompatibilities
``pyramid.session.UnencryptedCookieSessionFactoryConfig``.
See https://github.com/Pylons/pyramid/pull/3300
+- Variables defined in the ``[pshell]`` section of the settings will no
+ longer override those set by the ``setup`` function.
+ See https://github.com/Pylons/pyramid/pull/3318
+
Documentation Changes
---------------------
+- Add support for alembic to the pyramid-cookiecutter-alchemy cookiecutter
+ and update the wiki2 tutorial to explain how it works.
+ See https://github.com/Pylons/pyramid/pull/3307 and
+ https://github.com/Pylons/pyramid-cookiecutter-alchemy/pull/7
+
- Bump Sphinx to >= 1.7.4 in setup.py to support ``emphasize-lines`` in PDFs
and to pave the way for xelatex support. See
https://github.com/Pylons/pyramid/pull/3271,
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 7e16b569e..16a719e10 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -1206,3 +1206,6 @@ Glossary
context manager
A context manager is an object that defines the runtime context to be established when executing a :ref:`with <python:with>` statement in Python. The context manager handles the entry into, and the exit from, the desired runtime context for the execution of the block of code. Context managers are normally invoked using the ``with`` statement, but can also be used by directly invoking their methods. Pyramid adds context managers for :class:`pyramid.config.Configurator`, :meth:`pyramid.interfaces.IRouter.request_context`, :func:`pyramid.paster.bootstrap`, :func:`pyramid.scripting.prepare`, and :func:`pyramid.testing.testConfig`. See also the Python documentation for :ref:`With Statement Context Managers <python:context-managers>` and :pep:`343`.
+
+ Alembic
+ `Alembic <http://alembic.zzzcomputing.com/en/latest/>`_ is a lightweight database migration tool for usage with the SQLAlchemy Database Toolkit for Python.
diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst
index 98663cca6..d0c1e6edd 100644
--- a/docs/narr/commandline.rst
+++ b/docs/narr/commandline.rst
@@ -196,51 +196,61 @@ Extending the Shell
It is convenient when using the interactive shell often to have some variables
significant to your application already loaded as globals when you start the
``pshell``. To facilitate this, ``pshell`` will look for a special ``[pshell]``
-section in your INI file and expose the subsequent key/value pairs to the
+section in your ``.ini`` file and expose the subsequent key/value pairs to the
shell. Each key is a variable name that will be global within the pshell
session; each value is a :term:`dotted Python name`. If specified, the special
key ``setup`` should be a :term:`dotted Python name` pointing to a callable
that accepts the dictionary of globals that will be loaded into the shell. This
allows for some custom initializing code to be executed each time the
``pshell`` is run. The ``setup`` callable can also be specified from the
-commandline using the ``--setup`` option which will override the key in the INI
+commandline using the ``--setup`` option which will override the key in the ``.ini``
file.
For example, you want to expose your model to the shell along with the database
session so that you can mutate the model on an actual database. Here, we'll
-assume your model is stored in the ``myapp.models`` package.
+assume your model is stored in the ``myapp.models`` package and that you're
+using ``pyramid_tm`` to configure a transaction manager on the request as
+``request.tm``.
.. code-block:: ini
:linenos:
[pshell]
setup = myapp.lib.pshell.setup
- m = myapp.models
- session = myapp.models.DBSession
- t = transaction
+ models = myapp.models
-By defining the ``setup`` callable, we will create the module
-``myapp.lib.pshell`` containing a callable named ``setup`` that will receive
-the global environment before it is exposed to the shell. Here we mutate the
-environment's request as well as add a new value containing a WebTest version
-of the application to which we can easily submit requests.
+By defining the ``setup`` callable, we will create the module ``myapp.lib.pshell`` containing a callable named ``setup`` that will receive the global environment before it is exposed to the shell. Here we mutate the environment's request as well as add a new value containing a WebTest version of the application to which we can easily submit requests. The ``setup`` callable can also be a generator which can wrap the entire shell lifecycle, executing code when the shell exits.
.. code-block:: python
:linenos:
# myapp/lib/pshell.py
+ from contextlib import suppress
+ from transaction.interfaces import NoTransaction
from webtest import TestApp
def setup(env):
- env['request'].host = 'www.example.com'
- env['request'].scheme = 'https'
+ request = env['request']
+ request.host = 'www.example.com'
+ request.scheme = 'https'
+
env['testapp'] = TestApp(env['app'])
-When this INI file is loaded, the extra variables ``m``, ``session`` and ``t``
-will be available for use immediately. Since a ``setup`` callable was also
-specified, it is executed and a new variable ``testapp`` is exposed, and the
-request is configured to generate urls from the host
-``http://www.example.com``. For example:
+ # start a transaction which can be used in the shell
+ request.tm.begin()
+
+ # if using the alchemy cookiecutter, the dbsession is connected
+ # to the transaction manager above
+ env['tm'] = request.tm
+ env['dbsession'] = request.dbsession
+ try:
+ yield
+
+ finally:
+ with suppress(NoTransaction):
+ request.tm.abort()
+
+When this ``.ini`` file is loaded, the extra variable ``models`` will be available for use immediately. Since a ``setup`` callable was also specified, it is executed and new variables ``testapp``, ``tm``, and ``dbsession`` are exposed, and the request is configured to generate URLs from the host ``http://www.example.com``. For example:
.. code-block:: text
@@ -258,14 +268,21 @@ request is configured to generate urls from the host
testapp <webtest.TestApp object at ...>
Custom Variables:
- m myapp.models
- session myapp.models.DBSession
- t transaction
+ dbsession
+ model myapp.models
+ tm
>>> testapp.get('/')
<200 OK text/html body='<!DOCTYPE...l>\n'/3337>
>>> request.route_url('home')
'https://www.example.com/'
+ >>> user = dbsession.query(models.User).get(1)
+ >>> user.name = 'Joe'
+ >>> tm.commit()
+ >>> tm.begin()
+ >>> user = dbsession.query(models.User).get(1)
+ >>> user.name == 'Joe'
+ 'Joe'
.. _ipython_or_bpython:
diff --git a/docs/quick_tutorial/json/tutorial/__init__.py b/docs/quick_tutorial/json/tutorial/__init__.py
index 6652544c3..8c3ea97ff 100644
--- a/docs/quick_tutorial/json/tutorial/__init__.py
+++ b/docs/quick_tutorial/json/tutorial/__init__.py
@@ -6,6 +6,6 @@ def main(global_config, **settings):
config.include('pyramid_chameleon')
config.add_route('home', '/')
config.add_route('hello', '/howdy')
- config.add_route('hello_json', 'howdy.json')
+ config.add_route('hello_json', '/howdy.json')
config.scan('.views')
- return config.make_wsgi_app() \ No newline at end of file
+ return config.make_wsgi_app()
diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst
index 85977d1be..3f2fcec83 100644
--- a/docs/tutorials/wiki2/authentication.rst
+++ b/docs/tutorials/wiki2/authentication.rst
@@ -122,15 +122,15 @@ Remember our goals:
* Only allow ``editor`` users and the page creator (possibly a ``basic`` user)
to edit pages.
-Open the file ``tutorial/views/default.py`` and fix the following imports:
+Open the file ``tutorial/views/default.py`` and fix the following import:
.. literalinclude:: src/authentication/tutorial/views/default.py
- :lines: 5-13
+ :lines: 5-9
:lineno-match:
- :emphasize-lines: 2,9
+ :emphasize-lines: 2
:language: python
-Change the two highlighted lines.
+Change the highlighted line.
In the same file, now edit the ``edit_page`` view function:
diff --git a/docs/tutorials/wiki2/definingmodels.rst b/docs/tutorials/wiki2/definingmodels.rst
index fd1967d99..c22fd2b61 100644
--- a/docs/tutorials/wiki2/definingmodels.rst
+++ b/docs/tutorials/wiki2/definingmodels.rst
@@ -33,7 +33,7 @@ Open ``tutorial/setup.py`` and edit it to look like the following:
.. literalinclude:: src/models/setup.py
:linenos:
- :emphasize-lines: 12
+ :emphasize-lines: 13
:language: python
Only the highlighted line needs to be added.
@@ -70,7 +70,7 @@ like the following.
.. code-block:: text
- Successfully installed bcrypt-3.1.2 cffi-1.9.1 pycparser-2.17 tutorial
+ Successfully installed bcrypt-3.1.4 cffi-1.11.5 pycparser-2.18 tutorial
Remove ``mymodel.py``
@@ -158,56 +158,76 @@ the following:
Here we align our imports with the names of the models, ``Page`` and ``User``.
-Edit ``scripts/initializedb.py``
-================================
+.. _wiki2_migrate_database_alembic:
-We haven't looked at the details of this file yet, but within the ``scripts``
-directory of your ``tutorial`` package is a file named ``initializedb.py``.
-Code in this file is executed whenever we run the ``initialize_tutorial_db``
-command, as we did in the installation step of this tutorial.
+Migrate the database with Alembic
+=================================
-.. note::
+Now that we have written our models, we need to modify the database schema to reflect the changes to our code. Let's generate a new revision, then upgrade the database to the latest revision (head).
- The command is named ``initialize_tutorial_db`` because of the mapping defined in the ``[console_scripts]`` entry point of our project's ``setup.py`` file.
+On UNIX:
-Since we've changed our model, we need to make changes to our
-``initializedb.py`` script. In particular, we'll replace our import of
-``MyModel`` with those of ``User`` and ``Page``. We'll also change the very end
-of the script to create two ``User`` objects (``basic`` and ``editor``) as well
-as a ``Page``, rather than a ``MyModel``, and add them to our ``dbsession``.
+.. code-block:: bash
-Open ``tutorial/scripts/initializedb.py`` and edit it to look like the
-following:
+ $ $VENV/bin/alembic -c development.ini revision --autogenerate \
+ -m "use new models Page and User"
+ $ $VENV/bin/alembic -c development.ini upgrade head
-.. literalinclude:: src/models/tutorial/scripts/initializedb.py
- :linenos:
- :language: python
- :emphasize-lines: 18,44-57
+On Windows:
-Only the highlighted lines need to be changed.
+.. code-block:: doscon
+ c:\tutorial> %VENV%\Scripts\alembic -c development.ini revision \
+ --autogenerate -m "use new models Page and User"
+ c:\tutorial> %VENV%\Scripts\alembic -c development.ini upgrade head
-Installing the project and re-initializing the database
-=======================================================
+Success executing these commands will generate output similar to the following.
-Because our model has changed, and in order to reinitialize the database, we
-need to rerun the ``initialize_tutorial_db`` command to pick up the changes
-we've made to both the models.py file and to the initializedb.py file. See
-:ref:`initialize_db_wiki2` for instructions.
+.. code-block:: text
-Success will look something like this:
+ 2018-06-29 01:28:42,407 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:28:42,407 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:28:42,408 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:28:42,408 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:28:42,409 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-29 01:28:42,409 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,410 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT alembic_version.version_num
+ FROM alembic_version
+ 2018-06-29 01:28:42,410 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,411 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT name FROM sqlite_master WHERE type='table' ORDER BY name
+ 2018-06-29 01:28:42,412 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,413 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("models")
+ 2018-06-29 01:28:42,413 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'models' AND type = 'table'
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA foreign_key_list("models")
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,414 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'models' AND type = 'table'
+ 2018-06-29 01:28:42,415 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_list("models")
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_info("my_index")
+ 2018-06-29 01:28:42,416 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_list("models")
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA index_info("my_index")
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE name = 'models' AND type = 'table'
+ 2018-06-29 01:28:42,417 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ Generating /<somepath>/tutorial/tutorial/alembic/versions/20180629_23e9f8eb6c28.py ... done
-.. code-block:: bash
+.. code-block:: text
- 2016-12-20 02:51:11,195 INFO [sqlalchemy.engine.base.Engine:1235][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
- 2016-12-20 02:51:11,195 INFO [sqlalchemy.engine.base.Engine:1236][MainThread] ()
- 2016-12-20 02:51:11,195 INFO [sqlalchemy.engine.base.Engine:1235][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
- 2016-12-20 02:51:11,195 INFO [sqlalchemy.engine.base.Engine:1236][MainThread] ()
- 2016-12-20 02:51:11,196 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] PRAGMA table_info("pages")
- 2016-12-20 02:51:11,196 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
- 2016-12-20 02:51:11,196 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] PRAGMA table_info("users")
- 2016-12-20 02:51:11,197 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
- 2016-12-20 02:51:11,197 INFO [sqlalchemy.engine.base.Engine:1140][MainThread]
+ 2018-06-29 01:29:37,957 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:29:37,958 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:29:37,958 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:29:37,958 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT alembic_version.version_num
+ FROM alembic_version
+ 2018-06-29 01:29:37,960 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,963 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
CREATE TABLE users (
id INTEGER NOT NULL,
name TEXT NOT NULL,
@@ -218,30 +238,106 @@ Success will look something like this:
)
- 2016-12-20 02:51:11,197 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
- 2016-12-20 02:51:11,198 INFO [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
- 2016-12-20 02:51:11,199 INFO [sqlalchemy.engine.base.Engine:1140][MainThread]
+ 2018-06-29 01:29:37,963 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,966 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,968 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
CREATE TABLE pages (
id INTEGER NOT NULL,
name TEXT NOT NULL,
data TEXT NOT NULL,
creator_id INTEGER NOT NULL,
CONSTRAINT pk_pages PRIMARY KEY (id),
- CONSTRAINT uq_pages_name UNIQUE (name),
- CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id)
+ CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id),
+ CONSTRAINT uq_pages_name UNIQUE (name)
)
- 2016-12-20 02:51:11,199 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
- 2016-12-20 02:51:11,200 INFO [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
- 2016-12-20 02:51:11,755 INFO [sqlalchemy.engine.base.Engine:679][MainThread] BEGIN (implicit)
- 2016-12-20 02:51:11,755 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
- 2016-12-20 02:51:11,755 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ('editor', 'editor', '$2b$12$ds7h2Zb7.l6TEFup5h8f4ekA9GRfEpE1yQGDRvT9PConw73kKuupG')
- 2016-12-20 02:51:11,756 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
- 2016-12-20 02:51:11,756 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ('basic', 'basic', '$2b$12$KgruXP5Vv7rikr6dGB3TF.flGXYpiE0Li9K583EVomjY.SYmQOsyi')
- 2016-12-20 02:51:11,757 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
- 2016-12-20 02:51:11,757 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ('FrontPage', 'This is the front page', 1)
- 2016-12-20 02:51:11,757 INFO [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
+ 2018-06-29 01:29:37,968 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,969 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,969 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ DROP INDEX my_index
+ 2018-06-29 01:29:37,969 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,970 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,970 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ DROP TABLE models
+ 2018-06-29 01:29:37,970 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,971 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-29 01:29:37,972 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] UPDATE alembic_version SET version_num='23e9f8eb6c28' WHERE alembic_version.version_num = 'b6b22ae3e628'
+ 2018-06-29 01:29:37,972 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-29 01:29:37,972 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+
+
+.. _wiki2_alembic_overview:
+
+Alembic overview
+----------------
+
+Let's briefly discuss our configuration for Alembic.
+
+In the alchemy cookiecutter's ``development.ini`` file, the setting for ``script_location`` configures Alembic to look for the migration script in the directory ``tutorial/alembic``.
+By default Alembic stores the migration files one level deeper in ``tutorial/alembic/versions``.
+These files are generated by Alembic, then executed when we run upgrade or downgrade migrations.
+The setting ``file_template`` provides the format for each migration's file name.
+We've configured the ``file_template`` setting to make it somewhat easy to find migrations by file name.
+
+At this point in this tutorial, we have two migration files.
+Examine them to see what Alembic will do when you upgrade or downgrade the database to a specific revision.
+Notice the revision identifiers and how they relate to one another in a chained sequence.
+
+.. seealso:: For further information, see the `Alembic documentation <http://alembic.zzzcomputing.com/en/latest/>`_.
+
+
+Edit ``scripts/initialize_db.py``
+=================================
+
+We haven't looked at the details of this file yet, but within the ``scripts``
+directory of your ``tutorial`` package is a file named ``initialize_db.py``.
+Code in this file is executed whenever we run the ``initialize_tutorial_db``
+command, as we did in the installation step of this tutorial.
+
+.. note::
+
+ The command is named ``initialize_tutorial_db`` because of the mapping defined in the ``[console_scripts]`` entry point of our project's ``setup.py`` file.
+
+Since we've changed our model, we need to make changes to our
+``initialize_db.py`` script. In particular, we'll replace our import of
+``MyModel`` with those of ``User`` and ``Page``. We'll also change the the script to create two ``User`` objects (``basic`` and ``editor``) as well
+as a ``Page``, rather than a ``MyModel``, and add them to our ``dbsession``.
+
+Open ``tutorial/scripts/initialize_db.py`` and edit it to look like the
+following:
+
+.. literalinclude:: src/models/tutorial/scripts/initialize_db.py
+ :linenos:
+ :language: python
+ :emphasize-lines: 11-24
+
+Only the highlighted lines need to be changed.
+
+
+Populating the database
+=======================
+
+Because our model has changed, and to repopulate the database, we
+need to rerun the ``initialize_tutorial_db`` command to pick up the changes
+we've made to the initialize_db.py file. See :ref:`initialize_db_wiki2` for instructions.
+
+Success will look something like this:
+
+.. code-block:: text
+
+ 2018-06-29 01:30:39,326 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:30:39,326 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:30:39,327 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-29 01:30:39,327 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-29 01:30:39,328 INFO [sqlalchemy.engine.base.Engine:682][MainThread] BEGIN (implicit)
+ 2018-06-29 01:30:39,329 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2018-06-29 01:30:39,329 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('editor', 'editor', '$2b$12$PlaJSN7goVbyx8OFs8yAju9n5gHGdI6PZ2QRJGM2jDCiEU4ItUNxy')
+ 2018-06-29 01:30:39,330 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
+ 2018-06-29 01:30:39,330 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('basic', 'basic', '$2b$12$MvXdM8jlkbjEyPZ6uXzRg.yatZZK8jCwfPaM7kFkmVJiJjRoCCvmW')
+ 2018-06-29 01:30:39,331 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
+ 2018-06-29 01:30:39,331 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('FrontPage', 'This is the front page', 1)
+ 2018-06-29 01:30:39,332 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
View the application in a browser
@@ -250,11 +346,11 @@ View the application in a browser
We can't. At this point, our system is in a "non-runnable" state; we'll need
to change view-related files in the next chapter to be able to start the
application successfully. If you try to start the application (see
-:ref:`wiki2-start-the-application`), you'll wind up with a Python traceback on
+:ref:`wiki2-start-the-application`) and visit http://localhost:6543, you'll wind up with a Python traceback on
your console that ends with this exception:
.. code-block:: text
- ImportError: cannot import name MyModel
+ AttributeError: module 'tutorial.models' has no attribute 'MyModel'
This will also happen if you attempt to run the tests.
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst
index 3c343e155..2eb78cb00 100644
--- a/docs/tutorials/wiki2/definingviews.rst
+++ b/docs/tutorials/wiki2/definingviews.rst
@@ -126,7 +126,7 @@ edit it to look like the following:
.. literalinclude:: src/views/tutorial/views/default.py
:linenos:
:language: python
- :emphasize-lines: 1-9,12-
+ :emphasize-lines: 1-9,14-
The highlighted lines need to be added or edited.
diff --git a/docs/tutorials/wiki2/design.rst b/docs/tutorials/wiki2/design.rst
index 515aff276..e3b35d24a 100644
--- a/docs/tutorials/wiki2/design.rst
+++ b/docs/tutorials/wiki2/design.rst
@@ -19,7 +19,7 @@ Models
======
We'll be using an SQLite database to hold our wiki data, and we'll be using
-:term:`SQLAlchemy` to access the data in this database.
+:term:`SQLAlchemy` to access the data in this database. We will also use :term:`Alembic` for database migrations, including initialization of the SQLite database.
Within the database, we will define two tables:
diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst
index 5a935f7fc..9b89327cb 100644
--- a/docs/tutorials/wiki2/installation.rst
+++ b/docs/tutorials/wiki2/installation.rst
@@ -198,6 +198,143 @@ Testing requirements are defined in our project's ``setup.py`` file, in the ``te
:lines: 48-50
+.. _initialize_db_wiki2:
+
+Initialize and upgrade the database using Alembic
+-------------------------------------------------
+
+We use :term:`Alembic` to manage our database initialization and migrations.
+
+Generate your first revision.
+
+On UNIX
+^^^^^^^
+
+.. code-block:: bash
+
+ $ $VENV/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ c:\tutorial> %VENV%\Scripts\alembic -c development.ini revision --autogenerate -m "init"
+
+The output to your console should be something like this:
+
+.. code-block:: text
+
+ 2018-06-22 17:57:31,587 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:31,587 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:31,588 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:31,588 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:31,589 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-22 17:57:31,589 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:31,590 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-22 17:57:31,590 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:31,590 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ CREATE TABLE alembic_version (
+ version_num VARCHAR(32) NOT NULL,
+ CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num)
+ )
+
+
+ 2018-06-22 17:57:31,591 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:31,591 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-22 17:57:31,594 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT name FROM sqlite_master WHERE type='table' ORDER BY name
+ 2018-06-22 17:57:31,594 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ Generating /<somepath>/tutorial/alembic/versions/20180622_bab5a278ce04.py ... done
+
+Upgrade to that revision.
+
+On UNIX
+^^^^^^^
+
+.. code-block:: bash
+
+ $ $VENV/bin/alembic -c development.ini upgrade head
+
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ c:\tutorial> %VENV%\Scripts\alembic -c development.ini upgrade head
+
+The output to your console should be something like this:
+
+.. code-block:: text
+
+ 2018-06-22 17:57:37,814 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:37,814 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:37,814 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:37,814 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:37,816 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-22 17:57:37,816 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,817 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] SELECT alembic_version.version_num
+ FROM alembic_version
+ 2018-06-22 17:57:37,817 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,817 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] PRAGMA table_info("alembic_version")
+ 2018-06-22 17:57:37,817 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,819 INFO [sqlalchemy.engine.base.Engine:1151][MainThread]
+ CREATE TABLE models (
+ id INTEGER NOT NULL,
+ name TEXT,
+ value INTEGER,
+ CONSTRAINT pk_models PRIMARY KEY (id)
+ )
+
+
+ 2018-06-22 17:57:37,820 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,822 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-22 17:57:37,824 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] CREATE UNIQUE INDEX my_index ON models (name)
+ 2018-06-22 17:57:37,824 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,825 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+ 2018-06-22 17:57:37,825 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO alembic_version (version_num) VALUES ('bab5a278ce04')
+ 2018-06-22 17:57:37,825 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ()
+ 2018-06-22 17:57:37,825 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+
+
+.. _load_data_wiki2:
+
+Load default data
+-----------------
+
+Load default data into the database using a :term:`console script`. Type the following command, making sure you are still in the ``tutorial`` directory (the directory with a ``development.ini`` in it):
+
+On UNIX
+^^^^^^^
+
+.. code-block:: bash
+
+ $ $VENV/bin/initialize_tutorial_db development.ini
+
+On Windows
+^^^^^^^^^^
+
+.. code-block:: doscon
+
+ c:\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini
+
+The output to your console should be something like this:
+
+.. code-block:: bash
+
+ 2018-06-22 17:57:46,241 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:46,241 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:46,242 INFO [sqlalchemy.engine.base.Engine:1254][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
+ 2018-06-22 17:57:46,242 INFO [sqlalchemy.engine.base.Engine:1255][MainThread] ()
+ 2018-06-22 17:57:46,243 INFO [sqlalchemy.engine.base.Engine:682][MainThread] BEGIN (implicit)
+ 2018-06-22 17:57:46,244 INFO [sqlalchemy.engine.base.Engine:1151][MainThread] INSERT INTO models (name, value) VALUES (?, ?)
+ 2018-06-22 17:57:46,245 INFO [sqlalchemy.engine.base.Engine:1154][MainThread] ('one', 1)
+ 2018-06-22 17:57:46,246 INFO [sqlalchemy.engine.base.Engine:722][MainThread] COMMIT
+
+Success! You should now have a ``tutorial.sqlite`` file in your current
+working directory. This is an SQLite database with a single table defined in it
+(``models``) and single record inside of that.
+
+
.. _sql_running_tests:
Run the tests
@@ -260,27 +397,28 @@ If successful, you will see output something like this:
.. code-block:: bash
======================== test session starts ========================
- platform Python 3.6.0, pytest-3.0.5, py-1.4.31, pluggy-0.4.0
- rootdir: /Users/stevepiercy/tutorial, inifile:
- plugins: cov-2.4.0
+ platform Python 3.6.5, pytest-3.6.2, py-1.5.3, pluggy-0.6.0
+ rootdir: /<somepath>/tutorial, inifile: pytest.ini
+ plugins: cov-2.5.1
collected 2 items
tutorial/tests.py ..
- ------------------ coverage: platform Python 3.6.0 ------------------
- Name Stmts Miss Cover Missing
- ----------------------------------------------------------------
- tutorial/__init__.py 8 6 25% 7-12
- tutorial/models/__init__.py 22 0 100%
- tutorial/models/meta.py 5 0 100%
- tutorial/models/mymodel.py 8 0 100%
- tutorial/routes.py 3 2 33% 2-3
- tutorial/scripts/__init__.py 0 0 100%
- tutorial/scripts/initializedb.py 26 16 38% 22-25, 29-45
- tutorial/views/__init__.py 0 0 100%
- tutorial/views/default.py 12 0 100%
- tutorial/views/notfound.py 4 2 50% 6-7
- ----------------------------------------------------------------
- TOTAL 88 26 70%
+ ------------------ coverage: platform Python 3.6.5 ------------------
+ Name Stmts Miss Cover Missing
+ -----------------------------------------------------------------
+ tutorial/__init__.py 8 6 25% 7-12
+ tutorial/models/__init__.py 24 0 100%
+ tutorial/models/meta.py 5 0 100%
+ tutorial/models/mymodel.py 8 0 100%
+ tutorial/routes.py 3 3 0% 1-3
+ tutorial/scripts/__init__.py 0 0 100%
+ tutorial/scripts/initialize_db.py 24 24 0% 1-34
+ tutorial/views/__init__.py 0 0 100%
+ tutorial/views/default.py 12 0 100%
+ tutorial/views/notfound.py 4 4 0% 1-7
+ -----------------------------------------------------------------
+ TOTAL 88 37 58%
+
===================== 2 passed in 0.57 seconds ======================
Our package doesn't quite have 100% test coverage.
@@ -319,71 +457,6 @@ coverage.
``py.test -h`` to see its full set of options.
-.. _initialize_db_wiki2:
-
-Initializing the database
--------------------------
-
-We need to use the ``initialize_tutorial_db`` :term:`console script` to
-initialize our database.
-
-.. note::
-
- The ``initialize_tutorial_db`` command does not perform a migration, but
- rather it simply creates missing tables and adds some dummy data. If you
- already have a database, you should delete it before running
- ``initialize_tutorial_db`` again.
-
-Type the following command, making sure you are still in the ``tutorial``
-directory (the directory with a ``development.ini`` in it):
-
-On UNIX
-^^^^^^^
-
-.. code-block:: bash
-
- $ $VENV/bin/initialize_tutorial_db development.ini
-
-On Windows
-^^^^^^^^^^
-
-.. code-block:: doscon
-
- c:\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini
-
-The output to your console should be something like this:
-
-.. code-block:: bash
-
- 2016-12-18 21:30:08,675 INFO [sqlalchemy.engine.base.Engine:1235][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
- 2016-12-18 21:30:08,675 INFO [sqlalchemy.engine.base.Engine:1236][MainThread] ()
- 2016-12-18 21:30:08,676 INFO [sqlalchemy.engine.base.Engine:1235][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
- 2016-12-18 21:30:08,676 INFO [sqlalchemy.engine.base.Engine:1236][MainThread] ()
- 2016-12-18 21:30:08,676 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] PRAGMA table_info("models")
- 2016-12-18 21:30:08,676 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
- 2016-12-18 21:30:08,677 INFO [sqlalchemy.engine.base.Engine:1140][MainThread]
- CREATE TABLE models (
- id INTEGER NOT NULL,
- name TEXT,
- value INTEGER,
- CONSTRAINT pk_models PRIMARY KEY (id)
- )
-
-
- 2016-12-18 21:30:08,677 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
- 2016-12-18 21:30:08,678 INFO [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
- 2016-12-18 21:30:08,679 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] CREATE UNIQUE INDEX my_index ON models (name)
- 2016-12-18 21:30:08,679 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ()
- 2016-12-18 21:30:08,679 INFO [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
- 2016-12-18 21:30:08,681 INFO [sqlalchemy.engine.base.Engine:679][MainThread] BEGIN (implicit)
- 2016-12-18 21:30:08,682 INFO [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO models (name, value) VALUES (?, ?)
- 2016-12-18 21:30:08,682 INFO [sqlalchemy.engine.base.Engine:1143][MainThread] ('one', 1)
- 2016-12-18 21:30:08,682 INFO [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
-
-Success! You should now have a ``tutorial.sqlite`` file in your current
-working directory. This is an SQLite database with a single table defined in it
-(``models``).
-
.. _wiki2-start-the-application:
Start the application
@@ -445,6 +518,10 @@ assumptions:
- You are willing to use :term:`SQLAlchemy` for a database access tool.
+- You are willing to use :term:`Alembic` for a database migrations tool.
+
+- You are willing to use a :term:`console script` for a data loading tool.
+
- You are willing to use :term:`URL dispatch` to map URLs to code.
- You want to use zope.sqlalchemy_, pyramid_tm_, and the transaction_ packages
diff --git a/docs/tutorials/wiki2/src/authentication/.gitignore b/docs/tutorials/wiki2/src/authentication/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/authentication/README.txt b/docs/tutorials/wiki2/src/authentication/README.txt
index 7b33da610..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/authentication/README.txt
+++ b/docs/tutorials/wiki2/src/authentication/README.txt
@@ -20,7 +20,17 @@ Getting Started
env/bin/pip install -e ".[testing]"
-- Configure the database.
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
env/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/authentication/development.ini b/docs/tutorials/wiki2/src/authentication/development.ini
index cc2a5586e..d76a6cd72 100644
--- a/docs/tutorials/wiki2/src/authentication/development.ini
+++ b/docs/tutorials/wiki2/src/authentication/development.ini
@@ -28,6 +28,12 @@ auth.secret = seekrit
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = localhost:6543
diff --git a/docs/tutorials/wiki2/src/authentication/production.ini b/docs/tutorials/wiki2/src/authentication/production.ini
index 759807abf..c46adb7b5 100644
--- a/docs/tutorials/wiki2/src/authentication/production.ini
+++ b/docs/tutorials/wiki2/src/authentication/production.ini
@@ -22,6 +22,12 @@ auth.secret = real-seekrit
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = *:6543
diff --git a/docs/tutorials/wiki2/src/authentication/pytest.ini b/docs/tutorials/wiki2/src/authentication/pytest.ini
index 8b76bc410..a3489cdf8 100644
--- a/docs/tutorials/wiki2/src/authentication/pytest.ini
+++ b/docs/tutorials/wiki2/src/authentication/pytest.ini
@@ -1,3 +1,3 @@
[pytest]
testpaths = tutorial
-python_files = *.py
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/authentication/setup.py b/docs/tutorials/wiki2/src/authentication/setup.py
index abc24876d..28b766cbe 100644
--- a/docs/tutorials/wiki2/src/authentication/setup.py
+++ b/docs/tutorials/wiki2/src/authentication/setup.py
@@ -9,10 +9,11 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'alembic',
'bcrypt',
'docutils',
'plaster_pastedeploy',
- 'pyramid >= 1.9a',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
'pyramid_jinja2',
'pyramid_retry',
@@ -56,7 +57,7 @@ setup(
'main = tutorial:main',
],
'console_scripts': [
- 'initialize_tutorial_db = tutorial.scripts.initializedb:main',
+ 'initialize_tutorial_db = tutorial.scripts.initialize_db:main',
],
},
)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/alembic/env.py b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
index 3c9ba8e54..a4209a6e9 100644
--- a/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # noqa
-from .user import User # noqa
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..231d5d44b
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import os
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri>\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) != 2:
+ usage(argv)
+ config_uri = argv[1]
+ setup_logging(config_uri)
+ env = bootstrap(config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+Pyramid is having a problem using your SQL database. The problem
+might be caused by one of the following things:
+
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
+
+2. Your database server may not be running. Check that the
+ database server referred to by the "sqlalchemy.url" setting in
+ your "development.ini" file is running.
+ ''')
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py
deleted file mode 100644
index f3c0a6fef..000000000
--- a/docs/tutorials/wiki2/src/authentication/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import os
-import sys
-import transaction
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from pyramid.scripts.common import parse_vars
-
-from ..models.meta import Base
-from ..models import (
- get_engine,
- get_session_factory,
- get_tm_session,
- )
-from ..models import Page, User
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri> [var=value]\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) < 2:
- usage(argv)
- config_uri = argv[1]
- options = parse_vars(argv[2:])
- setup_logging(config_uri)
- settings = get_appsettings(config_uri, options=options)
-
- engine = get_engine(settings)
- Base.metadata.create_all(engine)
-
- session_factory = get_session_factory(engine)
-
- with transaction.manager:
- dbsession = get_tm_session(session_factory, transaction.manager)
-
- editor = User(name='editor', role='editor')
- editor.set_password('editor')
- dbsession.add(editor)
-
- basic = User(name='basic', role='basic')
- basic.set_password('basic')
- dbsession.add(basic)
-
- page = Page(
- name='FrontPage',
- creator=editor,
- data='This is the front page',
- )
- dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
index b98ccbd95..4016b26c9 100644
--- a/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/templates/layout.jinja2
@@ -29,7 +29,7 @@
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png') }}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
diff --git a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
index 2d058d874..8ed90d5b2 100644
--- a/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/authentication/tutorial/views/default.py
@@ -10,7 +10,7 @@ from pyramid.httpexceptions import (
from pyramid.view import view_config
-from ..models import Page
+from .. import models
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
@@ -23,13 +23,13 @@ def view_wiki(request):
@view_config(route_name='view_page', renderer='../templates/view.jinja2')
def view_page(request):
pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ page = request.dbsession.query(models.Page).filter_by(name=pagename).first()
if page is None:
raise HTTPNotFound('No such page')
def add_link(match):
word = match.group(1)
- exists = request.dbsession.query(Page).filter_by(name=word).all()
+ exists = request.dbsession.query(models.Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
return '<a href="%s">%s</a>' % (view_url, escape(word))
@@ -45,7 +45,7 @@ def view_page(request):
@view_config(route_name='edit_page', renderer='../templates/edit.jinja2')
def edit_page(request):
pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).one()
+ page = request.dbsession.query(models.Page).filter_by(name=pagename).one()
user = request.user
if user is None or (user.role != 'editor' and page.creator != user):
raise HTTPForbidden
@@ -65,12 +65,12 @@ def add_page(request):
if user is None or user.role not in ('editor', 'basic'):
raise HTTPForbidden
pagename = request.matchdict['pagename']
- if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0:
next_url = request.route_url('edit_page', pagename=pagename)
return HTTPFound(location=next_url)
if 'form.submitted' in request.params:
body = request.params['body']
- page = Page(name=pagename, data=body)
+ page = models.Page(name=pagename, data=body)
page.creator = request.user
request.dbsession.add(page)
next_url = request.route_url('view_page', pagename=pagename)
diff --git a/docs/tutorials/wiki2/src/authorization/.gitignore b/docs/tutorials/wiki2/src/authorization/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/authorization/README.txt b/docs/tutorials/wiki2/src/authorization/README.txt
index 7b33da610..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/authorization/README.txt
+++ b/docs/tutorials/wiki2/src/authorization/README.txt
@@ -20,7 +20,17 @@ Getting Started
env/bin/pip install -e ".[testing]"
-- Configure the database.
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
env/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/authorization/development.ini b/docs/tutorials/wiki2/src/authorization/development.ini
index cc2a5586e..d76a6cd72 100644
--- a/docs/tutorials/wiki2/src/authorization/development.ini
+++ b/docs/tutorials/wiki2/src/authorization/development.ini
@@ -28,6 +28,12 @@ auth.secret = seekrit
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = localhost:6543
diff --git a/docs/tutorials/wiki2/src/authorization/production.ini b/docs/tutorials/wiki2/src/authorization/production.ini
index 759807abf..c46adb7b5 100644
--- a/docs/tutorials/wiki2/src/authorization/production.ini
+++ b/docs/tutorials/wiki2/src/authorization/production.ini
@@ -22,6 +22,12 @@ auth.secret = real-seekrit
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = *:6543
diff --git a/docs/tutorials/wiki2/src/authorization/pytest.ini b/docs/tutorials/wiki2/src/authorization/pytest.ini
index 8b76bc410..a3489cdf8 100644
--- a/docs/tutorials/wiki2/src/authorization/pytest.ini
+++ b/docs/tutorials/wiki2/src/authorization/pytest.ini
@@ -1,3 +1,3 @@
[pytest]
testpaths = tutorial
-python_files = *.py
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/authorization/setup.py b/docs/tutorials/wiki2/src/authorization/setup.py
index abc24876d..28b766cbe 100644
--- a/docs/tutorials/wiki2/src/authorization/setup.py
+++ b/docs/tutorials/wiki2/src/authorization/setup.py
@@ -9,10 +9,11 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'alembic',
'bcrypt',
'docutils',
'plaster_pastedeploy',
- 'pyramid >= 1.9a',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
'pyramid_jinja2',
'pyramid_retry',
@@ -56,7 +57,7 @@ setup(
'main = tutorial:main',
],
'console_scripts': [
- 'initialize_tutorial_db = tutorial.scripts.initializedb:main',
+ 'initialize_tutorial_db = tutorial.scripts.initialize_db:main',
],
},
)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/alembic/env.py b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
index 3c9ba8e54..a4209a6e9 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # noqa
-from .user import User # noqa
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
index f0a8b7f96..1fd45a994 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/routes.py
@@ -7,7 +7,7 @@ from pyramid.security import (
Everyone,
)
-from .models import Page
+from . import models
def includeme(config):
config.add_static_view('static', 'static', cache_max_age=3600)
@@ -22,7 +22,7 @@ def includeme(config):
def new_page_factory(request):
pagename = request.matchdict['pagename']
- if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0:
next_url = request.route_url('edit_page', pagename=pagename)
raise HTTPFound(location=next_url)
return NewPage(pagename)
@@ -39,7 +39,7 @@ class NewPage(object):
def page_factory(request):
pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ page = request.dbsession.query(models.Page).filter_by(name=pagename).first()
if page is None:
raise HTTPNotFound
return PageResource(page)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..231d5d44b
--- /dev/null
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import os
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri>\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) != 2:
+ usage(argv)
+ config_uri = argv[1]
+ setup_logging(config_uri)
+ env = bootstrap(config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+Pyramid is having a problem using your SQL database. The problem
+might be caused by one of the following things:
+
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
+
+2. Your database server may not be running. Check that the
+ database server referred to by the "sqlalchemy.url" setting in
+ your "development.ini" file is running.
+ ''')
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
deleted file mode 100644
index f3c0a6fef..000000000
--- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import os
-import sys
-import transaction
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from pyramid.scripts.common import parse_vars
-
-from ..models.meta import Base
-from ..models import (
- get_engine,
- get_session_factory,
- get_tm_session,
- )
-from ..models import Page, User
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri> [var=value]\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) < 2:
- usage(argv)
- config_uri = argv[1]
- options = parse_vars(argv[2:])
- setup_logging(config_uri)
- settings = get_appsettings(config_uri, options=options)
-
- engine = get_engine(settings)
- Base.metadata.create_all(engine)
-
- session_factory = get_session_factory(engine)
-
- with transaction.manager:
- dbsession = get_tm_session(session_factory, transaction.manager)
-
- editor = User(name='editor', role='editor')
- editor.set_password('editor')
- dbsession.add(editor)
-
- basic = User(name='basic', role='basic')
- basic.set_password('basic')
- dbsession.add(basic)
-
- page = Page(
- name='FrontPage',
- creator=editor,
- data='This is the front page',
- )
- dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/security.py b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
index 25cff7b05..1ce1c8753 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/security.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/security.py
@@ -5,7 +5,7 @@ from pyramid.security import (
Everyone,
)
-from .models import User
+from . import models
class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
@@ -26,7 +26,7 @@ class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
def get_user(request):
user_id = request.unauthenticated_userid
if user_id is not None:
- user = request.dbsession.query(User).get(user_id)
+ user = request.dbsession.query(models.User).get(user_id)
return user
def includeme(config):
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
index b98ccbd95..4016b26c9 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/templates/layout.jinja2
@@ -29,7 +29,7 @@
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png') }}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
index 99e95efd3..ce650ca7c 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py
@@ -54,7 +54,7 @@ class TestMyViewSuccessCondition(BaseTest):
from .views.default import my_view
info = my_view(dummy_request(self.session))
self.assertEqual(info['one'].name, 'one')
- self.assertEqual(info['project'], 'tutorial')
+ self.assertEqual(info['project'], 'myproj')
class TestMyViewFailureCondition(BaseTest):
diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
index 65c12ed3b..ad271fb46 100644
--- a/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/authorization/tutorial/views/default.py
@@ -5,7 +5,7 @@ from docutils.core import publish_parts
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
-from ..models import Page
+from .. import models
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
@@ -22,7 +22,7 @@ def view_page(request):
def add_link(match):
word = match.group(1)
- exists = request.dbsession.query(Page).filter_by(name=word).all()
+ exists = request.dbsession.query(models.Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
return '<a href="%s">%s</a>' % (view_url, escape(word))
@@ -55,7 +55,7 @@ def add_page(request):
pagename = request.context.pagename
if 'form.submitted' in request.params:
body = request.params['body']
- page = Page(name=pagename, data=body)
+ page = models.Page(name=pagename, data=body)
page.creator = request.user
request.dbsession.add(page)
next_url = request.route_url('view_page', pagename=pagename)
diff --git a/docs/tutorials/wiki2/src/basiclayout/.gitignore b/docs/tutorials/wiki2/src/basiclayout/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/basiclayout/README.txt b/docs/tutorials/wiki2/src/basiclayout/README.txt
index 7b33da610..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/basiclayout/README.txt
+++ b/docs/tutorials/wiki2/src/basiclayout/README.txt
@@ -20,7 +20,17 @@ Getting Started
env/bin/pip install -e ".[testing]"
-- Configure the database.
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
env/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/basiclayout/development.ini b/docs/tutorials/wiki2/src/basiclayout/development.ini
index 4a67896b2..ee050c0ea 100644
--- a/docs/tutorials/wiki2/src/basiclayout/development.ini
+++ b/docs/tutorials/wiki2/src/basiclayout/development.ini
@@ -26,6 +26,12 @@ retry.attempts = 3
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = localhost:6543
diff --git a/docs/tutorials/wiki2/src/basiclayout/production.ini b/docs/tutorials/wiki2/src/basiclayout/production.ini
index a28e47a83..91d0f5ddb 100644
--- a/docs/tutorials/wiki2/src/basiclayout/production.ini
+++ b/docs/tutorials/wiki2/src/basiclayout/production.ini
@@ -20,6 +20,12 @@ retry.attempts = 3
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = *:6543
diff --git a/docs/tutorials/wiki2/src/basiclayout/pytest.ini b/docs/tutorials/wiki2/src/basiclayout/pytest.ini
index 8b76bc410..a3489cdf8 100644
--- a/docs/tutorials/wiki2/src/basiclayout/pytest.ini
+++ b/docs/tutorials/wiki2/src/basiclayout/pytest.ini
@@ -1,3 +1,3 @@
[pytest]
testpaths = tutorial
-python_files = *.py
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/basiclayout/setup.py b/docs/tutorials/wiki2/src/basiclayout/setup.py
index 9fc5519a5..e0cc964fe 100644
--- a/docs/tutorials/wiki2/src/basiclayout/setup.py
+++ b/docs/tutorials/wiki2/src/basiclayout/setup.py
@@ -9,8 +9,9 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'alembic',
'plaster_pastedeploy',
- 'pyramid >= 1.9a',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
'pyramid_jinja2',
'pyramid_retry',
@@ -54,7 +55,7 @@ setup(
'main = tutorial:main',
],
'console_scripts': [
- 'initialize_tutorial_db = tutorial.scripts.initializedb:main',
+ 'initialize_tutorial_db = tutorial.scripts.initialize_db:main',
],
},
)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/env.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..b882f9bf7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initialize_db.py
@@ -0,0 +1,44 @@
+import os
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ model = models.mymodel.MyModel(name='one', value=1)
+ dbsession.add(model)
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri>\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) != 2:
+ usage(argv)
+ config_uri = argv[1]
+ setup_logging(config_uri)
+ env = bootstrap(config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+Pyramid is having a problem using your SQL database. The problem
+might be caused by one of the following things:
+
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
+
+2. Your database server may not be running. Check that the
+ database server referred to by the "sqlalchemy.url" setting in
+ your "development.ini" file is running.
+ ''')
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
deleted file mode 100644
index 7307ecc5c..000000000
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import os
-import sys
-import transaction
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from pyramid.scripts.common import parse_vars
-
-from ..models.meta import Base
-from ..models import (
- get_engine,
- get_session_factory,
- get_tm_session,
- )
-from ..models import MyModel
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri> [var=value]\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) < 2:
- usage(argv)
- config_uri = argv[1]
- options = parse_vars(argv[2:])
- setup_logging(config_uri)
- settings = get_appsettings(config_uri, options=options)
-
- engine = get_engine(settings)
- Base.metadata.create_all(engine)
-
- session_factory = get_session_factory(engine)
-
- with transaction.manager:
- dbsession = get_tm_session(session_factory, transaction.manager)
-
- model = MyModel(name='one', value=1)
- dbsession.add(model)
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2
index 26d72c0a6..d8b0a4232 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/templates/mytemplate.jinja2
@@ -5,4 +5,4 @@
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy project</span></h1>
<p class="lead">Welcome to <span class="font-normal">{{project}}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
</div>
-{% endblock content %}
+{% endblock content %} \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
index a404d4154..ef69ff895 100644
--- a/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/basiclayout/tutorial/views/default.py
@@ -3,14 +3,14 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from ..models import MyModel
+from .. import models
@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
- query = request.dbsession.query(MyModel)
- one = query.filter(MyModel.name == 'one').first()
+ query = request.dbsession.query(models.MyModel)
+ one = query.filter(models.MyModel.name == 'one').first()
except DBAPIError:
return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'myproj'}
@@ -20,9 +20,8 @@ db_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
-1. You may need to run the "initialize_tutorial_db" script
- to initialize your database tables. Check your virtual
- environment's "bin" directory for this script and try to run it.
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
2. Your database server may not be running. Check that the
database server referred to by the "sqlalchemy.url" setting in
diff --git a/docs/tutorials/wiki2/src/installation/.gitignore b/docs/tutorials/wiki2/src/installation/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/installation/README.txt b/docs/tutorials/wiki2/src/installation/README.txt
index 7b33da610..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/installation/README.txt
+++ b/docs/tutorials/wiki2/src/installation/README.txt
@@ -20,7 +20,17 @@ Getting Started
env/bin/pip install -e ".[testing]"
-- Configure the database.
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
env/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/installation/development.ini b/docs/tutorials/wiki2/src/installation/development.ini
index 4a67896b2..ee050c0ea 100644
--- a/docs/tutorials/wiki2/src/installation/development.ini
+++ b/docs/tutorials/wiki2/src/installation/development.ini
@@ -26,6 +26,12 @@ retry.attempts = 3
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = localhost:6543
diff --git a/docs/tutorials/wiki2/src/installation/production.ini b/docs/tutorials/wiki2/src/installation/production.ini
index a28e47a83..91d0f5ddb 100644
--- a/docs/tutorials/wiki2/src/installation/production.ini
+++ b/docs/tutorials/wiki2/src/installation/production.ini
@@ -20,6 +20,12 @@ retry.attempts = 3
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = *:6543
diff --git a/docs/tutorials/wiki2/src/installation/pytest.ini b/docs/tutorials/wiki2/src/installation/pytest.ini
index 8b76bc410..a3489cdf8 100644
--- a/docs/tutorials/wiki2/src/installation/pytest.ini
+++ b/docs/tutorials/wiki2/src/installation/pytest.ini
@@ -1,3 +1,3 @@
[pytest]
testpaths = tutorial
-python_files = *.py
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/installation/setup.py b/docs/tutorials/wiki2/src/installation/setup.py
index 9fc5519a5..e0cc964fe 100644
--- a/docs/tutorials/wiki2/src/installation/setup.py
+++ b/docs/tutorials/wiki2/src/installation/setup.py
@@ -9,8 +9,9 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'alembic',
'plaster_pastedeploy',
- 'pyramid >= 1.9a',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
'pyramid_jinja2',
'pyramid_retry',
@@ -54,7 +55,7 @@ setup(
'main = tutorial:main',
],
'console_scripts': [
- 'initialize_tutorial_db = tutorial.scripts.initializedb:main',
+ 'initialize_tutorial_db = tutorial.scripts.initialize_db:main',
],
},
)
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/alembic/env.py b/docs/tutorials/wiki2/src/installation/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/installation/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..b882f9bf7
--- /dev/null
+++ b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initialize_db.py
@@ -0,0 +1,44 @@
+import os
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ model = models.mymodel.MyModel(name='one', value=1)
+ dbsession.add(model)
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri>\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) != 2:
+ usage(argv)
+ config_uri = argv[1]
+ setup_logging(config_uri)
+ env = bootstrap(config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+Pyramid is having a problem using your SQL database. The problem
+might be caused by one of the following things:
+
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
+
+2. Your database server may not be running. Check that the
+ database server referred to by the "sqlalchemy.url" setting in
+ your "development.ini" file is running.
+ ''')
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py
deleted file mode 100644
index 7307ecc5c..000000000
--- a/docs/tutorials/wiki2/src/installation/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import os
-import sys
-import transaction
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from pyramid.scripts.common import parse_vars
-
-from ..models.meta import Base
-from ..models import (
- get_engine,
- get_session_factory,
- get_tm_session,
- )
-from ..models import MyModel
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri> [var=value]\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) < 2:
- usage(argv)
- config_uri = argv[1]
- options = parse_vars(argv[2:])
- setup_logging(config_uri)
- settings = get_appsettings(config_uri, options=options)
-
- engine = get_engine(settings)
- Base.metadata.create_all(engine)
-
- session_factory = get_session_factory(engine)
-
- with transaction.manager:
- dbsession = get_tm_session(session_factory, transaction.manager)
-
- model = MyModel(name='one', value=1)
- dbsession.add(model)
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2
index 26d72c0a6..d8b0a4232 100644
--- a/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2
+++ b/docs/tutorials/wiki2/src/installation/tutorial/templates/mytemplate.jinja2
@@ -5,4 +5,4 @@
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy project</span></h1>
<p class="lead">Welcome to <span class="font-normal">{{project}}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
</div>
-{% endblock content %}
+{% endblock content %} \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/installation/tutorial/views/default.py b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
index a404d4154..ef69ff895 100644
--- a/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/installation/tutorial/views/default.py
@@ -3,14 +3,14 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from ..models import MyModel
+from .. import models
@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
- query = request.dbsession.query(MyModel)
- one = query.filter(MyModel.name == 'one').first()
+ query = request.dbsession.query(models.MyModel)
+ one = query.filter(models.MyModel.name == 'one').first()
except DBAPIError:
return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'myproj'}
@@ -20,9 +20,8 @@ db_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
-1. You may need to run the "initialize_tutorial_db" script
- to initialize your database tables. Check your virtual
- environment's "bin" directory for this script and try to run it.
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
2. Your database server may not be running. Check that the
database server referred to by the "sqlalchemy.url" setting in
diff --git a/docs/tutorials/wiki2/src/models/.gitignore b/docs/tutorials/wiki2/src/models/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/models/README.txt b/docs/tutorials/wiki2/src/models/README.txt
index 7b33da610..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/models/README.txt
+++ b/docs/tutorials/wiki2/src/models/README.txt
@@ -20,7 +20,17 @@ Getting Started
env/bin/pip install -e ".[testing]"
-- Configure the database.
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
env/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/models/development.ini b/docs/tutorials/wiki2/src/models/development.ini
index 4a67896b2..ee050c0ea 100644
--- a/docs/tutorials/wiki2/src/models/development.ini
+++ b/docs/tutorials/wiki2/src/models/development.ini
@@ -26,6 +26,12 @@ retry.attempts = 3
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = localhost:6543
diff --git a/docs/tutorials/wiki2/src/models/production.ini b/docs/tutorials/wiki2/src/models/production.ini
index a28e47a83..91d0f5ddb 100644
--- a/docs/tutorials/wiki2/src/models/production.ini
+++ b/docs/tutorials/wiki2/src/models/production.ini
@@ -20,6 +20,12 @@ retry.attempts = 3
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = *:6543
diff --git a/docs/tutorials/wiki2/src/models/pytest.ini b/docs/tutorials/wiki2/src/models/pytest.ini
index 8b76bc410..a3489cdf8 100644
--- a/docs/tutorials/wiki2/src/models/pytest.ini
+++ b/docs/tutorials/wiki2/src/models/pytest.ini
@@ -1,3 +1,3 @@
[pytest]
testpaths = tutorial
-python_files = *.py
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/models/setup.py b/docs/tutorials/wiki2/src/models/setup.py
index c688c6866..2576cc848 100644
--- a/docs/tutorials/wiki2/src/models/setup.py
+++ b/docs/tutorials/wiki2/src/models/setup.py
@@ -9,9 +9,10 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'alembic',
'bcrypt',
'plaster_pastedeploy',
- 'pyramid >= 1.9a',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
'pyramid_jinja2',
'pyramid_retry',
@@ -55,7 +56,7 @@ setup(
'main = tutorial:main',
],
'console_scripts': [
- 'initialize_tutorial_db = tutorial.scripts.initializedb:main',
+ 'initialize_tutorial_db = tutorial.scripts.initialize_db:main',
],
},
)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/alembic/env.py b/docs/tutorials/wiki2/src/models/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/models/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/models/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
index 3c9ba8e54..a4209a6e9 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # noqa
-from .user import User # noqa
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..231d5d44b
--- /dev/null
+++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import os
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri>\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) != 2:
+ usage(argv)
+ config_uri = argv[1]
+ setup_logging(config_uri)
+ env = bootstrap(config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+Pyramid is having a problem using your SQL database. The problem
+might be caused by one of the following things:
+
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
+
+2. Your database server may not be running. Check that the
+ database server referred to by the "sqlalchemy.url" setting in
+ your "development.ini" file is running.
+ ''')
diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
deleted file mode 100644
index f3c0a6fef..000000000
--- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import os
-import sys
-import transaction
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from pyramid.scripts.common import parse_vars
-
-from ..models.meta import Base
-from ..models import (
- get_engine,
- get_session_factory,
- get_tm_session,
- )
-from ..models import Page, User
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri> [var=value]\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) < 2:
- usage(argv)
- config_uri = argv[1]
- options = parse_vars(argv[2:])
- setup_logging(config_uri)
- settings = get_appsettings(config_uri, options=options)
-
- engine = get_engine(settings)
- Base.metadata.create_all(engine)
-
- session_factory = get_session_factory(engine)
-
- with transaction.manager:
- dbsession = get_tm_session(session_factory, transaction.manager)
-
- editor = User(name='editor', role='editor')
- editor.set_password('editor')
- dbsession.add(editor)
-
- basic = User(name='basic', role='basic')
- basic.set_password('basic')
- dbsession.add(basic)
-
- page = Page(
- name='FrontPage',
- creator=editor,
- data='This is the front page',
- )
- dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2 b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2
index 26d72c0a6..d8b0a4232 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2
+++ b/docs/tutorials/wiki2/src/models/tutorial/templates/mytemplate.jinja2
@@ -5,4 +5,4 @@
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy project</span></h1>
<p class="lead">Welcome to <span class="font-normal">{{project}}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
</div>
-{% endblock content %}
+{% endblock content %} \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/models/tutorial/views/default.py b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
index a404d4154..ef69ff895 100644
--- a/docs/tutorials/wiki2/src/models/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/models/tutorial/views/default.py
@@ -3,14 +3,14 @@ from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
-from ..models import MyModel
+from .. import models
@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
def my_view(request):
try:
- query = request.dbsession.query(MyModel)
- one = query.filter(MyModel.name == 'one').first()
+ query = request.dbsession.query(models.MyModel)
+ one = query.filter(models.MyModel.name == 'one').first()
except DBAPIError:
return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'myproj'}
@@ -20,9 +20,8 @@ db_err_msg = """\
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
-1. You may need to run the "initialize_tutorial_db" script
- to initialize your database tables. Check your virtual
- environment's "bin" directory for this script and try to run it.
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
2. Your database server may not be running. Check that the
database server referred to by the "sqlalchemy.url" setting in
diff --git a/docs/tutorials/wiki2/src/tests/.gitignore b/docs/tutorials/wiki2/src/tests/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/tests/README.txt b/docs/tutorials/wiki2/src/tests/README.txt
index 7b33da610..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/tests/README.txt
+++ b/docs/tutorials/wiki2/src/tests/README.txt
@@ -20,7 +20,17 @@ Getting Started
env/bin/pip install -e ".[testing]"
-- Configure the database.
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
env/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/tests/development.ini b/docs/tutorials/wiki2/src/tests/development.ini
index cc2a5586e..d76a6cd72 100644
--- a/docs/tutorials/wiki2/src/tests/development.ini
+++ b/docs/tutorials/wiki2/src/tests/development.ini
@@ -28,6 +28,12 @@ auth.secret = seekrit
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = localhost:6543
diff --git a/docs/tutorials/wiki2/src/tests/production.ini b/docs/tutorials/wiki2/src/tests/production.ini
index 759807abf..c46adb7b5 100644
--- a/docs/tutorials/wiki2/src/tests/production.ini
+++ b/docs/tutorials/wiki2/src/tests/production.ini
@@ -22,6 +22,12 @@ auth.secret = real-seekrit
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = *:6543
diff --git a/docs/tutorials/wiki2/src/tests/pytest.ini b/docs/tutorials/wiki2/src/tests/pytest.ini
index 8b76bc410..a3489cdf8 100644
--- a/docs/tutorials/wiki2/src/tests/pytest.ini
+++ b/docs/tutorials/wiki2/src/tests/pytest.ini
@@ -1,3 +1,3 @@
[pytest]
testpaths = tutorial
-python_files = *.py
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/tests/setup.py b/docs/tutorials/wiki2/src/tests/setup.py
index abc24876d..28b766cbe 100644
--- a/docs/tutorials/wiki2/src/tests/setup.py
+++ b/docs/tutorials/wiki2/src/tests/setup.py
@@ -9,10 +9,11 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'alembic',
'bcrypt',
'docutils',
'plaster_pastedeploy',
- 'pyramid >= 1.9a',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
'pyramid_jinja2',
'pyramid_retry',
@@ -56,7 +57,7 @@ setup(
'main = tutorial:main',
],
'console_scripts': [
- 'initialize_tutorial_db = tutorial.scripts.initializedb:main',
+ 'initialize_tutorial_db = tutorial.scripts.initialize_db:main',
],
},
)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/alembic/env.py b/docs/tutorials/wiki2/src/tests/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/tests/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
index 3c9ba8e54..a4209a6e9 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # noqa
-from .user import User # noqa
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/routes.py b/docs/tutorials/wiki2/src/tests/tutorial/routes.py
index f0a8b7f96..1fd45a994 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/routes.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/routes.py
@@ -7,7 +7,7 @@ from pyramid.security import (
Everyone,
)
-from .models import Page
+from . import models
def includeme(config):
config.add_static_view('static', 'static', cache_max_age=3600)
@@ -22,7 +22,7 @@ def includeme(config):
def new_page_factory(request):
pagename = request.matchdict['pagename']
- if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0:
next_url = request.route_url('edit_page', pagename=pagename)
raise HTTPFound(location=next_url)
return NewPage(pagename)
@@ -39,7 +39,7 @@ class NewPage(object):
def page_factory(request):
pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ page = request.dbsession.query(models.Page).filter_by(name=pagename).first()
if page is None:
raise HTTPNotFound
return PageResource(page)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..231d5d44b
--- /dev/null
+++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import os
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri>\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) != 2:
+ usage(argv)
+ config_uri = argv[1]
+ setup_logging(config_uri)
+ env = bootstrap(config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+Pyramid is having a problem using your SQL database. The problem
+might be caused by one of the following things:
+
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
+
+2. Your database server may not be running. Check that the
+ database server referred to by the "sqlalchemy.url" setting in
+ your "development.ini" file is running.
+ ''')
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
deleted file mode 100644
index c860ef8cf..000000000
--- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import os
-import sys
-import transaction
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from pyramid.scripts.common import parse_vars
-
-from ..models.meta import Base
-from ..models import (
- get_engine,
- get_session_factory,
- get_tm_session,
- )
-from ..models import Page, User
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri> [var=value]\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-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)
- settings = get_appsettings(config_uri, options=options)
-
- engine = get_engine(settings)
- Base.metadata.create_all(engine)
-
- session_factory = get_session_factory(engine)
-
- with transaction.manager:
- dbsession = get_tm_session(session_factory, transaction.manager)
-
- editor = User(name='editor', role='editor')
- editor.set_password('editor')
- dbsession.add(editor)
-
- basic = User(name='basic', role='basic')
- basic.set_password('basic')
- dbsession.add(basic)
-
- page = Page(
- name='FrontPage',
- creator=editor,
- data='This is the front page',
- )
- dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/security.py b/docs/tutorials/wiki2/src/tests/tutorial/security.py
index 25cff7b05..1ce1c8753 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/security.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/security.py
@@ -5,7 +5,7 @@ from pyramid.security import (
Everyone,
)
-from .models import User
+from . import models
class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
@@ -26,7 +26,7 @@ class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
def get_user(request):
user_id = request.unauthenticated_userid
if user_id is not None:
- user = request.dbsession.query(User).get(user_id)
+ user = request.dbsession.query(models.User).get(user_id)
return user
def includeme(config):
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2 b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
index b98ccbd95..4016b26c9 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
+++ b/docs/tutorials/wiki2/src/tests/tutorial/templates/layout.jinja2
@@ -29,7 +29,7 @@
<div class="container">
<div class="row">
<div class="col-md-2">
- <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png')}}" alt="pyramid web framework">
+ <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png') }}" alt="pyramid web framework">
</div>
<div class="col-md-10">
<div class="content">
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py
index f5273456e..72fbff04b 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py
@@ -5,12 +5,12 @@ import unittest
class TestInitializeDB(unittest.TestCase):
def test_usage(self):
- from ..scripts.initializedb import main
+ from ..scripts.initialize_db import main
with self.assertRaises(SystemExit):
main(argv=['foo'])
def test_run(self):
- from ..scripts.initializedb import main
+ from ..scripts.initialize_db import main
main(argv=['foo', 'development.ini'])
self.assertTrue(os.path.exists('tutorial.sqlite'))
os.remove('tutorial.sqlite')
diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
index 65c12ed3b..ad271fb46 100644
--- a/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/tests/tutorial/views/default.py
@@ -5,7 +5,7 @@ from docutils.core import publish_parts
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
-from ..models import Page
+from .. import models
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
@@ -22,7 +22,7 @@ def view_page(request):
def add_link(match):
word = match.group(1)
- exists = request.dbsession.query(Page).filter_by(name=word).all()
+ exists = request.dbsession.query(models.Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
return '<a href="%s">%s</a>' % (view_url, escape(word))
@@ -55,7 +55,7 @@ def add_page(request):
pagename = request.context.pagename
if 'form.submitted' in request.params:
body = request.params['body']
- page = Page(name=pagename, data=body)
+ page = models.Page(name=pagename, data=body)
page.creator = request.user
request.dbsession.add(page)
next_url = request.route_url('view_page', pagename=pagename)
diff --git a/docs/tutorials/wiki2/src/views/.gitignore b/docs/tutorials/wiki2/src/views/.gitignore
new file mode 100644
index 000000000..1853d983c
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/.gitignore
@@ -0,0 +1,21 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/docs/tutorials/wiki2/src/views/README.txt b/docs/tutorials/wiki2/src/views/README.txt
index 7b33da610..5d5133e34 100644
--- a/docs/tutorials/wiki2/src/views/README.txt
+++ b/docs/tutorials/wiki2/src/views/README.txt
@@ -20,7 +20,17 @@ Getting Started
env/bin/pip install -e ".[testing]"
-- Configure the database.
+- Initialize and upgrade the database using Alembic.
+
+ - Generate your first revision.
+
+ env/bin/alembic -c development.ini revision --autogenerate -m "init"
+
+ - Upgrade to that revision.
+
+ env/bin/alembic -c development.ini upgrade head
+
+- Load default data into the database using a script.
env/bin/initialize_tutorial_db development.ini
diff --git a/docs/tutorials/wiki2/src/views/development.ini b/docs/tutorials/wiki2/src/views/development.ini
index 4a67896b2..ee050c0ea 100644
--- a/docs/tutorials/wiki2/src/views/development.ini
+++ b/docs/tutorials/wiki2/src/views/development.ini
@@ -26,6 +26,12 @@ retry.attempts = 3
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = localhost:6543
diff --git a/docs/tutorials/wiki2/src/views/production.ini b/docs/tutorials/wiki2/src/views/production.ini
index a28e47a83..91d0f5ddb 100644
--- a/docs/tutorials/wiki2/src/views/production.ini
+++ b/docs/tutorials/wiki2/src/views/production.ini
@@ -20,6 +20,12 @@ retry.attempts = 3
# wsgi server configuration
###
+[alembic]
+# path to migration scripts
+script_location = tutorial/alembic
+file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
+# file_template = %%(rev)s_%%(slug)s
+
[server:main]
use = egg:waitress#main
listen = *:6543
diff --git a/docs/tutorials/wiki2/src/views/pytest.ini b/docs/tutorials/wiki2/src/views/pytest.ini
index 8b76bc410..a3489cdf8 100644
--- a/docs/tutorials/wiki2/src/views/pytest.ini
+++ b/docs/tutorials/wiki2/src/views/pytest.ini
@@ -1,3 +1,3 @@
[pytest]
testpaths = tutorial
-python_files = *.py
+python_files = test*.py
diff --git a/docs/tutorials/wiki2/src/views/setup.py b/docs/tutorials/wiki2/src/views/setup.py
index abc24876d..28b766cbe 100644
--- a/docs/tutorials/wiki2/src/views/setup.py
+++ b/docs/tutorials/wiki2/src/views/setup.py
@@ -9,10 +9,11 @@ with open(os.path.join(here, 'CHANGES.txt')) as f:
CHANGES = f.read()
requires = [
+ 'alembic',
'bcrypt',
'docutils',
'plaster_pastedeploy',
- 'pyramid >= 1.9a',
+ 'pyramid >= 1.9',
'pyramid_debugtoolbar',
'pyramid_jinja2',
'pyramid_retry',
@@ -56,7 +57,7 @@ setup(
'main = tutorial:main',
],
'console_scripts': [
- 'initialize_tutorial_db = tutorial.scripts.initializedb:main',
+ 'initialize_tutorial_db = tutorial.scripts.initialize_db:main',
],
},
)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/alembic/env.py b/docs/tutorials/wiki2/src/views/tutorial/alembic/env.py
new file mode 100644
index 000000000..ba116d0f3
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/alembic/env.py
@@ -0,0 +1,58 @@
+"""Pyramid bootstrap environment. """
+from alembic import context
+from pyramid.paster import get_appsettings, setup_logging
+from sqlalchemy import engine_from_config
+
+from tutorial.models.meta import Base
+
+config = context.config
+
+setup_logging(config.config_file_name)
+
+settings = get_appsettings(config.config_file_name)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ context.configure(url=settings['sqlalchemy.url'])
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ engine = engine_from_config(settings, prefix='sqlalchemy.')
+
+ connection = engine.connect()
+ context.configure(
+ connection=connection,
+ target_metadata=target_metadata
+ )
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/docs/tutorials/wiki2/src/views/tutorial/alembic/script.py.mako b/docs/tutorials/wiki2/src/views/tutorial/alembic/script.py.mako
new file mode 100644
index 000000000..2c0156303
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/alembic/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/README.txt b/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/README.txt
new file mode 100644
index 000000000..09ed32c8d
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/alembic/versions/README.txt
@@ -0,0 +1 @@
+Placeholder for alembic versions \ No newline at end of file
diff --git a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
index 3c9ba8e54..a4209a6e9 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/models/__init__.py
@@ -5,8 +5,8 @@ import zope.sqlalchemy
# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
-from .page import Page # noqa
-from .user import User # noqa
+from .page import Page # flake8: noqa
+from .user import User # flake8: noqa
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py
new file mode 100644
index 000000000..231d5d44b
--- /dev/null
+++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initialize_db.py
@@ -0,0 +1,56 @@
+import os
+import sys
+
+from pyramid.paster import bootstrap, setup_logging
+from sqlalchemy.exc import OperationalError
+
+from .. import models
+
+
+def setup_models(dbsession):
+ editor = models.User(name='editor', role='editor')
+ editor.set_password('editor')
+ dbsession.add(editor)
+
+ basic = models.User(name='basic', role='basic')
+ basic.set_password('basic')
+ dbsession.add(basic)
+
+ page = models.Page(
+ name='FrontPage',
+ creator=editor,
+ data='This is the front page',
+ )
+ dbsession.add(page)
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri>\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) != 2:
+ usage(argv)
+ config_uri = argv[1]
+ setup_logging(config_uri)
+ env = bootstrap(config_uri)
+
+ try:
+ with env['request'].tm:
+ dbsession = env['request'].dbsession
+ setup_models(dbsession)
+ except OperationalError:
+ print('''
+Pyramid is having a problem using your SQL database. The problem
+might be caused by one of the following things:
+
+1. You may need to initialize your database tables with `alembic`.
+ Check your README.txt for description and try to run it.
+
+2. Your database server may not be running. Check that the
+ database server referred to by the "sqlalchemy.url" setting in
+ your "development.ini" file is running.
+ ''')
diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
deleted file mode 100644
index f3c0a6fef..000000000
--- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import os
-import sys
-import transaction
-
-from pyramid.paster import (
- get_appsettings,
- setup_logging,
- )
-
-from pyramid.scripts.common import parse_vars
-
-from ..models.meta import Base
-from ..models import (
- get_engine,
- get_session_factory,
- get_tm_session,
- )
-from ..models import Page, User
-
-
-def usage(argv):
- cmd = os.path.basename(argv[0])
- print('usage: %s <config_uri> [var=value]\n'
- '(example: "%s development.ini")' % (cmd, cmd))
- sys.exit(1)
-
-
-def main(argv=sys.argv):
- if len(argv) < 2:
- usage(argv)
- config_uri = argv[1]
- options = parse_vars(argv[2:])
- setup_logging(config_uri)
- settings = get_appsettings(config_uri, options=options)
-
- engine = get_engine(settings)
- Base.metadata.create_all(engine)
-
- session_factory = get_session_factory(engine)
-
- with transaction.manager:
- dbsession = get_tm_session(session_factory, transaction.manager)
-
- editor = User(name='editor', role='editor')
- editor.set_password('editor')
- dbsession.add(editor)
-
- basic = User(name='basic', role='basic')
- basic.set_password('basic')
- dbsession.add(basic)
-
- page = Page(
- name='FrontPage',
- creator=editor,
- data='This is the front page',
- )
- dbsession.add(page)
diff --git a/docs/tutorials/wiki2/src/views/tutorial/views/default.py b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
index 3b95e0f59..a866af1de 100644
--- a/docs/tutorials/wiki2/src/views/tutorial/views/default.py
+++ b/docs/tutorials/wiki2/src/views/tutorial/views/default.py
@@ -9,7 +9,7 @@ from pyramid.httpexceptions import (
from pyramid.view import view_config
-from ..models import Page, User
+from .. import models
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
@@ -22,13 +22,13 @@ def view_wiki(request):
@view_config(route_name='view_page', renderer='../templates/view.jinja2')
def view_page(request):
pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).first()
+ page = request.dbsession.query(models.Page).filter_by(name=pagename).first()
if page is None:
raise HTTPNotFound('No such page')
def add_link(match):
word = match.group(1)
- exists = request.dbsession.query(Page).filter_by(name=word).all()
+ exists = request.dbsession.query(models.Page).filter_by(name=word).all()
if exists:
view_url = request.route_url('view_page', pagename=word)
return '<a href="%s">%s</a>' % (view_url, escape(word))
@@ -44,7 +44,7 @@ def view_page(request):
@view_config(route_name='edit_page', renderer='../templates/edit.jinja2')
def edit_page(request):
pagename = request.matchdict['pagename']
- page = request.dbsession.query(Page).filter_by(name=pagename).one()
+ page = request.dbsession.query(models.Page).filter_by(name=pagename).one()
if 'form.submitted' in request.params:
page.data = request.params['body']
next_url = request.route_url('view_page', pagename=page.name)
@@ -58,14 +58,14 @@ def edit_page(request):
@view_config(route_name='add_page', renderer='../templates/edit.jinja2')
def add_page(request):
pagename = request.matchdict['pagename']
- if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
+ if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0:
next_url = request.route_url('edit_page', pagename=pagename)
return HTTPFound(location=next_url)
if 'form.submitted' in request.params:
body = request.params['body']
- page = Page(name=pagename, data=body)
+ page = models.Page(name=pagename, data=body)
page.creator = (
- request.dbsession.query(User).filter_by(name='editor').one())
+ request.dbsession.query(models.User).filter_by(name='editor').one())
request.dbsession.add(page)
next_url = request.route_url('view_page', pagename=pagename)
return HTTPFound(location=next_url)
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
index 412440a02..a9604e336 100644
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -34,6 +34,7 @@ from pyramid.security import (
)
from pyramid.util import strings_differ
+from pyramid.util import SimpleSerializer
VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$")
@@ -827,7 +828,7 @@ class AuthTktCookieHelper(object):
samesite='Lax',
):
- serializer = _SimpleSerializer()
+ serializer = SimpleSerializer()
self.cookie_profile = CookieProfile(
cookie_name=cookie_name,
@@ -1156,14 +1157,6 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy):
return self.check(username, password, request)
-class _SimpleSerializer(object):
- def loads(self, bstruct):
- return native_(bstruct)
-
- def dumps(self, appstruct):
- return bytes_(appstruct)
-
-
HTTPBasicCredentials = namedtuple(
'HTTPBasicCredentials', ['username', 'password'])
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index 886eec0df..f4fcf413e 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -58,12 +58,17 @@ from pyramid.settings import aslist
from pyramid.threadlocal import manager
from pyramid.util import (
- ActionInfo,
WeakOrderedSet,
- action_method,
object_description,
)
+from pyramid.config.util import (
+ ActionInfo,
+ PredicateList,
+ action_method,
+ not_,
+)
+
from pyramid.config.adapters import AdaptersConfiguratorMixin
from pyramid.config.assets import AssetsConfiguratorMixin
from pyramid.config.factories import FactoriesConfiguratorMixin
@@ -74,7 +79,6 @@ from pyramid.config.security import SecurityConfiguratorMixin
from pyramid.config.settings import SettingsConfiguratorMixin
from pyramid.config.testing import TestingConfiguratorMixin
from pyramid.config.tweens import TweensConfiguratorMixin
-from pyramid.config.util import PredicateList, not_
from pyramid.config.views import ViewsConfiguratorMixin
from pyramid.config.zca import ZCAConfiguratorMixin
@@ -83,9 +87,7 @@ from pyramid.path import DottedNameResolver
empty = text_('')
_marker = object()
-ConfigurationError = ConfigurationError # pyflakes
-
-not_ = not_ # pyflakes, this is an API
+not_ = not_ # api
PHASE0_CONFIG = PHASE0_CONFIG # api
PHASE1_CONFIG = PHASE1_CONFIG # api
diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py
index a68070134..945faa3c6 100644
--- a/pyramid/config/adapters.py
+++ b/pyramid/config/adapters.py
@@ -10,10 +10,9 @@ from pyramid.interfaces import (
IResourceURL,
)
-from pyramid.config.util import (
- action_method,
- takes_one_arg,
- )
+from pyramid.util import takes_one_arg
+
+from pyramid.config.util import action_method
class AdaptersConfiguratorMixin(object):
diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py
index 6eafc1eb1..b9536df42 100644
--- a/pyramid/config/assets.py
+++ b/pyramid/config/assets.py
@@ -12,7 +12,7 @@ from pyramid.interfaces import (
from pyramid.exceptions import ConfigurationError
from pyramid.threadlocal import get_current_registry
-from pyramid.util import action_method
+from pyramid.config.util import action_method
class OverrideProvider(pkg_resources.DefaultProvider):
def __init__(self, module):
diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py
index 202c3ef61..7a5b589cf 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -15,11 +15,11 @@ from pyramid.router import default_execution_policy
from pyramid.traversal import DefaultRootFactory
from pyramid.util import (
- action_method,
get_callable_name,
InstancePropertyHelper,
)
+from pyramid.config.util import action_method
class FactoriesConfiguratorMixin(object):
@action_method
diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py
index 9c5b7e682..5dabe2845 100644
--- a/pyramid/config/i18n.py
+++ b/pyramid/config/i18n.py
@@ -5,7 +5,8 @@ from pyramid.interfaces import (
from pyramid.exceptions import ConfigurationError
from pyramid.path import AssetResolver
-from pyramid.util import action_method
+
+from pyramid.config.util import action_method
class I18NConfiguratorMixin(object):
@action_method
diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py
index 5d99d6564..bda763161 100644
--- a/pyramid/config/predicates.py
+++ b/pyramid/config/predicates.py
@@ -1,2 +1,2 @@
import zope.deprecation
-zope.deprecation.moved('pyramid.predicates', 'Pyramid 2.0')
+zope.deprecation.moved('pyramid.predicates', 'Pyramid 1.10')
diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py
index 68671d08e..0d55c41e8 100644
--- a/pyramid/config/rendering.py
+++ b/pyramid/config/rendering.py
@@ -3,8 +3,8 @@ from pyramid.interfaces import (
PHASE1_CONFIG,
)
-from pyramid.util import action_method
from pyramid import renderers
+from pyramid.config.util import action_method
DEFAULT_RENDERERS = (
('json', renderers.json_renderer_factory),
diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py
index 20fdb2b5d..904c7bd4e 100644
--- a/pyramid/config/routes.py
+++ b/pyramid/config/routes.py
@@ -10,15 +10,18 @@ from pyramid.interfaces import (
)
from pyramid.exceptions import ConfigurationError
-from pyramid.registry import predvalseq
from pyramid.request import route_request_iface
from pyramid.urldispatch import RoutesMapper
-from pyramid.config.util import action_method
from pyramid.util import as_sorted_tuple
import pyramid.predicates
+from pyramid.config.util import (
+ action_method,
+ predvalseq,
+)
+
class RoutesConfiguratorMixin(object):
@action_method
def add_route(self,
diff --git a/pyramid/config/security.py b/pyramid/config/security.py
index 20b816161..c7afbcf4e 100644
--- a/pyramid/config/security.py
+++ b/pyramid/config/security.py
@@ -12,9 +12,9 @@ from pyramid.interfaces import (
from pyramid.csrf import LegacySessionCSRFStoragePolicy
from pyramid.exceptions import ConfigurationError
-from pyramid.util import action_method
from pyramid.util import as_sorted_tuple
+from pyramid.config.util import action_method
class SecurityConfiguratorMixin(object):
diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py
index 5df726a31..1daf5cdeb 100644
--- a/pyramid/config/testing.py
+++ b/pyramid/config/testing.py
@@ -14,7 +14,7 @@ from pyramid.traversal import (
split_path_info,
)
-from pyramid.util import action_method
+from pyramid.config.util import action_method
class TestingConfiguratorMixin(object):
# testing API
diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py
index 16712ab16..8bf21cf71 100644
--- a/pyramid/config/tweens.py
+++ b/pyramid/config/tweens.py
@@ -15,11 +15,12 @@ from pyramid.tweens import (
EXCVIEW,
)
-from pyramid.config.util import (
- action_method,
+from pyramid.util import (
+ is_string_or_iterable,
TopologicalSorter,
)
-from pyramid.util import is_string_or_iterable
+
+from pyramid.config.util import action_method
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 63f06ff9b..aedebd9e2 100644
--- a/pyramid/config/util.py
+++ b/pyramid/config/util.py
@@ -1,28 +1,81 @@
+import functools
from hashlib import md5
-import inspect
+import traceback
+from zope.interface import implementer
from pyramid.compat import (
bytes_,
- getargspec,
is_nonstr_iter
- )
+)
+from pyramid.interfaces import IActionInfo
-from pyramid.compat import im_func
from pyramid.exceptions import ConfigurationError
+from pyramid.predicates import Notted
from pyramid.registry import predvalseq
-
from pyramid.util import (
TopologicalSorter,
- action_method,
- ActionInfo,
- )
+ takes_one_arg,
+)
+
+TopologicalSorter = TopologicalSorter # support bw-compat imports
+takes_one_arg = takes_one_arg # support bw-compat imports
+
+@implementer(IActionInfo)
+class ActionInfo(object):
+ def __init__(self, file, line, function, src):
+ self.file = file
+ self.line = line
+ self.function = function
+ self.src = src
+
+ def __str__(self):
+ srclines = self.src.split('\n')
+ src = '\n'.join(' %s' % x for x in srclines)
+ return 'Line %s of file %s:\n%s' % (self.line, self.file, src)
+
+def action_method(wrapped):
+ """ Wrapper to provide the right conflict info report data when a method
+ that calls Configurator.action calls another that does the same. Not a
+ documented API but used by some external systems."""
+ def wrapper(self, *arg, **kw):
+ if self._ainfo is None:
+ self._ainfo = []
+ info = kw.pop('_info', None)
+ # backframes for outer decorators to actionmethods
+ backframes = kw.pop('_backframes', 0) + 2
+ if is_nonstr_iter(info) and len(info) == 4:
+ # _info permitted as extract_stack tuple
+ info = ActionInfo(*info)
+ if info is None:
+ try:
+ f = traceback.extract_stack(limit=4)
+
+ # Work around a Python 3.5 issue whereby it would insert an
+ # extra stack frame. This should no longer be necessary in
+ # Python 3.5.1
+ last_frame = ActionInfo(*f[-1])
+ if last_frame.function == 'extract_stack': # pragma: no cover
+ f.pop()
+ info = ActionInfo(*f[-backframes])
+ except Exception: # pragma: no cover
+ info = ActionInfo(None, 0, '', '')
+ self._ainfo.append(info)
+ try:
+ result = wrapped(self, *arg, **kw)
+ finally:
+ self._ainfo.pop()
+ return result
+
+ if hasattr(wrapped, '__name__'):
+ functools.update_wrapper(wrapper, wrapped)
+ wrapper.__docobj__ = wrapped
+ return wrapper
-action_method = action_method # support bw compat imports
-ActionInfo = ActionInfo # support bw compat imports
MAX_ORDER = 1 << 30
DEFAULT_PHASH = md5().hexdigest()
+
class not_(object):
"""
@@ -61,30 +114,6 @@ class not_(object):
def __init__(self, value):
self.value = value
-class Notted(object):
- def __init__(self, predicate):
- self.predicate = predicate
-
- def _notted_text(self, val):
- # if the underlying predicate doesnt return a value, it's not really
- # a predicate, it's just something pretending to be a predicate,
- # so dont update the hash
- if val:
- val = '!' + val
- return val
-
- def text(self):
- return self._notted_text(self.predicate.text())
-
- def phash(self):
- return self._notted_text(self.predicate.phash())
-
- def __call__(self, context, request):
- result = self.predicate(context, request)
- phash = self.phash()
- if phash:
- result = not result
- return result
# under = after
# over = before
@@ -189,52 +218,3 @@ class PredicateList(object):
score = score | bit
order = (MAX_ORDER - score) / (len(preds) + 1)
return order, preds, phash.hexdigest()
-
-def takes_one_arg(callee, attr=None, argname=None):
- ismethod = False
- if attr is None:
- attr = '__call__'
- if inspect.isroutine(callee):
- fn = callee
- elif inspect.isclass(callee):
- try:
- fn = callee.__init__
- except AttributeError:
- return False
- ismethod = hasattr(fn, '__call__')
- else:
- try:
- fn = getattr(callee, attr)
- except AttributeError:
- return False
-
- try:
- argspec = getargspec(fn)
- except TypeError:
- return False
-
- args = argspec[0]
-
- if hasattr(fn, im_func) or ismethod:
- # it's an instance method (or unbound method on py2)
- if not args:
- return False
- args = args[1:]
-
- if not args:
- return False
-
- if len(args) == 1:
- return True
-
- if argname:
-
- defaults = argspec[3]
- if defaults is None:
- defaults = ()
-
- if args[0] == argname:
- if len(args) - len(defaults) == 1:
- return True
-
- return False
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index e5ebc8e07..eb002ec2d 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -1,3 +1,4 @@
+import functools
import inspect
import posixpath
import operator
@@ -54,10 +55,7 @@ from pyramid.httpexceptions import (
default_exceptionresponse_view,
)
-from pyramid.registry import (
- predvalseq,
- Deferred,
- )
+from pyramid.registry import Deferred
from pyramid.security import NO_PERMISSION_REQUIRED
from pyramid.static import static_view
@@ -66,10 +64,7 @@ from pyramid.url import parse_url_overrides
from pyramid.view import AppendSlashNotFoundViewFactory
-import pyramid.util
from pyramid.util import (
- viewdefaults,
- action_method,
as_sorted_tuple,
TopologicalSorter,
)
@@ -88,8 +83,10 @@ from pyramid.viewderivers import (
)
from pyramid.config.util import (
+ action_method,
DEFAULT_PHASH,
MAX_ORDER,
+ predvalseq,
)
urljoin = urlparse.urljoin
@@ -183,6 +180,68 @@ class MultiView(object):
continue
raise PredicateMismatch(self.name)
+def attr_wrapped_view(view, info):
+ accept, order, phash = (info.options.get('accept', None),
+ getattr(info, 'order', MAX_ORDER),
+ getattr(info, 'phash', DEFAULT_PHASH))
+ # this is a little silly but we don't want to decorate the original
+ # function with attributes that indicate accept, order, and phash,
+ # so we use a wrapper
+ if (
+ (accept is None) and
+ (order == MAX_ORDER) and
+ (phash == DEFAULT_PHASH)
+ ):
+ return view # defaults
+ def attr_view(context, request):
+ return view(context, request)
+ attr_view.__accept__ = accept
+ attr_view.__order__ = order
+ attr_view.__phash__ = phash
+ attr_view.__view_attr__ = info.options.get('attr')
+ attr_view.__permission__ = info.options.get('permission')
+ return attr_view
+
+attr_wrapped_view.options = ('accept', 'attr', 'permission')
+
+def predicated_view(view, info):
+ preds = info.predicates
+ if not preds:
+ return view
+ def predicate_wrapper(context, request):
+ for predicate in preds:
+ if not predicate(context, request):
+ view_name = getattr(view, '__name__', view)
+ raise PredicateMismatch(
+ 'predicate mismatch for view %s (%s)' % (
+ view_name, predicate.text()))
+ return view(context, request)
+ def checker(context, request):
+ return all((predicate(context, request) for predicate in
+ preds))
+ predicate_wrapper.__predicated__ = checker
+ predicate_wrapper.__predicates__ = preds
+ return predicate_wrapper
+
+def viewdefaults(wrapped):
+ """ Decorator for add_view-like methods which takes into account
+ __view_defaults__ attached to view it is passed. Not a documented API but
+ used by some external systems."""
+ def wrapper(self, *arg, **kw):
+ defaults = {}
+ if arg:
+ view = arg[0]
+ else:
+ view = kw.get('view')
+ view = self.maybe_dotted(view)
+ if inspect.isclass(view):
+ defaults = getattr(view, '__view_defaults__', {}).copy()
+ if '_backframes' not in kw:
+ kw['_backframes'] = 1 # for action_method
+ defaults.update(kw)
+ return wrapped(self, *arg, **defaults)
+ return functools.wraps(wrapped)(wrapper)
+
class ViewsConfiguratorMixin(object):
@viewdefaults
@action_method
@@ -1106,11 +1165,9 @@ class ViewsConfiguratorMixin(object):
raise ConfigurationError('Unknown view options: %s' % (kw,))
def _apply_view_derivers(self, info):
- d = pyramid.viewderivers
-
# These derivers are not really derivers and so have fixed order
- outer_derivers = [('attr_wrapped_view', d.attr_wrapped_view),
- ('predicated_view', d.predicated_view)]
+ outer_derivers = [('attr_wrapped_view', attr_wrapped_view),
+ ('predicated_view', predicated_view)]
view = info.original_view
derivers = self.registry.getUtility(IViewDerivers)
diff --git a/pyramid/csrf.py b/pyramid/csrf.py
index d762e2999..da171d9af 100644
--- a/pyramid/csrf.py
+++ b/pyramid/csrf.py
@@ -4,8 +4,6 @@ from webob.cookies import CookieProfile
from zope.interface import implementer
-from pyramid.authentication import _SimpleSerializer
-
from pyramid.compat import (
bytes_,
urlparse,
@@ -18,6 +16,7 @@ from pyramid.exceptions import (
from pyramid.interfaces import ICSRFStoragePolicy
from pyramid.settings import aslist
from pyramid.util import (
+ SimpleSerializer,
is_same_domain,
strings_differ
)
@@ -116,7 +115,7 @@ class CookieCSRFStoragePolicy(object):
def __init__(self, cookie_name='csrf_token', secure=False, httponly=False,
domain=None, max_age=None, path='/', samesite='Lax'):
- serializer = _SimpleSerializer()
+ serializer = SimpleSerializer()
self.cookie_profile = CookieProfile(
cookie_name=cookie_name,
secure=secure,
diff --git a/pyramid/predicates.py b/pyramid/predicates.py
index 3d7bb1b4b..5e54badff 100644
--- a/pyramid/predicates.py
+++ b/pyramid/predicates.py
@@ -12,8 +12,10 @@ from pyramid.traversal import (
)
from pyramid.urldispatch import _compile_route
-from pyramid.util import object_description
-from pyramid.util import as_sorted_tuple
+from pyramid.util import (
+ as_sorted_tuple,
+ object_description,
+)
_marker = object()
@@ -298,3 +300,27 @@ class EffectivePrincipalsPredicate(object):
return True
return False
+class Notted(object):
+ def __init__(self, predicate):
+ self.predicate = predicate
+
+ def _notted_text(self, val):
+ # if the underlying predicate doesnt return a value, it's not really
+ # a predicate, it's just something pretending to be a predicate,
+ # so dont update the hash
+ if val:
+ val = '!' + val
+ return val
+
+ def text(self):
+ return self._notted_text(self.predicate.text())
+
+ def phash(self):
+ return self._notted_text(self.predicate.phash())
+
+ def __call__(self, context, request):
+ result = self.predicate(context, request)
+ phash = self.phash()
+ if phash:
+ result = not result
+ return result
diff --git a/pyramid/registry.py b/pyramid/registry.py
index 7b75aeefc..a741c495e 100644
--- a/pyramid/registry.py
+++ b/pyramid/registry.py
@@ -2,7 +2,6 @@ import operator
import threading
from zope.interface import implementer
-
from zope.interface.registry import Components
from pyramid.compat import text_
@@ -294,6 +293,5 @@ def undefer(v):
class predvalseq(tuple):
""" A subtype of tuple used to represent a sequence of predicate values """
- pass
global_registry = Registry('global')
diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py
index bb201dbc2..4898eb39f 100644
--- a/pyramid/scripts/pshell.py
+++ b/pyramid/scripts/pshell.py
@@ -1,4 +1,5 @@
from code import interact
+from contextlib import contextmanager
import argparse
import os
import sys
@@ -7,6 +8,7 @@ import pkg_resources
from pyramid.compat import exec_
from pyramid.util import DottedNameResolver
+from pyramid.util import make_contextmanager
from pyramid.paster import bootstrap
from pyramid.settings import aslist
@@ -85,6 +87,7 @@ class PShellCommand(object):
preferred_shells = []
setup = None
pystartup = os.environ.get('PYTHONSTARTUP')
+ resolver = DottedNameResolver(None)
def __init__(self, argv, quiet=False):
self.quiet = quiet
@@ -92,7 +95,6 @@ class PShellCommand(object):
def pshell_file_config(self, loader, defaults):
settings = loader.get_settings('pshell', defaults)
- resolver = DottedNameResolver(None)
self.loaded_objects = {}
self.object_help = {}
self.setup = None
@@ -102,7 +104,7 @@ class PShellCommand(object):
elif k == 'default_shell':
self.preferred_shells = [x.lower() for x in aslist(v)]
else:
- self.loaded_objects[k] = resolver.maybe_resolve(v)
+ self.loaded_objects[k] = self.resolver.maybe_resolve(v)
self.object_help[k] = v
def out(self, msg): # pragma: no cover
@@ -115,18 +117,36 @@ class PShellCommand(object):
if not self.args.config_uri:
self.out('Requires a config file argument')
return 2
+
config_uri = self.args.config_uri
config_vars = parse_vars(self.args.config_vars)
loader = self.get_config_loader(config_uri)
loader.setup_logging(config_vars)
self.pshell_file_config(loader, config_vars)
- env = self.bootstrap(config_uri, options=config_vars)
+ self.env = self.bootstrap(config_uri, options=config_vars)
# remove the closer from the env
- self.closer = env.pop('closer')
+ self.closer = self.env.pop('closer')
+
+ try:
+ if shell is None:
+ try:
+ shell = self.make_shell()
+ except ValueError as e:
+ self.out(str(e))
+ return 1
+
+ with self.setup_env():
+ shell(self.env, self.help)
+
+ finally:
+ self.closer()
+ @contextmanager
+ def setup_env(self):
# setup help text for default environment
+ env = self.env
env_help = dict(env)
env_help['app'] = 'The WSGI application.'
env_help['root'] = 'Root of the default resource tree.'
@@ -135,65 +155,55 @@ class PShellCommand(object):
env_help['root_factory'] = (
'Default root factory used to create `root`.')
+ # load the pshell section of the ini file
+ env.update(self.loaded_objects)
+
+ # eliminate duplicates from env, allowing custom vars to override
+ for k in self.loaded_objects:
+ if k in env_help:
+ del env_help[k]
+
# override use_script with command-line options
if self.args.setup:
self.setup = self.args.setup
if self.setup:
- # store the env before muddling it with the script
- orig_env = env.copy()
-
# call the setup callable
- resolver = DottedNameResolver(None)
- setup = resolver.maybe_resolve(self.setup)
- setup(env)
+ self.setup = self.resolver.maybe_resolve(self.setup)
+ # store the env before muddling it with the script
+ orig_env = env.copy()
+ setup_manager = make_contextmanager(self.setup)
+ with setup_manager(env):
# remove any objects from default help that were overidden
for k, v in env.items():
- if k not in orig_env or env[k] != orig_env[k]:
+ if k not in orig_env or v != orig_env[k]:
if getattr(v, '__doc__', False):
env_help[k] = v.__doc__.replace("\n", " ")
else:
env_help[k] = v
-
- # load the pshell section of the ini file
- env.update(self.loaded_objects)
-
- # eliminate duplicates from env, allowing custom vars to override
- for k in self.loaded_objects:
- if k in env_help:
- del env_help[k]
-
- # generate help text
- help = ''
- if env_help:
- help += 'Environment:'
- for var in sorted(env_help.keys()):
- help += '\n %-12s %s' % (var, env_help[var])
-
- if self.object_help:
- help += '\n\nCustom Variables:'
- for var in sorted(self.object_help.keys()):
- help += '\n %-12s %s' % (var, self.object_help[var])
-
- if shell is None:
- try:
- shell = self.make_shell()
- except ValueError as e:
- self.out(str(e))
- self.closer()
- return 1
-
- if self.pystartup and os.path.isfile(self.pystartup):
- with open(self.pystartup, 'rb') as fp:
- exec_(fp.read().decode('utf-8'), env)
- if '__builtins__' in env:
- del env['__builtins__']
-
- try:
- shell(env, help)
- finally:
- self.closer()
+ del orig_env
+
+ # generate help text
+ help = ''
+ if env_help:
+ help += 'Environment:'
+ for var in sorted(env_help.keys()):
+ help += '\n %-12s %s' % (var, env_help[var])
+
+ if self.object_help:
+ help += '\n\nCustom Variables:'
+ for var in sorted(self.object_help.keys()):
+ help += '\n %-12s %s' % (var, self.object_help[var])
+
+ if self.pystartup and os.path.isfile(self.pystartup):
+ with open(self.pystartup, 'rb') as fp:
+ exec_(fp.read().decode('utf-8'), env)
+ if '__builtins__' in env:
+ del env['__builtins__']
+
+ self.help = help.strip()
+ yield
def show_shells(self):
shells = self.find_all_shells()
diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py
index 7938d5a18..4efd76f2b 100644
--- a/pyramid/tests/test_authentication.py
+++ b/pyramid/tests/test_authentication.py
@@ -1639,21 +1639,6 @@ class TestExtractHTTPBasicCredentials(unittest.TestCase):
self.assertEqual(result.username, 'chrisr')
self.assertEqual(result.password, 'pass')
-
-
-
-class TestSimpleSerializer(unittest.TestCase):
- def _makeOne(self):
- from pyramid.authentication import _SimpleSerializer
- return _SimpleSerializer()
-
- def test_loads(self):
- inst = self._makeOne()
- self.assertEqual(inst.loads(b'abc'), text_('abc'))
-
- def test_dumps(self):
- inst = self._makeOne()
- self.assertEqual(inst.dumps('abc'), bytes_('abc'))
class DummyContext:
pass
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index ab584cc3d..76a3d703d 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -852,7 +852,7 @@ pyramid.tests.test_config.dummy_include2""",
self.assertEqual(config.action('discrim', kw={'a':1}), None)
def test_action_autocommit_with_introspectables(self):
- from pyramid.util import ActionInfo
+ from pyramid.config.util import ActionInfo
config = self._makeOne(autocommit=True)
intr = DummyIntrospectable()
config.action('discrim', introspectables=(intr,))
diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py
index bb86a1f56..99c67e8c6 100644
--- a/pyramid/tests/test_config/test_util.py
+++ b/pyramid/tests/test_config/test_util.py
@@ -1,6 +1,38 @@
import unittest
+
from pyramid.compat import text_
+class TestActionInfo(unittest.TestCase):
+ def _getTargetClass(self):
+ from pyramid.config.util import ActionInfo
+ return ActionInfo
+
+ def _makeOne(self, filename, lineno, function, linerepr):
+ return self._getTargetClass()(filename, lineno, function, linerepr)
+
+ def test_class_conforms(self):
+ from zope.interface.verify import verifyClass
+ from pyramid.interfaces import IActionInfo
+ verifyClass(IActionInfo, self._getTargetClass())
+
+ def test_instance_conforms(self):
+ from zope.interface.verify import verifyObject
+ from pyramid.interfaces import IActionInfo
+ verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f'))
+
+ def test_ctor(self):
+ inst = self._makeOne('filename', 10, 'function', 'src')
+ self.assertEqual(inst.file, 'filename')
+ self.assertEqual(inst.line, 10)
+ self.assertEqual(inst.function, 'function')
+ self.assertEqual(inst.src, 'src')
+
+ def test___str__(self):
+ inst = self._makeOne('filename', 0, 'function', ' linerepr ')
+ self.assertEqual(str(inst),
+ "Line 0 of file filename:\n linerepr ")
+
+
class TestPredicateList(unittest.TestCase):
def _makeOne(self):
@@ -391,220 +423,6 @@ class TestPredicateList(unittest.TestCase):
self.assertEqual(predicates[1](None, request), True)
self.assertEqual(predicates[2](None, request), True)
-
-class Test_takes_one_arg(unittest.TestCase):
- def _callFUT(self, view, attr=None, argname=None):
- from pyramid.config.util import takes_one_arg
- return takes_one_arg(view, attr=attr, argname=argname)
-
- def test_requestonly_newstyle_class_no_init(self):
- class foo(object):
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_requestonly_newstyle_class_init_toomanyargs(self):
- class foo(object):
- def __init__(self, context, request):
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_requestonly_newstyle_class_init_onearg_named_request(self):
- class foo(object):
- def __init__(self, request):
- """ """
- self.assertTrue(self._callFUT(foo))
-
- def test_newstyle_class_init_onearg_named_somethingelse(self):
- class foo(object):
- def __init__(self, req):
- """ """
- self.assertTrue(self._callFUT(foo))
-
- def test_newstyle_class_init_defaultargs_firstname_not_request(self):
- class foo(object):
- def __init__(self, context, request=None):
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_newstyle_class_init_defaultargs_firstname_request(self):
- class foo(object):
- def __init__(self, request, foo=1, bar=2):
- """ """
- self.assertTrue(self._callFUT(foo, argname='request'))
-
- def test_newstyle_class_init_firstname_request_with_secondname(self):
- class foo(object):
- def __init__(self, request, two):
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_newstyle_class_init_noargs(self):
- class foo(object):
- def __init__():
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_oldstyle_class_no_init(self):
- class foo:
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_oldstyle_class_init_toomanyargs(self):
- class foo:
- def __init__(self, context, request):
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_oldstyle_class_init_onearg_named_request(self):
- class foo:
- def __init__(self, request):
- """ """
- self.assertTrue(self._callFUT(foo))
-
- def test_oldstyle_class_init_onearg_named_somethingelse(self):
- class foo:
- def __init__(self, req):
- """ """
- self.assertTrue(self._callFUT(foo))
-
- def test_oldstyle_class_init_defaultargs_firstname_not_request(self):
- class foo:
- def __init__(self, context, request=None):
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_oldstyle_class_init_defaultargs_firstname_request(self):
- class foo:
- def __init__(self, request, foo=1, bar=2):
- """ """
- self.assertTrue(self._callFUT(foo, argname='request'), True)
-
- def test_oldstyle_class_init_noargs(self):
- class foo:
- def __init__():
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_function_toomanyargs(self):
- def foo(context, request):
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_function_with_attr_false(self):
- def bar(context, request):
- """ """
- def foo(context, request):
- """ """
- foo.bar = bar
- self.assertFalse(self._callFUT(foo, 'bar'))
-
- def test_function_with_attr_true(self):
- def bar(context, request):
- """ """
- def foo(request):
- """ """
- foo.bar = bar
- self.assertTrue(self._callFUT(foo, 'bar'))
-
- def test_function_onearg_named_request(self):
- def foo(request):
- """ """
- self.assertTrue(self._callFUT(foo))
-
- def test_function_onearg_named_somethingelse(self):
- def foo(req):
- """ """
- self.assertTrue(self._callFUT(foo))
-
- def test_function_defaultargs_firstname_not_request(self):
- def foo(context, request=None):
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_function_defaultargs_firstname_request(self):
- def foo(request, foo=1, bar=2):
- """ """
- self.assertTrue(self._callFUT(foo, argname='request'))
-
- def test_function_noargs(self):
- def foo():
- """ """
- self.assertFalse(self._callFUT(foo))
-
- def test_instance_toomanyargs(self):
- class Foo:
- def __call__(self, context, request):
- """ """
- foo = Foo()
- self.assertFalse(self._callFUT(foo))
-
- def test_instance_defaultargs_onearg_named_request(self):
- class Foo:
- def __call__(self, request):
- """ """
- foo = Foo()
- self.assertTrue(self._callFUT(foo))
-
- def test_instance_defaultargs_onearg_named_somethingelse(self):
- class Foo:
- def __call__(self, req):
- """ """
- foo = Foo()
- self.assertTrue(self._callFUT(foo))
-
- def test_instance_defaultargs_firstname_not_request(self):
- class Foo:
- def __call__(self, context, request=None):
- """ """
- foo = Foo()
- self.assertFalse(self._callFUT(foo))
-
- def test_instance_defaultargs_firstname_request(self):
- class Foo:
- def __call__(self, request, foo=1, bar=2):
- """ """
- foo = Foo()
- self.assertTrue(self._callFUT(foo, argname='request'), True)
-
- def test_instance_nocall(self):
- class Foo: pass
- foo = Foo()
- self.assertFalse(self._callFUT(foo))
-
- def test_method_onearg_named_request(self):
- class Foo:
- def method(self, request):
- """ """
- foo = Foo()
- self.assertTrue(self._callFUT(foo.method))
-
- def test_function_annotations(self):
- def foo(bar):
- """ """
- # avoid SyntaxErrors in python2, this if effectively nop
- getattr(foo, '__annotations__', {}).update({'bar': 'baz'})
- self.assertTrue(self._callFUT(foo))
-
-class TestNotted(unittest.TestCase):
- def _makeOne(self, predicate):
- from pyramid.config.util import Notted
- return Notted(predicate)
-
- def test_it_with_phash_val(self):
- pred = DummyPredicate('val')
- inst = self._makeOne(pred)
- self.assertEqual(inst.text(), '!val')
- self.assertEqual(inst.phash(), '!val')
- self.assertEqual(inst(None, None), False)
-
- def test_it_without_phash_val(self):
- pred = DummyPredicate('')
- inst = self._makeOne(pred)
- self.assertEqual(inst.text(), '')
- self.assertEqual(inst.phash(), '')
- self.assertEqual(inst(None, None), True)
-
-
class TestDeprecatedPredicates(unittest.TestCase):
def test_it(self):
import warnings
@@ -613,18 +431,6 @@ class TestDeprecatedPredicates(unittest.TestCase):
from pyramid.config.predicates import XHRPredicate
self.assertEqual(len(w), 1)
-class DummyPredicate(object):
- def __init__(self, result):
- self.result = result
-
- def text(self):
- return self.result
-
- phash = text
-
- def __call__(self, context, request):
- return True
-
class DummyCustomPredicate(object):
def __init__(self):
self.__text__ = 'custom predicate'
@@ -636,8 +442,9 @@ class DummyCustomPredicate(object):
@classmethod
def classmethod_predicate_no_text(*args): pass # pragma: no cover
-class Dummy:
- pass
+class Dummy(object):
+ def __init__(self, **kw):
+ self.__dict__.update(**kw)
class DummyRequest:
subpath = ()
@@ -652,4 +459,3 @@ class DummyRequest:
class DummyConfigurator(object):
def maybe_dotted(self, thing):
return thing
-
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index 860254385..cb554a816 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -542,8 +542,8 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertEqual(wrapper(None, request), 'OK')
def test_add_view_default_phash_overrides_default_phash(self):
- from pyramid.renderers import null_renderer
from pyramid.config.util import DEFAULT_PHASH
+ from pyramid.renderers import null_renderer
from zope.interface import Interface
from pyramid.interfaces import IRequest
from pyramid.interfaces import IView
@@ -564,8 +564,8 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertEqual(wrapper(None, request), 'OK')
def test_add_view_exc_default_phash_overrides_default_phash(self):
- from pyramid.renderers import null_renderer
from pyramid.config.util import DEFAULT_PHASH
+ from pyramid.renderers import null_renderer
from zope.interface import implementedBy
from pyramid.interfaces import IRequest
from pyramid.interfaces import IView
@@ -3385,6 +3385,18 @@ class Test_view_description(unittest.TestCase):
self.assertEqual(result,
'function pyramid.tests.test_config.test_views.view')
+class Test_viewdefaults(unittest.TestCase):
+ def _makeOne(self, wrapped):
+ from pyramid.decorator import reify
+ return reify(wrapped)
+
+ def test_dunder_attrs_copied(self):
+ from pyramid.config.views import viewdefaults
+ decorator = self._makeOne(viewdefaults)
+ self.assertEqual(decorator.__doc__, viewdefaults.__doc__)
+ self.assertEqual(decorator.__name__, viewdefaults.__name__)
+ self.assertEqual(decorator.__module__, viewdefaults.__module__)
+
class DummyRegistry:
utility = None
diff --git a/pyramid/tests/test_decorator.py b/pyramid/tests/test_decorator.py
index 0a98a512d..4b9313e06 100644
--- a/pyramid/tests/test_decorator.py
+++ b/pyramid/tests/test_decorator.py
@@ -21,13 +21,6 @@ class TestReify(unittest.TestCase):
result = decorator.__get__(None)
self.assertEqual(result, decorator)
- def test_dunder_attrs_copied(self):
- from pyramid.util import viewdefaults
- decorator = self._makeOne(viewdefaults)
- self.assertEqual(decorator.__doc__, viewdefaults.__doc__)
- self.assertEqual(decorator.__name__, viewdefaults.__name__)
- self.assertEqual(decorator.__module__, viewdefaults.__module__)
-
class Dummy(object):
pass
diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py
index 4f9011cc0..793ac329f 100644
--- a/pyramid/tests/test_events.py
+++ b/pyramid/tests/test_events.py
@@ -235,6 +235,7 @@ class TestBeforeRender(unittest.TestCase):
from pyramid.events import BeforeRender
return BeforeRender(system, val)
+ @testing.skip_on('pypy') # see https://github.com/Pylons/pyramid/issues/3237
def test_instance_conforms(self):
from zope.interface.verify import verifyObject
from pyramid.interfaces import IBeforeRender
diff --git a/pyramid/tests/test_predicates.py b/pyramid/tests/test_predicates.py
index 8a002c24e..da0b44708 100644
--- a/pyramid/tests/test_predicates.py
+++ b/pyramid/tests/test_predicates.py
@@ -514,13 +514,43 @@ class Test_EffectivePrincipalsPredicate(unittest.TestCase):
context = Dummy()
self.assertFalse(inst(context, request))
+
+class TestNotted(unittest.TestCase):
+ def _makeOne(self, predicate):
+ from pyramid.predicates import Notted
+ return Notted(predicate)
+
+ def test_it_with_phash_val(self):
+ pred = DummyPredicate('val')
+ inst = self._makeOne(pred)
+ self.assertEqual(inst.text(), '!val')
+ self.assertEqual(inst.phash(), '!val')
+ self.assertEqual(inst(None, None), False)
+
+ def test_it_without_phash_val(self):
+ pred = DummyPredicate('')
+ inst = self._makeOne(pred)
+ self.assertEqual(inst.text(), '')
+ self.assertEqual(inst.phash(), '')
+ self.assertEqual(inst(None, None), True)
+
class predicate(object):
def __repr__(self):
return 'predicate'
def __hash__(self):
return 1
-
+
class Dummy(object):
- def __init__(self, **kw):
- self.__dict__.update(**kw)
-
+ pass
+
+class DummyPredicate(object):
+ def __init__(self, result):
+ self.result = result
+
+ def text(self):
+ return self.result
+
+ phash = text
+
+ def __call__(self, context, request):
+ return True
diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py
index 2d2b0549f..f1ef403f8 100644
--- a/pyramid/tests/test_scripts/dummy.py
+++ b/pyramid/tests/test_scripts/dummy.py
@@ -22,11 +22,13 @@ class DummyShell(object):
env = {}
help = ''
called = False
+ dummy_attr = 1
def __call__(self, env, help):
self.env = env
self.help = help
self.called = True
+ self.env['request'].dummy_attr = self.dummy_attr
class DummyInteractor:
def __call__(self, banner, local):
diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py
index ca9eb7af2..df664bea9 100644
--- a/pyramid/tests/test_scripts/test_pshell.py
+++ b/pyramid/tests/test_scripts/test_pshell.py
@@ -226,6 +226,33 @@ class TestPShellCommand(unittest.TestCase):
self.assertTrue(self.bootstrap.closer.called)
self.assertTrue(shell.help)
+ def test_command_setup_generator(self):
+ command = self._makeOne()
+ did_resume_after_yield = {}
+ def setup(env):
+ env['a'] = 1
+ env['root'] = 'root override'
+ env['none'] = None
+ request = env['request']
+ yield
+ did_resume_after_yield['result'] = True
+ self.assertEqual(request.dummy_attr, 1)
+ self.loader.settings = {'pshell': {'setup': setup}}
+ shell = dummy.DummyShell()
+ command.run(shell)
+ self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp')
+ self.assertEqual(shell.env, {
+ 'app':self.bootstrap.app, 'root':'root override',
+ 'registry':self.bootstrap.registry,
+ 'request':self.bootstrap.request,
+ 'root_factory':self.bootstrap.root_factory,
+ 'a':1,
+ 'none': None,
+ })
+ self.assertTrue(did_resume_after_yield['result'])
+ self.assertTrue(self.bootstrap.closer.called)
+ self.assertTrue(shell.help)
+
def test_command_default_shell_option(self):
command = self._makeOne()
ipshell = dummy.DummyShell()
@@ -259,7 +286,7 @@ class TestPShellCommand(unittest.TestCase):
'registry':self.bootstrap.registry,
'request':self.bootstrap.request,
'root_factory':self.bootstrap.root_factory,
- 'a':1, 'm':model,
+ 'a':1, 'm':'model override',
})
self.assertTrue(self.bootstrap.closer.called)
self.assertTrue(shell.help)
diff --git a/pyramid/tests/test_testing.py b/pyramid/tests/test_testing.py
index 0b4619de4..86c219988 100644
--- a/pyramid/tests/test_testing.py
+++ b/pyramid/tests/test_testing.py
@@ -1,5 +1,6 @@
import unittest
from zope.component import getSiteManager
+from pyramid import testing
class TestDummyRootFactory(unittest.TestCase):
def _makeOne(self, environ):
@@ -526,6 +527,7 @@ class TestDummySession(unittest.TestCase):
from pyramid.testing import DummySession
return DummySession()
+ @testing.skip_on('pypy') # see https://github.com/Pylons/pyramid/issues/3237
def test_instance_conforms(self):
from zope.interface.verify import verifyObject
from pyramid.interfaces import ISession
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index ab9de262e..a76cd2017 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -1,5 +1,9 @@
import unittest
-from pyramid.compat import PY2
+from pyramid.compat import (
+ PY2,
+ text_,
+ bytes_,
+ )
class Test_InstancePropertyHelper(unittest.TestCase):
@@ -747,36 +751,6 @@ class TestSentinel(unittest.TestCase):
r = repr(Sentinel('ABC'))
self.assertEqual(r, 'ABC')
-class TestActionInfo(unittest.TestCase):
- def _getTargetClass(self):
- from pyramid.util import ActionInfo
- return ActionInfo
-
- def _makeOne(self, filename, lineno, function, linerepr):
- return self._getTargetClass()(filename, lineno, function, linerepr)
-
- def test_class_conforms(self):
- from zope.interface.verify import verifyClass
- from pyramid.interfaces import IActionInfo
- verifyClass(IActionInfo, self._getTargetClass())
-
- def test_instance_conforms(self):
- from zope.interface.verify import verifyObject
- from pyramid.interfaces import IActionInfo
- verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f'))
-
- def test_ctor(self):
- inst = self._makeOne('filename', 10, 'function', 'src')
- self.assertEqual(inst.file, 'filename')
- self.assertEqual(inst.line, 10)
- self.assertEqual(inst.function, 'function')
- self.assertEqual(inst.src, 'src')
-
- def test___str__(self):
- inst = self._makeOne('filename', 0, 'function', ' linerepr ')
- self.assertEqual(str(inst),
- "Line 0 of file filename:\n linerepr ")
-
class TestCallableName(unittest.TestCase):
def test_valid_ascii(self):
@@ -889,3 +863,249 @@ class Test_is_same_domain(unittest.TestCase):
self.assertTrue(self._callFUT("example.com:8080", "example.com:8080"))
self.assertFalse(self._callFUT("example.com:8080", "example.com"))
self.assertFalse(self._callFUT("example.com", "example.com:8080"))
+
+
+class Test_make_contextmanager(unittest.TestCase):
+ def _callFUT(self, *args, **kw):
+ from pyramid.util import make_contextmanager
+ return make_contextmanager(*args, **kw)
+
+ def test_with_None(self):
+ mgr = self._callFUT(None)
+ with mgr() as ctx:
+ self.assertIsNone(ctx)
+
+ def test_with_generator(self):
+ def mygen(ctx):
+ yield ctx
+ mgr = self._callFUT(mygen)
+ with mgr('a') as ctx:
+ self.assertEqual(ctx, 'a')
+
+ def test_with_multiple_yield_generator(self):
+ def mygen():
+ yield 'a'
+ yield 'b'
+ mgr = self._callFUT(mygen)
+ try:
+ with mgr() as ctx:
+ self.assertEqual(ctx, 'a')
+ except RuntimeError:
+ pass
+ else: # pragma: no cover
+ raise AssertionError('expected raise from multiple yields')
+
+ def test_with_regular_fn(self):
+ def mygen():
+ return 'a'
+ mgr = self._callFUT(mygen)
+ with mgr() as ctx:
+ self.assertEqual(ctx, 'a')
+
+
+class Test_takes_one_arg(unittest.TestCase):
+ def _callFUT(self, view, attr=None, argname=None):
+ from pyramid.util import takes_one_arg
+ return takes_one_arg(view, attr=attr, argname=argname)
+
+ def test_requestonly_newstyle_class_no_init(self):
+ class foo(object):
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_requestonly_newstyle_class_init_toomanyargs(self):
+ class foo(object):
+ def __init__(self, context, request):
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_requestonly_newstyle_class_init_onearg_named_request(self):
+ class foo(object):
+ def __init__(self, request):
+ """ """
+ self.assertTrue(self._callFUT(foo))
+
+ def test_newstyle_class_init_onearg_named_somethingelse(self):
+ class foo(object):
+ def __init__(self, req):
+ """ """
+ self.assertTrue(self._callFUT(foo))
+
+ def test_newstyle_class_init_defaultargs_firstname_not_request(self):
+ class foo(object):
+ def __init__(self, context, request=None):
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_newstyle_class_init_defaultargs_firstname_request(self):
+ class foo(object):
+ def __init__(self, request, foo=1, bar=2):
+ """ """
+ self.assertTrue(self._callFUT(foo, argname='request'))
+
+ def test_newstyle_class_init_firstname_request_with_secondname(self):
+ class foo(object):
+ def __init__(self, request, two):
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_newstyle_class_init_noargs(self):
+ class foo(object):
+ def __init__():
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_oldstyle_class_no_init(self):
+ class foo:
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_oldstyle_class_init_toomanyargs(self):
+ class foo:
+ def __init__(self, context, request):
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_oldstyle_class_init_onearg_named_request(self):
+ class foo:
+ def __init__(self, request):
+ """ """
+ self.assertTrue(self._callFUT(foo))
+
+ def test_oldstyle_class_init_onearg_named_somethingelse(self):
+ class foo:
+ def __init__(self, req):
+ """ """
+ self.assertTrue(self._callFUT(foo))
+
+ def test_oldstyle_class_init_defaultargs_firstname_not_request(self):
+ class foo:
+ def __init__(self, context, request=None):
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_oldstyle_class_init_defaultargs_firstname_request(self):
+ class foo:
+ def __init__(self, request, foo=1, bar=2):
+ """ """
+ self.assertTrue(self._callFUT(foo, argname='request'), True)
+
+ def test_oldstyle_class_init_noargs(self):
+ class foo:
+ def __init__():
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_function_toomanyargs(self):
+ def foo(context, request):
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_function_with_attr_false(self):
+ def bar(context, request):
+ """ """
+ def foo(context, request):
+ """ """
+ foo.bar = bar
+ self.assertFalse(self._callFUT(foo, 'bar'))
+
+ def test_function_with_attr_true(self):
+ def bar(context, request):
+ """ """
+ def foo(request):
+ """ """
+ foo.bar = bar
+ self.assertTrue(self._callFUT(foo, 'bar'))
+
+ def test_function_onearg_named_request(self):
+ def foo(request):
+ """ """
+ self.assertTrue(self._callFUT(foo))
+
+ def test_function_onearg_named_somethingelse(self):
+ def foo(req):
+ """ """
+ self.assertTrue(self._callFUT(foo))
+
+ def test_function_defaultargs_firstname_not_request(self):
+ def foo(context, request=None):
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_function_defaultargs_firstname_request(self):
+ def foo(request, foo=1, bar=2):
+ """ """
+ self.assertTrue(self._callFUT(foo, argname='request'))
+
+ def test_function_noargs(self):
+ def foo():
+ """ """
+ self.assertFalse(self._callFUT(foo))
+
+ def test_instance_toomanyargs(self):
+ class Foo:
+ def __call__(self, context, request):
+ """ """
+ foo = Foo()
+ self.assertFalse(self._callFUT(foo))
+
+ def test_instance_defaultargs_onearg_named_request(self):
+ class Foo:
+ def __call__(self, request):
+ """ """
+ foo = Foo()
+ self.assertTrue(self._callFUT(foo))
+
+ def test_instance_defaultargs_onearg_named_somethingelse(self):
+ class Foo:
+ def __call__(self, req):
+ """ """
+ foo = Foo()
+ self.assertTrue(self._callFUT(foo))
+
+ def test_instance_defaultargs_firstname_not_request(self):
+ class Foo:
+ def __call__(self, context, request=None):
+ """ """
+ foo = Foo()
+ self.assertFalse(self._callFUT(foo))
+
+ def test_instance_defaultargs_firstname_request(self):
+ class Foo:
+ def __call__(self, request, foo=1, bar=2):
+ """ """
+ foo = Foo()
+ self.assertTrue(self._callFUT(foo, argname='request'), True)
+
+ def test_instance_nocall(self):
+ class Foo: pass
+ foo = Foo()
+ self.assertFalse(self._callFUT(foo))
+
+ def test_method_onearg_named_request(self):
+ class Foo:
+ def method(self, request):
+ """ """
+ foo = Foo()
+ self.assertTrue(self._callFUT(foo.method))
+
+ def test_function_annotations(self):
+ def foo(bar):
+ """ """
+ # avoid SyntaxErrors in python2, this if effectively nop
+ getattr(foo, '__annotations__', {}).update({'bar': 'baz'})
+ self.assertTrue(self._callFUT(foo))
+
+
+class TestSimpleSerializer(unittest.TestCase):
+ def _makeOne(self):
+ from pyramid.util import SimpleSerializer
+ return SimpleSerializer()
+
+ def test_loads(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.loads(b'abc'), text_('abc'))
+
+ def test_dumps(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.dumps('abc'), bytes_('abc'))
diff --git a/pyramid/util.py b/pyramid/util.py
index 09a3e530f..6655455bf 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -1,4 +1,4 @@
-import contextlib
+from contextlib import contextmanager
import functools
try:
# py2.7.7+ and py3.3+ have native comparison support
@@ -6,26 +6,25 @@ try:
except ImportError: # pragma: no cover
compare_digest = None
import inspect
-import traceback
import weakref
-from zope.interface import implementer
-
from pyramid.exceptions import (
ConfigurationError,
CyclicDependencyError,
)
from pyramid.compat import (
+ getargspec,
+ im_func,
is_nonstr_iter,
integer_types,
string_types,
+ bytes_,
text_,
PY2,
native_
)
-from pyramid.interfaces import IActionInfo
from pyramid.path import DottedNameResolver as _DottedNameResolver
_marker = object()
@@ -528,76 +527,6 @@ class TopologicalSorter(object):
return result
-def viewdefaults(wrapped):
- """ Decorator for add_view-like methods which takes into account
- __view_defaults__ attached to view it is passed. Not a documented API but
- used by some external systems."""
- def wrapper(self, *arg, **kw):
- defaults = {}
- if arg:
- view = arg[0]
- else:
- view = kw.get('view')
- view = self.maybe_dotted(view)
- if inspect.isclass(view):
- defaults = getattr(view, '__view_defaults__', {}).copy()
- if '_backframes' not in kw:
- kw['_backframes'] = 1 # for action_method
- defaults.update(kw)
- return wrapped(self, *arg, **defaults)
- return functools.wraps(wrapped)(wrapper)
-
-@implementer(IActionInfo)
-class ActionInfo(object):
- def __init__(self, file, line, function, src):
- self.file = file
- self.line = line
- self.function = function
- self.src = src
-
- def __str__(self):
- srclines = self.src.split('\n')
- src = '\n'.join(' %s' % x for x in srclines)
- return 'Line %s of file %s:\n%s' % (self.line, self.file, src)
-
-def action_method(wrapped):
- """ Wrapper to provide the right conflict info report data when a method
- that calls Configurator.action calls another that does the same. Not a
- documented API but used by some external systems."""
- def wrapper(self, *arg, **kw):
- if self._ainfo is None:
- self._ainfo = []
- info = kw.pop('_info', None)
- # backframes for outer decorators to actionmethods
- backframes = kw.pop('_backframes', 0) + 2
- if is_nonstr_iter(info) and len(info) == 4:
- # _info permitted as extract_stack tuple
- info = ActionInfo(*info)
- if info is None:
- try:
- f = traceback.extract_stack(limit=4)
-
- # Work around a Python 3.5 issue whereby it would insert an
- # extra stack frame. This should no longer be necessary in
- # Python 3.5.1
- last_frame = ActionInfo(*f[-1])
- if last_frame.function == 'extract_stack': # pragma: no cover
- f.pop()
- info = ActionInfo(*f[-backframes])
- except Exception: # pragma: no cover
- info = ActionInfo(None, 0, '', '')
- self._ainfo.append(info)
- try:
- result = wrapped(self, *arg, **kw)
- finally:
- self._ainfo.pop()
- return result
-
- if hasattr(wrapped, '__name__'):
- functools.update_wrapper(wrapper, wrapped)
- wrapper.__docobj__ = wrapped
- return wrapper
-
def get_callable_name(name):
"""
@@ -613,7 +542,7 @@ def get_callable_name(name):
)
raise ConfigurationError(msg % name)
-@contextlib.contextmanager
+@contextmanager
def hide_attrs(obj, *attrs):
"""
Temporarily delete object attrs and restore afterward.
@@ -648,3 +577,75 @@ def is_same_domain(host, pattern):
return (pattern[0] == "." and
(host.endswith(pattern) or host == pattern[1:]) or
pattern == host)
+
+
+def make_contextmanager(fn):
+ if inspect.isgeneratorfunction(fn):
+ return contextmanager(fn)
+
+ if fn is None:
+ fn = lambda *a, **kw: None
+
+ @contextmanager
+ @functools.wraps(fn)
+ def wrapper(*a, **kw):
+ yield fn(*a, **kw)
+ return wrapper
+
+
+def takes_one_arg(callee, attr=None, argname=None):
+ ismethod = False
+ if attr is None:
+ attr = '__call__'
+ if inspect.isroutine(callee):
+ fn = callee
+ elif inspect.isclass(callee):
+ try:
+ fn = callee.__init__
+ except AttributeError:
+ return False
+ ismethod = hasattr(fn, '__call__')
+ else:
+ try:
+ fn = getattr(callee, attr)
+ except AttributeError:
+ return False
+
+ try:
+ argspec = getargspec(fn)
+ except TypeError:
+ return False
+
+ args = argspec[0]
+
+ if hasattr(fn, im_func) or ismethod:
+ # it's an instance method (or unbound method on py2)
+ if not args:
+ return False
+ args = args[1:]
+
+ if not args:
+ return False
+
+ if len(args) == 1:
+ return True
+
+ if argname:
+
+ defaults = argspec[3]
+ if defaults is None:
+ defaults = ()
+
+ if args[0] == argname:
+ if len(args) - len(defaults) == 1:
+ return True
+
+ return False
+
+
+class SimpleSerializer(object):
+ def loads(self, bstruct):
+ return native_(bstruct)
+
+ def dumps(self, appstruct):
+ return bytes_(appstruct)
diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py
index d2869b162..d914a4752 100644
--- a/pyramid/viewderivers.py
+++ b/pyramid/viewderivers.py
@@ -28,18 +28,14 @@ from pyramid.compat import (
is_unbound_method,
)
-from pyramid.config.util import (
- DEFAULT_PHASH,
- MAX_ORDER,
- takes_one_arg,
- )
-
from pyramid.exceptions import (
ConfigurationError,
- PredicateMismatch,
)
from pyramid.httpexceptions import HTTPForbidden
-from pyramid.util import object_description
+from pyramid.util import (
+ object_description,
+ takes_one_arg,
+)
from pyramid.view import render_view_to_response
from pyramid import renderers
@@ -353,49 +349,6 @@ def _authdebug_view(view, info):
return wrapped_view
-def predicated_view(view, info):
- preds = info.predicates
- if not preds:
- return view
- def predicate_wrapper(context, request):
- for predicate in preds:
- if not predicate(context, request):
- view_name = getattr(view, '__name__', view)
- raise PredicateMismatch(
- 'predicate mismatch for view %s (%s)' % (
- view_name, predicate.text()))
- return view(context, request)
- def checker(context, request):
- return all((predicate(context, request) for predicate in
- preds))
- predicate_wrapper.__predicated__ = checker
- predicate_wrapper.__predicates__ = preds
- return predicate_wrapper
-
-def attr_wrapped_view(view, info):
- accept, order, phash = (info.options.get('accept', None),
- getattr(info, 'order', MAX_ORDER),
- getattr(info, 'phash', DEFAULT_PHASH))
- # this is a little silly but we don't want to decorate the original
- # function with attributes that indicate accept, order, and phash,
- # so we use a wrapper
- if (
- (accept is None) and
- (order == MAX_ORDER) and
- (phash == DEFAULT_PHASH)
- ):
- return view # defaults
- def attr_view(context, request):
- return view(context, request)
- attr_view.__accept__ = accept
- attr_view.__order__ = order
- attr_view.__phash__ = phash
- attr_view.__view_attr__ = info.options.get('attr')
- attr_view.__permission__ = info.options.get('permission')
- return attr_view
-
-attr_wrapped_view.options = ('accept', 'attr', 'permission')
-
def rendered_view(view, info):
# one way or another this wrapper must produce a Response (unless
# the renderer is a NullRendererHelper)
diff --git a/tox.ini b/tox.ini
index 50ff4f166..758d4216b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
[tox]
envlist =
lint,
- py27,py34,py35,py36,pypy,
+ py27,py34,py35,py36,pypy,pypy3,
docs,{py2,py3}-cover,coverage,
[testenv]
@@ -14,13 +14,16 @@ basepython =
py36: python3.6
py37: python3.7
pypy: pypy
+ pypy3: pypy3
py2: python2.7
py3: python3.5
commands =
- pip install -q pyramid[testing]
nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:}
+extras =
+ testing
+
[testenv:py27-scaffolds]
basepython = python2.7
commands =
@@ -67,34 +70,38 @@ deps =
basepython = python3.5
whitelist_externals = make
commands =
- pip install pyramid[docs]
make -C docs doctest html epub BUILDDIR={envdir} "SPHINXOPTS=-W -E"
+extras =
+ docs
[testenv:pdf]
basepython = python3.5
whitelist_externals = make
commands =
- pip install pyramid[docs]
make -C docs latexpdf BUILDDIR={envdir} "SPHINXOPTS=-W -E"
+extras =
+ docs
# we separate coverage into its own testenv because a) "last run wins" wrt
# cobertura jenkins reporting and b) pypy and jython can't handle any
# combination of versions of coverage and nosexcover that i can find.
[testenv:py2-cover]
commands =
- pip install -q pyramid[testing]
coverage run --source=pyramid {envbindir}/nosetests
coverage xml -o coverage-py2.xml
setenv =
COVERAGE_FILE=.coverage.py2
+extras =
+ testing
[testenv:py3-cover]
commands =
- pip install -q pyramid[testing]
coverage run --source=pyramid {envbindir}/nosetests
coverage xml -o coverage-py3.xml
setenv =
COVERAGE_FILE=.coverage.py3
+extras =
+ testing
[testenv:coverage]
skip_install = True