Steve Piercy
2016-12-23 b29848f9d7b49715c1027c7361bb68e03707deae
wiki2/*.rst first cut from comparing across branches
9 files modified
597 ■■■■■ changed files
docs/tutorials/wiki2/authentication.rst 2 ●●● patch | view | raw | blame | history
docs/tutorials/wiki2/basiclayout.rst 14 ●●●● patch | view | raw | blame | history
docs/tutorials/wiki2/definingmodels.rst 135 ●●●● patch | view | raw | blame | history
docs/tutorials/wiki2/definingviews.rst 12 ●●●●● patch | view | raw | blame | history
docs/tutorials/wiki2/design.rst 33 ●●●●● patch | view | raw | blame | history
docs/tutorials/wiki2/distributing.rst 2 ●●● patch | view | raw | blame | history
docs/tutorials/wiki2/index.rst 7 ●●●● patch | view | raw | blame | history
docs/tutorials/wiki2/installation.rst 388 ●●●●● patch | view | raw | blame | history
docs/tutorials/wiki2/tests.rst 4 ●●●● patch | view | raw | blame | history
docs/tutorials/wiki2/authentication.rst
@@ -92,7 +92,7 @@
the file ``development.ini`` and add the highlighted line below:
.. literalinclude:: src/authentication/development.ini
   :lines: 18-20
   :lines: 17-19
   :emphasize-lines: 3
   :lineno-match:
   :language: ini
docs/tutorials/wiki2/basiclayout.rst
@@ -4,7 +4,7 @@
Basic Layout
============
The starter files generated by the ``alchemy`` scaffold are very basic, but
The starter files generated by the ``alchemy`` cookiecutter are very basic, but
they provide a good orientation for the high-level patterns common to most
:term:`URL dispatch`-based :app:`Pyramid` projects.
@@ -29,7 +29,6 @@
.. literalinclude:: src/basiclayout/tutorial/__init__.py
  :end-before: main
  :linenos:
  :lineno-match:
  :language: py
@@ -38,7 +37,6 @@
.. literalinclude:: src/basiclayout/tutorial/__init__.py
  :pyobject: main
  :linenos:
  :lineno-match:
  :language: py
@@ -179,7 +177,7 @@
Without being processed by ``scan``, the decorator effectively does nothing.
``@view_config`` is inert without being detected via a :term:`scan`.
The sample ``my_view()`` created by the scaffold uses a ``try:`` and
The sample ``my_view()`` created by the cookiecutter uses a ``try:`` and
``except:`` clause to detect if there is a problem accessing the project
database and provide an alternate error response.  That response will include
the text shown at the end of the file, which will be displayed in the browser
@@ -191,7 +189,7 @@
In an SQLAlchemy-based application, a *model* object is an object composed by
querying the SQL database. The ``models`` package is where the ``alchemy``
scaffold put the classes that implement our models.
cookiecutter put the classes that implement our models.
First, open ``tutorial/models/meta.py``, which should already contain the
following:
@@ -222,7 +220,6 @@
.. literalinclude:: src/basiclayout/tutorial/models/meta.py
  :lines: 15-16
  :lineno-match:
  :linenos:
  :language: py
Next open ``tutorial/models/mymodel.py``, which should already contain the
@@ -239,7 +236,6 @@
.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
  :pyobject: MyModel
  :lineno-match:
  :linenos:
  :language: py
Our example model does not require an ``__init__`` method because SQLAlchemy
@@ -292,7 +288,6 @@
.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
  :pyobject: get_engine
  :lineno-match:
  :linenos:
  :language: py
The function ``get_session_factory`` accepts an :term:`SQLAlchemy` database
@@ -303,7 +298,6 @@
.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
  :pyobject: get_session_factory
  :lineno-match:
  :linenos:
  :language: py
The function ``get_tm_session`` registers a database session with a transaction
@@ -314,7 +308,6 @@
.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
  :pyobject: get_tm_session
  :lineno-match:
  :linenos:
  :language: py
Finally, we define an ``includeme`` function, which is a hook for use with
@@ -328,7 +321,6 @@
.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
  :pyobject: includeme
  :lineno-match:
  :linenos:
  :language: py
That's about all there is to it regarding models, views, and initialization
docs/tutorials/wiki2/definingmodels.rst
@@ -4,7 +4,7 @@
Defining the Domain Model
=========================
The first change we'll make to our stock ``pcreate``-generated application will
The first change we'll make to our stock cookiecutter-generated application will
be to define a wiki page :term:`domain model`.
.. note::
@@ -22,10 +22,10 @@
The models code in our application will depend on a package which is not a
dependency of the original "tutorial" application.  The original "tutorial"
application was generated by the ``pcreate`` command; it doesn't know about our
application was generated by the cookiecutter; it doesn't know about our
custom application requirements.
We need to add a dependency, the ``bcrypt`` package, to our ``tutorial``
We need to add a dependency, the `bcrypt <https://pypi.python.org/pypi/bcrypt>`_ package, to our ``tutorial``
package's ``setup.py`` file by assigning this dependency to the ``requires``
parameter in the ``setup()`` function.
@@ -37,6 +37,10 @@
   :language: python
Only the highlighted line needs to be added.
.. note::
    We are using the ``bcrypt`` package from PyPI to hash our passwords securely. There are other one-way hash algorithms for passwords if bcrypt is an issue on your system. Just make sure that it's an algorithm approved for storing passwords versus a generic one-way hash.
Running ``pip install -e .``
@@ -53,31 +57,31 @@
.. code-block:: bash
   $ cd tutorial
   $ $VENV/bin/pip install -e .
    $ $VENV/bin/pip install -e .
On Windows:
.. code-block:: doscon
   c:\pyramidtut> cd tutorial
   c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e .
    c:\tutorial> %VENV%\Scripts\pip install -e .
Success executing this command will end with a line to the console something
like this::
like the following.
   Successfully installed bcrypt-2.0.0 cffi-1.5.2 pycparser-2.14 tutorial-0.0
.. code-block:: text
    Successfully installed bcrypt-3.1.2 cffi-1.9.1 pycparser-2.17 tutorial
Remove ``mymodel.py``
---------------------
=====================
Let's delete the file ``tutorial/models/mymodel.py``. The ``MyModel`` class is
only a sample and we're not going to use it.
Add ``user.py``
---------------
===============
Create a new file ``tutorial/models/user.py`` with the following contents:
@@ -98,12 +102,12 @@
map to columns in the ``users`` table. The ``id`` attribute will be the primary
key in the table. The ``name`` attribute will be a text column, each value of
which needs to be unique within the column. The ``password_hash`` is a nullable
text attribute that will contain a securely hashed password [1]_. Finally, the
text attribute that will contain a securely hashed password. Finally, the
``role`` text attribute will hold the role of the user.
There are two helper methods that will help us later when using the user
objects. The first is ``set_password`` which will take a raw password and
transform it using bcrypt_ into an irreversible representation, a process known
transform it using ``bcrypt`` into an irreversible representation, a process known
as "hashing". The second method, ``check_password``, will allow us to compare
the hashed value of the submitted password against the hashed value of the
password stored in the user's record in the database. If the two hashed values
@@ -116,7 +120,7 @@
Add ``page.py``
---------------
===============
Create a new file ``tutorial/models/page.py`` with the following contents:
@@ -138,7 +142,7 @@
Edit ``models/__init__.py``
---------------------------
===========================
Since we are using a package for our models, we also need to update our
``__init__.py`` file to ensure that the models are attached to the metadata.
@@ -155,12 +159,16 @@
Edit ``scripts/initializedb.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 ``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 [2]_.
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
``initializedb.py`` script.  In particular, we'll replace our import of
@@ -180,7 +188,7 @@
Installing the project and re-initializing the database
-------------------------------------------------------
=======================================================
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
@@ -191,53 +199,53 @@
.. code-block:: bash
   2016-05-22 04:12:09,226 INFO  [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
   2016-05-22 04:12:09,226 INFO  [sqlalchemy.engine.base.Engine:1193][MainThread] ()
   2016-05-22 04:12:09,226 INFO  [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
   2016-05-22 04:12:09,227 INFO  [sqlalchemy.engine.base.Engine:1193][MainThread] ()
   2016-05-22 04:12:09,227 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("users")
   2016-05-22 04:12:09,227 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ()
   2016-05-22 04:12:09,228 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("pages")
   2016-05-22 04:12:09,228 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ()
   2016-05-22 04:12:09,229 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread]
   CREATE TABLE users (
       id INTEGER NOT NULL,
       name TEXT NOT NULL,
       role TEXT NOT NULL,
       password_hash TEXT,
       CONSTRAINT pk_users PRIMARY KEY (id),
       CONSTRAINT uq_users_name UNIQUE (name)
   )
    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]
    CREATE TABLE users (
            id INTEGER NOT NULL,
            name TEXT NOT NULL,
            role TEXT NOT NULL,
            password_hash TEXT,
            CONSTRAINT pk_users PRIMARY KEY (id),
            CONSTRAINT uq_users_name UNIQUE (name)
    )
   2016-05-22 04:12:09,229 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ()
   2016-05-22 04:12:09,230 INFO  [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
   2016-05-22 04:12:09,230 INFO  [sqlalchemy.engine.base.Engine:1097][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)
   )
    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]
    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)
    )
   2016-05-22 04:12:09,231 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ()
   2016-05-22 04:12:09,231 INFO  [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
   2016-05-22 04:12:09,782 INFO  [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
   2016-05-22 04:12:09,783 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
   2016-05-22 04:12:09,784 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ('editor', 'editor', b'$2b$12$K/WLVKRl5fMAb6UM58ueTetXlE3rlc5cRK5zFPimK598scXBR/xWC')
   2016-05-22 04:12:09,784 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
   2016-05-22 04:12:09,784 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ('basic', 'basic', b'$2b$12$JfwLyCJGv3t.RTSmIrh3B.FKXRT9FevkAqafWdK5oq7Hl4mgAQORe')
   2016-05-22 04:12:09,785 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
   2016-05-22 04:12:09,785 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ('FrontPage', 'This is the front page', 1)
   2016-05-22 04:12:09,786 INFO  [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
    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
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
@@ -250,14 +258,3 @@
   ImportError: cannot import name MyModel
This will also happen if you attempt to run the tests.
.. _bcrypt: https://pypi.python.org/pypi/bcrypt
.. [1] We are using the bcrypt_ package from PyPI to hash our passwords
       securely. There are other one-way hash algorithms for passwords if
       bcrypt is an issue on your system. Just make sure that it's an
       algorithm approved for storing passwords versus a generic one-way hash.
.. [2] 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.
docs/tutorials/wiki2/definingviews.rst
@@ -42,7 +42,7 @@
Static assets
-------------
=============
Our templates name static assets, including CSS and images.  We don't need
to create these files within our package's ``static`` directory because they
@@ -133,7 +133,7 @@
We added some imports, and created a regular expression to find "WikiWords".
We got rid of the ``my_view`` view function and its decorator that was added
when we originally rendered the ``alchemy`` scaffold.  It was only an example
when we originally rendered the ``alchemy`` cookiecutter.  It was only an example
and isn't relevant to our application.  We also deleted the ``db_err_msg``
string.
@@ -340,7 +340,7 @@
.. literalinclude:: src/views/tutorial/templates/layout.jinja2
   :linenos:
   :emphasize-lines: 11,35-36
   :emphasize-lines: 11,35-37
   :language: html
Since we're using a templating engine, we can factor common boilerplate out of
@@ -350,7 +350,7 @@
- We have defined two placeholders in the layout template where a child
  template can override the content. These blocks are named ``subtitle`` (line
  11) and ``content`` (line 36).
- Please refer to the Jinja2_ documentation for more information about template
- Please refer to the `Jinja2 documentation <http://jinja.pocoo.org/>`_ for more information about template
  inheritance.
@@ -436,7 +436,7 @@
  the view.
Finally, we may delete the ``tutorial/templates/mytemplate.jinja2`` template
that was provided by the ``alchemy`` scaffold, as we have created our own
that was provided by the ``alchemy`` cookiecutter, as we have created our own
templates for the wiki.
.. note::
@@ -475,5 +475,3 @@
  will generate a ``NoResultFound: No row was found for one()`` error. You'll
  see an interactive traceback facility provided by
  :term:`pyramid_debugtoolbar`.
.. _jinja2: http://jinja.pocoo.org/
docs/tutorials/wiki2/design.rst
@@ -23,22 +23,22 @@
Within the database, we will define two tables:
- The `users` table which will store the `id`, `name`, `password_hash` and
  `role` of each wiki user.
- The `pages` table, whose elements will store the wiki pages.
  There are four columns: `id`, `name`, `data` and `creator_id`.
- The ``users`` table which will store the ``id``, ``name``, ``password_hash`` and
  ``role`` of each wiki user.
- The ``pages`` table, whose elements will store the wiki pages.
  There are four columns: ``id``, ``name``, ``data`` and ``creator_id``.
There is a one-to-many relationship between `users` and `pages` tracking
the user who created each wiki page defined by the `creator_id` column on the
`pages` table.
There is a one-to-many relationship between ``users`` and ``pages`` tracking
the user who created each wiki page defined by the ``creator_id`` column on the
``pages`` table.
URLs like ``/PageName`` will try to find an element in the `pages` table that
URLs like ``/PageName`` will try to find an element in the ``pages`` table that
has a corresponding name.
To add a page to the wiki, a new row is created and the text is stored in
`data`.
``data``.
A page named ``FrontPage`` containing the text *This is the front page*, will
A page named ``FrontPage`` containing the text "This is the front page" will
be created when the storage is initialized, and will be used as the wiki home
page.
@@ -61,12 +61,12 @@
be using a very simple role-based security model. We'll assign a single
role category to each user in our system.
`basic`
  An authenticated user who can view content and create new pages. A `basic`
``basic``
  An authenticated user who can view content and create new pages. A ``basic``
  user may also edit the pages they have created but not pages created by
  other users.
`editor`
``editor``
  An authenticated user who can create and edit any content in the system.
In order to accomplish this we'll need to define an authentication policy
@@ -101,16 +101,12 @@
+----------------------+-----------------------+-------------+----------------+------------+
| URL                  |  Action               |  View       |  Template      | Permission |
|                      |                       |             |                |            |
+======================+=======================+=============+================+============+
| /                    |  Redirect to          |  view_wiki  |                |            |
|                      |  /FrontPage           |             |                |            |
+----------------------+-----------------------+-------------+----------------+------------+
| /PageName            |  Display existing     |  view_page  |  view.jinja2   |  view      |
|                      |  page [2]_            |  [1]_       |                |            |
|                      |                       |             |                |            |
|                      |                       |             |                |            |
|                      |                       |             |                |            |
+----------------------+-----------------------+-------------+----------------+------------+
| /PageName/edit_page  |  Display edit form    |  edit_page  |  edit.jinja2   |  edit      |
|                      |  with existing        |             |                |            |
@@ -149,14 +145,13 @@
|                      |    login form with    |             |                |            |
|                      |    "login failed"     |             |                |            |
|                      |    message.           |             |                |            |
|                      |                       |             |                |            |
+----------------------+-----------------------+-------------+----------------+------------+
| /logout              |  Redirect to          |  logout     |                |            |
|                      |  /FrontPage           |             |                |            |
+----------------------+-----------------------+-------------+----------------+------------+
.. [1] This is the default view for a Page context when there is no view name.
.. [2] Pyramid will return a default 404 Not Found page if the page *PageName*
.. [2] Pyramid will return a default 404 Not Found page if the page ``PageName``
       does not exist yet.
.. [3] ``pyramid.exceptions.Forbidden`` is reached when a user tries to invoke
       a view that is not authorized by the authorization policy.
docs/tutorials/wiki2/distributing.rst
@@ -31,7 +31,7 @@
   Creating tar archive
   removing 'tutorial-0.0' (and everything under it)
Note that this command creates a tarball in the "dist" subdirectory named
Note that this command creates a tarball in the ``dist`` subdirectory named
``tutorial-0.0.tar.gz``.  You can send this file to your friends to show them
your cool new application.  They should be able to install it by pointing the
``pip install`` command directly at it. Or you can upload it to `PyPI
docs/tutorials/wiki2/index.rst
@@ -4,13 +4,11 @@
=======================================
This tutorial introduces an :term:`SQLAlchemy` and :term:`URL dispatch`-based
:app:`Pyramid` application to a developer familiar with Python.  When the
tutorial is finished, the developer will have created a basic wiki
:app:`Pyramid` application to a developer familiar with Python.  When finished, the developer will have created a basic wiki
application with authentication and authorization.
For cut and paste purposes, the source code for all stages of this tutorial can
be browsed on GitHub at `docs/tutorials/wiki2/src
<https://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src>`_,
be browsed on `GitHub <https://github.com/Pylons/pyramid/>`_,
which corresponds to the same location if you have Pyramid sources.
.. toctree::
@@ -26,4 +24,3 @@
   authorization
   tests
   distributing
docs/tutorials/wiki2/installation.rst
@@ -15,66 +15,126 @@
* You've satisfied the :ref:`requirements-for-installing-packages`.
Create directory to contain the project
---------------------------------------
Install SQLite3 and its development packages
--------------------------------------------
We need a workspace for our project files.
If you used a package manager to install your Python or if you compiled your Python from source, then you must install SQLite3 and its development packages.  If you downloaded your Python as an installer from https://www.python.org, then you already have it installed and can skip this step.
If you need to install the SQLite3 packages, then, for example, using the Debian system and ``apt-get``, the command would be the following:
.. code-block:: bash
    $ sudo apt-get install libsqlite3-dev
Install cookiecutter
--------------------
We will use a :term:`cookiecutter` to create a Python package project from a Python package project template.  See `Cookiecutter Installation <https://cookiecutter.readthedocs.io/en/latest/installation.html>`_ for instructions.
.. note::
    At the time of writing, the installation instructions for Cookiecutter suggest the optional use of ``sudo``, implying to install it in the system Python. We suggest that you install it in a virtual environment instead.
Generate a Pyramid project from a cookiecutter
----------------------------------------------
We will create a Pyramid project in your home directory for UNIX or at the root for Windows. It is assumed you know the path to where you installed ``cookiecutter``. Issue the following commands and override the defaults in the prompts as follows.
On UNIX
^^^^^^^
.. code-block:: bash
    $ mkdir ~/pyramidtut
    $ cd ~
    $ cookiecutter https://github.com/Pylons/pyramid-cookiecutter-alchemy
On Windows
^^^^^^^^^^
.. code-block:: doscon
   c:\> mkdir pyramidtut
    c:\> cd \
    c:\> cookiecutter https://github.com/Pylons/pyramid-cookiecutter-alchemy
On all operating systems
^^^^^^^^^^^^^^^^^^^^^^^^
If prompted for the first item, accept the default ``yes`` by hitting return.
#. ``You've cloned ~/.cookiecutters/pyramid-cookiecutter-alchemy before. Is it
   okay to delete and re-clone it? [yes]:``
#. ``project_name [Pyramid Scaffold]: myproj``
#. ``repo_name [scaffold]: tutorial``
Create and use a virtual Python environment
Change directory into your newly created project
------------------------------------------------
On UNIX
^^^^^^^
.. code-block:: bash
    $ cd tutorial
On Windows
^^^^^^^^^^
.. code-block:: doscon
    c:\> cd tutorial
Set and use a ``VENV`` environment variable
-------------------------------------------
Next let's create a virtual environment workspace for our project.  We will use
the ``VENV`` environment variable instead of the absolute path of the virtual
environment.
We will set the ``VENV`` environment variable to the absolute path of the virtual environment, and use it going forward.
On UNIX
^^^^^^^
.. code-block:: bash
   $ export VENV=~/pyramidtut
   $ python3 -m venv $VENV
    $ export VENV=~/tutorial
On Windows
^^^^^^^^^^
.. code-block:: doscon
   c:\> set VENV=c:\pyramidtut
    c:\tutorial> set VENV=c:\tutorial
Each version of Python uses different paths, so you will need to adjust the
path to the command for your Python version.
Create a virtual environment
----------------------------
On UNIX
^^^^^^^
.. code-block:: bash
    $ python3 -m venv $VENV
On Windows
^^^^^^^^^^
Each version of Python uses different paths, so you will need to adjust the path to the command for your Python version.
Python 2.7:
.. code-block:: doscon
   c:\> c:\Python27\Scripts\virtualenv %VENV%
    c:\tutorial> c:\Python27\Scripts\virtualenv %VENV%
Python 3.6:
.. code-block:: doscon
   c:\> c:\Python35\Scripts\python -m venv %VENV%
    c:\tutorial> c:\Python36\Scripts\python -m venv %VENV%
Upgrade ``pip`` and ``setuptools`` in the virtual environment
-------------------------------------------------------------
Upgrade packaging tools in the virtual environment
--------------------------------------------------
On UNIX
^^^^^^^
@@ -88,105 +148,7 @@
.. code-block:: doscon
   c:\> %VENV%\Scripts\pip install --upgrade pip setuptools
Install Pyramid into the virtual Python environment
---------------------------------------------------
On UNIX
^^^^^^^
.. parsed-literal::
   $ $VENV/bin/pip install "pyramid==\ |release|\ "
On Windows
^^^^^^^^^^
.. parsed-literal::
   c:\\> %VENV%\\Scripts\\pip install "pyramid==\ |release|\ "
Install SQLite3 and its development packages
--------------------------------------------
If you used a package manager to install your Python or if you compiled
your Python from source, then you must install SQLite3 and its
development packages.  If you downloaded your Python as an installer
from https://www.python.org, then you already have it installed and can skip
this step.
If you need to install the SQLite3 packages, then, for example, using
the Debian system and ``apt-get``, the command would be the following:
.. code-block:: bash
   $ sudo apt-get install libsqlite3-dev
Change directory to your virtual Python environment
---------------------------------------------------
Change directory to the ``pyramidtut`` directory, which is both your workspace
and your virtual environment.
On UNIX
^^^^^^^
.. code-block:: bash
   $ cd pyramidtut
On Windows
^^^^^^^^^^
.. code-block:: doscon
   c:\> cd pyramidtut
.. _sql_making_a_project:
Making a project
----------------
Your next step is to create a project.  For this tutorial we will use
the :term:`scaffold` named ``alchemy`` which generates an application
that uses :term:`SQLAlchemy` and :term:`URL dispatch`.
:app:`Pyramid` supplies a variety of scaffolds to generate sample projects. We
will use ``pcreate``, a script that comes with Pyramid, to create our project
using a scaffold.
By passing ``alchemy`` into the ``pcreate`` command, the script creates the
files needed to use SQLAlchemy. By passing in our application name
``tutorial``, the script inserts that application name into all the required
files. For example, ``pcreate`` creates the ``initialize_tutorial_db`` in the
``pyramidtut/bin`` directory.
The below instructions assume your current working directory is "pyramidtut".
On UNIX
^^^^^^^
.. code-block:: bash
   $ $VENV/bin/pcreate -s alchemy tutorial
On Windows
^^^^^^^^^^
.. code-block:: doscon
   c:\pyramidtut> %VENV%\Scripts\pcreate -s alchemy tutorial
.. note:: If you are using Windows, the ``alchemy`` scaffold may not deal
   gracefully with installation into a location that contains spaces in the
   path. If you experience startup problems, try putting both the virtual
   environment and the project into directories that do not contain spaces in
   their paths.
    c:\tutorial> %VENV%\Scripts\pip install --upgrade pip setuptools
.. _installing_project_in_dev_mode:
@@ -194,74 +156,49 @@
Installing the project in development mode
------------------------------------------
In order to do development on the project easily, you must "register" the
project as a development egg in your workspace using the ``pip install -e .``
command. In order to do so, change directory to the ``tutorial`` directory that
you created in :ref:`sql_making_a_project`, and run the ``pip install -e .``
command using the virtual environment Python interpreter.
In order to do development on the project easily, you must "register" the project as a development egg in your workspace. We will install testing requirements at the same time. We do so with the following command.
On UNIX
^^^^^^^
.. code-block:: bash
   $ cd tutorial
   $ $VENV/bin/pip install -e .
    $ $VENV/bin/pip install -e ".[testing]"
On Windows
^^^^^^^^^^
.. code-block:: doscon
   c:\pyramidtut> cd tutorial
   c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e .
    c:\tutorial> %VENV%\Scripts\pip install -e ".[testing]"
The console will show ``pip`` checking for packages and installing missing
packages. Success executing this command will show a line like the following:
On all operating systems
^^^^^^^^^^^^^^^^^^^^^^^^
The console will show ``pip`` checking for packages and installing missing packages. Success executing this command will show a line like the following:
.. code-block:: bash
   Successfully installed Chameleon-2.24 Mako-1.0.4 MarkupSafe-0.23 \
   Pygments-2.1.3 SQLAlchemy-1.0.12 pyramid-chameleon-0.3 \
   pyramid-debugtoolbar-2.4.2 pyramid-mako-1.0.2 pyramid-tm-0.12.1 \
   transaction-1.4.4 tutorial waitress-0.8.10 zope.sqlalchemy-0.7.6
    Successfully installed Jinja2-2.8 Mako-1.0.6 MarkupSafe-0.23 \
    PasteDeploy-1.5.2 Pygments-2.1.3 SQLAlchemy-1.1.4 WebOb-1.6.3 \
    WebTest-2.0.24 beautifulsoup4-4.5.1 coverage-4.2 py-1.4.32 pyramid-1.7.3 \
    pyramid-debugtoolbar-3.0.5 pyramid-jinja2-2.7 pyramid-mako-1.0.2 \
    pyramid-tm-1.1.1 tutorial pytest-3.0.5 pytest-cov-2.4.0 repoze.lru-0.6 \
    six-1.10.0 transaction-2.0.3 translationstring-1.3 venusian-1.0 \
    waitress-1.0.1 zope.deprecation-4.2.0 zope.interface-4.3.3 \
    zope.sqlalchemy-0.7.7
.. _install-testing-requirements:
Install testing requirements
----------------------------
In order to run tests, we need to install the testing requirements. This is
done through our project's ``setup.py`` file, in the ``tests_require`` and
``extras_require`` stanzas, and by issuing the command below for your
operating system.
Testing requirements are defined in our project's ``setup.py`` file, in the ``tests_require`` and ``extras_require`` stanzas.
.. literalinclude:: src/installation/setup.py
   :language: python
   :linenos:
   :lineno-start: 22
   :lineno-match:
   :lines: 22-26
.. literalinclude:: src/installation/setup.py
   :language: python
   :linenos:
   :lineno-start: 45
   :lines: 45-47
On UNIX
^^^^^^^
.. code-block:: bash
   $ $VENV/bin/pip install -e ".[testing]"
On Windows
^^^^^^^^^^
.. code-block:: doscon
   c:\pyramidtut\tutorial> %VENV%\Scripts\pip install -e ".[testing]"
   :lineno-match:
   :lines: 46-48
.. _sql_running_tests:
@@ -286,7 +223,7 @@
.. code-block:: doscon
   c:\pyramidtut\tutorial> %VENV%\Scripts\py.test -q
   c:\tutorial> %VENV%\Scripts\py.test -q
For a successful test run, you should see output that ends like this:
@@ -301,7 +238,7 @@
You can run the ``py.test`` command to see test coverage information. This
runs the tests in the same way that ``py.test`` does, but provides additional
"coverage" information, exposing which lines of your project are covered by the
:term:`coverage` information, exposing which lines of your project are covered by the
tests.
We've already installed the ``pytest-cov`` package into our virtual
@@ -319,46 +256,45 @@
.. code-block:: doscon
   c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov \
       --cov-report=term-missing
   c:\tutorial> %VENV%\Scripts\py.test --cov --cov-report=term-missing
If successful, you will see output something like this:
.. code-block:: bash
   ======================== test session starts ========================
   platform Python 3.6.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1
   rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile:
   plugins: cov-2.2.1
   collected 2 items
    ======================== 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
    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%
   ===================== 2 passed in 0.57 seconds ======================
    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%
    ===================== 2 passed in 0.57 seconds ======================
Our package doesn't quite have 100% test coverage.
.. _test_and_coverage_scaffold_defaults_sql:
.. _test_and_coverage_cookiecutter_defaults_sql:
Test and coverage scaffold defaults
-----------------------------------
Test and coverage cookiecutter defaults
---------------------------------------
Scaffolds include configuration defaults for ``py.test`` and test coverage.
Cookiecutters include configuration defaults for ``py.test`` and test coverage.
These configuration files are ``pytest.ini`` and ``.coveragerc``, located at
the root of your package. Without these defaults, we would need to specify the
path to the module on which we want to run tests and coverage.
@@ -375,11 +311,10 @@
.. code-block:: doscon
   c:\pyramidtut\tutorial> %VENV%\Scripts\py.test --cov=tutorial \
       --cov-report=term-missing tutorial\tests.py -q
   c:\tutorial> %VENV%\Scripts\py.test --cov=tutorial tutorial\tests.py -q
py.test follows :ref:`conventions for Python test discovery
<pytest:test discovery>`, and the configuration defaults from the scaffold
<pytest:test discovery>`, and the configuration defaults from the cookiecutter
tell ``py.test`` where to find the module on which we want to run tests and
coverage.
@@ -417,36 +352,36 @@
.. code-block:: doscon
   c:\pyramidtut\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini
   c:\tutorial> %VENV%\Scripts\initialize_tutorial_db development.ini
The output to your console should be something like this:
.. code-block:: bash
   2016-05-22 04:03:28,888 INFO  [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
   2016-05-22 04:03:28,888 INFO  [sqlalchemy.engine.base.Engine:1193][MainThread] ()
   2016-05-22 04:03:28,888 INFO  [sqlalchemy.engine.base.Engine:1192][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
   2016-05-22 04:03:28,889 INFO  [sqlalchemy.engine.base.Engine:1193][MainThread] ()
   2016-05-22 04:03:28,890 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread] PRAGMA table_info("models")
   2016-05-22 04:03:28,890 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ()
   2016-05-22 04:03:28,892 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread]
   CREATE TABLE models (
       id INTEGER NOT NULL,
       name TEXT,
       value INTEGER,
       CONSTRAINT pk_models PRIMARY KEY (id)
   )
    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-05-22 04:03:28,892 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ()
   2016-05-22 04:03:28,893 INFO  [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
   2016-05-22 04:03:28,893 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread] CREATE UNIQUE INDEX my_index ON models (name)
   2016-05-22 04:03:28,893 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ()
   2016-05-22 04:03:28,894 INFO  [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
   2016-05-22 04:03:28,896 INFO  [sqlalchemy.engine.base.Engine:646][MainThread] BEGIN (implicit)
   2016-05-22 04:03:28,897 INFO  [sqlalchemy.engine.base.Engine:1097][MainThread] INSERT INTO models (name, value) VALUES (?, ?)
   2016-05-22 04:03:28,897 INFO  [sqlalchemy.engine.base.Engine:1100][MainThread] ('one', 1)
   2016-05-22 04:03:28,898 INFO  [sqlalchemy.engine.base.Engine:686][MainThread] COMMIT
    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
@@ -472,7 +407,7 @@
.. code-block:: doscon
   c:\pyramidtut\tutorial> %VENV%\Scripts\pserve development.ini --reload
   c:\tutorial> %VENV%\Scripts\pserve development.ini --reload
.. note::
@@ -483,9 +418,10 @@
.. code-block:: text
   Starting subprocess with file monitor
   Starting server in PID 82349.
   serving on http://127.0.0.1:6543
    Starting subprocess with file monitor
    Starting server in PID 44078.
    Serving on http://localhost:6543
    Serving on http://localhost:6543
This means the server is ready to accept requests.
@@ -502,13 +438,15 @@
application while you develop.
Decisions the ``alchemy`` scaffold has made for you
---------------------------------------------------
Decisions the ``alchemy`` cookiecutter has made for you
-------------------------------------------------------
Creating a project using the ``alchemy`` scaffold makes the following
Creating a project using the ``alchemy`` cookiecutter makes the following
assumptions:
- You are willing to use :term:`SQLAlchemy` as a database access tool.
- You are willing to use SQLite for persistent storage, although almost any SQL database could be used with SQLAlchemy.
- You are willing to use :term:`SQLAlchemy` for a database access tool.
- You are willing to use :term:`URL dispatch` to map URLs to code.
docs/tutorials/wiki2/tests.rst
@@ -8,7 +8,7 @@
tests in a new ``tests`` subpackage.  Tests ensure that an application works,
and that it continues to work when changes are made in the future.
The file ``tests.py`` was generated as part of the ``alchemy`` scaffold, but it
The file ``tests.py`` was generated as part of the ``alchemy`` cookiecutter, but it
is a common practice to put tests into a ``tests`` subpackage, especially as
projects grow in size and complexity.  Each module in the test subpackage
should contain tests for its corresponding module in our application.  Each
@@ -89,7 +89,7 @@
.. code-block:: doscon
   c:\pyramidtut\tutorial> %VENV%\Scripts\py.test -q
   c:\tutorial> %VENV%\Scripts\py.test -q
The expected result should look like the following: