diff options
262 files changed, 9307 insertions, 1092 deletions
diff --git a/.travis.yml b/.travis.yml index 9d4324ff8..bc82c8faf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - 2.7 - pypy - 3.2 + - 3.3 script: python setup.py test -q diff --git a/CHANGES.txt b/CHANGES.txt index 8295a98c9..9f780fe45 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,142 @@ +Unreleased +========== + +Features +-------- + +- An authorization API has been added as a method of the + request: ``request.has_permission``. + + ``request.has_permission`` is a method-based alternative to the + ``pyramid.security.has_permission`` API and works exactly the same. The + older API is now deprecated. + +- Property API attributes have been added to the request for easier access to + authentication data: ``request.authenticated_userid``, + ``request.unauthenticated_userid``, and ``request.effective_principals``. + + These are analogues, respectively, of + ``pyramid.security.authenticated_userid``, + ``pyramid.security.unauthenticated_userid``, and + ``pyramid.security.effective_principals``. They operate exactly the same, + except they are attributes of the request instead of functions accepting a + request. They are properties, so they cannot be assigned to. The older + function-based APIs are now deprecated. + +- Pyramid's console scripts (``pserve``, ``pviews``, etc) can now be run + directly, allowing custom arguments to be sent to the python interpreter + at runtime. For example:: + + python -3 -m pyramid.scripts.pserve development.ini + +- Added a specific subclass of ``HTTPBadRequest`` named + ``pyramid.exceptions.BadCSRFToken`` which will now be raised in response + to failures in ``check_csrf_token``. + See https://github.com/Pylons/pyramid/pull/1149 + +- Added a new ``SignedCookieSessionFactory`` which is very similar to the + ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on + signing content. The custom serializer arguments to this function should + only focus on serializing, unlike its predecessor which required the + serializer to also perform signing. + See https://github.com/Pylons/pyramid/pull/1142 + +- Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie + factory that can be used by framework implementors to create their own + session implementations. It provides a reusable API which focuses strictly + on providing a dictionary-like object that properly handles renewals, + timeouts, and conformance with the ``ISession`` API. + See https://github.com/Pylons/pyramid/pull/1142 + +Bug Fixes +--------- + +- Fix the ``pcreate`` script so that when the target directory name ends with a + slash it does not produce a non-working project directory structure. + Previously saying ``pcreate -s starter /foo/bar/`` produced different output + than saying ``pcreate -s starter /foo/bar``. The former did not work + properly. + +- Fix the ``principals_allowed_by_permission`` method of + ``ACLAuthorizationPolicy`` so it anticipates a callable ``__acl__`` + on resources. Previously it did not try to call the ``__acl__`` + if it was callable. + +- The ``pviews`` script did not work when a url required custom request + methods in order to perform traversal. Custom methods and descriptors added + via ``pyramid.config.Configurator.add_request_method`` will now be present, + allowing traversal to continue. + See https://github.com/Pylons/pyramid/issues/1104 + +- Remove unused ``renderer`` argument from ``Configurator.add_route``. + +- Allow the ``BasicAuthenticationPolicy`` to work with non-ascii usernames + and passwords. The charset is not passed as part of the header and different + browsers alternate between UTF-8 and Latin-1, so the policy now attempts + to decode with UTF-8 first, and will fallback to Latin-1. + See https://github.com/Pylons/pyramid/pull/1170 + +- The ``@view_defaults`` now apply to notfound and forbidden views + that are defined as methods of a decorated class. + See https://github.com/Pylons/pyramid/issues/1173 + +Documentation +------------- + +- Added a "Quick Tutorial" to go with the Quick Tour + +- Removed mention of ``pyramid_beaker`` from docs. Beaker is no longer + maintained. Point people at ``pyramid_redis_sessions`` instead. + +- Add documentation for ``pyramid.interfaces.IRendererFactory`` and + ``pyramid.interfaces.IRenderer``. + +Backwards Incompatibilities +--------------------------- + +- The key/values in the ``_query`` parameter of ``request.route_url`` and the + ``query`` parameter of ``request.resource_url`` (and their variants), used + to encode a value of ``None`` as the string ``'None'``, leaving the resulting + query string to be ``a=b&key=None``. The value is now dropped in this + situation, leaving a query string of ``a=b&key=``. + See https://github.com/Pylons/pyramid/issues/1119 + +Deprecations +------------ + +- Deprecate the ``pyramid.interfaces.ITemplateRenderer`` interface. It was + ill-defined and became unused when Mako and Chameleon template bindings were + split into their own packages. + +- The ``pyramid.session.UnencryptedCookieSessionFactoryConfig`` API has been + deprecated and is superseded by the + ``pyramid.session.SignedCookieSessionFactory``. Note that while the cookies + generated by the ``UnencryptedCookieSessionFactoryConfig`` + are compatible with cookies generated by old releases, cookies generated by + the SignedCookieSessionFactory are not. See + https://github.com/Pylons/pyramid/pull/1142 + +- The ``pyramid.security.has_permission`` API is now deprecated. Instead, use + the newly-added ``has_permission`` method of the request object. + +- The ``pyramid.security.forget`` API is now deprecated. Instead, use + the newly-added ``forget_userid`` method of the request object. + +- The ``pyramid.security.remember`` API is now deprecated. Instead, use + the newly-added ``remember_userid`` method of the request object. + +- The ``pyramid.security.effective_principals`` API is now deprecated. + Instead, use the newly-added ``effective_principals`` attribute of the + request object. + +- The ``pyramid.security.authenticated_userid`` API is now deprecated. + Instead, use the newly-added ``authenticated_userid`` attribute of the + request object. + +- The ``pyramid.security.unauthenticated_userid`` API is now deprecated. + Instead, use the newly-added ``unauthenticated_userid`` attribute of the + request object. + 1.5a2 (2013-09-22) ================== @@ -247,7 +386,7 @@ Features The above example will ensure that the view is called if the request method is not POST (at least if no other view is more specific). - The :class:`pyramid.config.not_` class can be used against any value that is + The ``pyramid.config.not_`` class can be used against any value that is a predicate value passed in any of these contexts: - ``pyramid.config.Configurator.add_view`` @@ -316,9 +455,7 @@ Features In the past, only the most specific type containing views would be checked and if no matching view could be found then a PredicateMismatch would be raised. Now predicate mismatches don't hide valid views registered on - super-types. Here's an example that now works: - - .. code-block:: python + super-types. Here's an example that now works:: class IResource(Interface): diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 1a5b975d7..63528e662 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -222,3 +222,9 @@ Contributors - Takahiro Fujiwara, 2013/08/28 - Doug Hellmann, 2013/09/06 + +- Karl O. Pinc, 2013/09/27 + +- Matthew Russell, 2013/10/14 + +- Antti Haapala, 2013/11/15 diff --git a/HACKING.txt b/HACKING.txt index 4ebb59160..b32a8a957 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -6,47 +6,42 @@ Here are some guidelines about hacking on Pyramid. Using a Development Checkout ---------------------------- -Below is a quick start on creating a development environment using a Pyramid -checkout. +You'll have to create a development environment to hack on Pyramid, using a +Pyramid checkout. You can either do this by hand or, if you have ``tox`` +installed (it's on PyPI), you can (ab)use tox to get a working development +environment. Each installation method is described below. -- Create a new directory somewhere and ``cd`` to it:: - - $ mkdir ~/hack-on-pyramid - $ cd ~/hack-on-pyramid - -- Check out a read-only copy of the Pyramid source:: - - $ git clone git://github.com/Pylons/pyramid.git . - - (alternately, create a writeable fork on GitHub and check that out). +By Hand ++++++++ -Since pyramid is a framework and not an application, it can be -convenient to work against a sample application, preferably in its own -virtualenv. A quick way to achieve this is to (ab-)use ``tox`` -(http://tox.readthedocs.org/en/latest/) with a custom configuration -file that's part of the checkout:: +- Check out Pyramid from source:: - tox -c hacking-tox.ini + $ cd ~ + $ git clone git://github.com/Pylons/pyramid.git hack-on-pyramid + $ cd hack-on-pyramid -This will create a python-2.7 based virtualenv named ``env27`` (pyramid's -``.gitconfig` ignores all top-level folders that start with ``env`` specifically -for this use case) and inside that a simple pyramid application named -``hacking`` that you can then fire up like so:: +- Create a virtualenv in which to install Pyramid:: - cd env27/hacking - ../bin/pserve development.ini + $ cd ~/hack-on-pyramid + $ virtualenv -ppython2.7 env -Alternatively, if you don't want to install ``tox`` at this point, -you an achieve the same manually by following these steps: + Note that very old versions of virtualenv (virtualenv versions below, say, + 1.10 or thereabouts) require you to pass a ``--no-site-packages`` flag to + get a completely isolated environment. -- Create a virtualenv in which to install Pyramid:: + You can choose which Python version you want to use by passing a ``-p`` + flag to ``virtualenv``. For example, ``virtualenv -ppython2.7`` + chooses the Python 2.7 interpreter to be installed. - $ virtualenv env + From here on in within these instructions, the ``~/hack-on-pyramid/env`` + virtual environment you created above will be referred to as ``$VENV``. + To use the instructions in the steps that follow literally, use the + ``export VENV=~/hack-on-pyramid/env`` command. - Install ``setuptools-git`` into the virtualenv (for good measure, as we're using git to do version control):: - $ $VENV/bin/easy_install setuptools-git + $ $VENV/bin/easy_install setuptools-git - Install Pyramid from the checkout into the virtualenv using ``setup.py dev``. ``setup.py dev`` is an alias for "setup.py develop" which also @@ -54,21 +49,54 @@ you an achieve the same manually by following these steps: ``setup.py dev`` *must* be done while the current working directory is the ``pyramid`` checkout directory:: - $ cd pyramid - $ $VENV/bin/python setup.py dev + $ cd ~/hack-on-pyramid + $ $VENV/bin/python setup.py dev - At that point, you should be able to create new Pyramid projects by using ``pcreate``:: - $ cd ../env - $ $VENV/bin/pcreate -s starter starter + $ cd $VENV + $ bin/pcreate -s starter starter - And install those projects (also using ``setup.py develop``) into the virtualenv:: - $ cd starter + $ cd $VENV/starter $ $VENV/bin/python setup.py develop +Using Tox ++++++++++ + +Alternatively, if you already have ``tox`` installed, there is an easier +way to get going. + +- Create a new directory somewhere and ``cd`` to it:: + + $ mkdir ~/hack-on-pyramid + $ cd ~/hack-on-pyramid + +- Check out a read-only copy of the Pyramid source:: + + $ git clone git://github.com/Pylons/pyramid.git . + + (alternately, create a writeable fork on GitHub and check that out). + +Since Pyramid is a framework and not an application, it can be +convenient to work against a sample application, preferably in its own +virtualenv. A quick way to achieve this is to (ab-)use ``tox`` +(http://tox.readthedocs.org/en/latest/) with a custom configuration +file that's part of the checkout:: + + tox -c hacking-tox.ini + +This will create a python-2.7 based virtualenv named ``env27`` (Pyramid's +``.gitconfig` ignores all top-level folders that start with ``env`` specifically +for this use case) and inside that a simple pyramid application named +``hacking`` that you can then fire up like so:: + + cd env27/hacking + ../bin/pserve development.ini + Adding Features --------------- @@ -133,11 +161,11 @@ Running Tests it creates a virtualenv for each version/platform combination. For example:: - $ /usr/bin/easy_install tox + $ sudo /usr/bin/easy_install tox $ cd ~/hack-on-pyramid/ $ /usr/bin/tox -- The tests can also be run usign ``pytest`` (http://pytest.org/). This is +- The tests can also be run using ``pytest`` (http://pytest.org/). This is intended as a convenience for people who are more used or fond of ``pytest``. Run the tests like so:: @@ -163,18 +191,28 @@ or adds the feature. To build and review docs (where ``$VENV`` refers to the virtualenv you're using to develop Pyramid): -1. Run ``$VENV/bin/python setup.py dev docs``. This will cause Sphinx - and all development requirements to be installed in your virtualenv. +1. After following the steps above in "Using a Development Checkout", cause + Sphinx and all development requirements to be installed in your + virtualenv:: + + $ cd ~/hack-on-pyramid + $ $VENV/bin/python setup.py docs 2. Update all git submodules from the top-level of your Pyramid checkout, like - so: - git submodule update --init --recursive + so:: + + $ git submodule update --init --recursive + This will checkout theme subrepositories and prevent error conditions when HTML docs are generated. 3. cd to the ``docs`` directory within your Pyramid checkout and execute - ``make clean html SPHINXBUILD=$VENV/bin/sphinx-build``. The - ``SPHINXBUILD=...`` hair is there in order to tell it to use the + the ``make`` command with some flags:: + + $ cd ~/hack-on-pyramid/pyramid/docs + $ make clean html SPHINXBUILD=$VENV/bin/sphinx-build + + The ``SPHINXBUILD=...`` hair is there in order to tell it to use the virtualenv Python, which will have both Sphinx and Pyramid (for API documentation generation) installed. diff --git a/docs/_static/directory_structure_generic.png b/docs/_static/directory_structure_generic.png Binary files differnew file mode 100644 index 000000000..c6d1a5b03 --- /dev/null +++ b/docs/_static/directory_structure_generic.png diff --git a/docs/_static/directory_structure_initial.png b/docs/_static/directory_structure_initial.png Binary files differnew file mode 100644 index 000000000..000f1bb27 --- /dev/null +++ b/docs/_static/directory_structure_initial.png diff --git a/docs/_static/directory_structure_pyramid.png b/docs/_static/directory_structure_pyramid.png Binary files differnew file mode 100644 index 000000000..74edd6533 --- /dev/null +++ b/docs/_static/directory_structure_pyramid.png diff --git a/docs/_themes b/docs/_themes -Subproject f3acb7cfd1ab69510bc202676dc1d8f32128295 +Subproject 26732645619b372764097e5e8086f89871d90c0 diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst index ab158f18d..0c630571f 100644 --- a/docs/api/exceptions.rst +++ b/docs/api/exceptions.rst @@ -5,6 +5,8 @@ .. automodule:: pyramid.exceptions + .. autoclass:: BadCSRFToken + .. autoclass:: PredicateMismatch .. autoclass:: Forbidden diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 6a08d1048..b50f10beb 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -7,9 +7,9 @@ .. attribute:: status_map - A mapping of integer status code to exception class (eg. the - integer "401" maps to - :class:`pyramid.httpexceptions.HTTPUnauthorized`). + A mapping of integer status code to HTTP exception class (eg. the integer + "401" maps to :class:`pyramid.httpexceptions.HTTPUnauthorized`). All + mapped exception classes are children of :class:`pyramid.httpexceptions`, .. autofunction:: exception_response diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index 1dea5fab0..d8d935afd 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -50,7 +50,10 @@ Other Interfaces .. autointerface:: IRendererInfo :members: - .. autointerface:: ITemplateRenderer + .. autointerface:: IRendererFactory + :members: + + .. autointerface:: IRenderer :members: .. autointerface:: IViewMapperFactory diff --git a/docs/api/renderers.rst b/docs/api/renderers.rst index ea000ad02..0caca02b4 100644 --- a/docs/api/renderers.rst +++ b/docs/api/renderers.rst @@ -13,8 +13,12 @@ .. autoclass:: JSON + .. automethod:: add_adapter + .. autoclass:: JSONP + .. automethod:: add_adapter + .. attribute:: null_renderer An object that can be used in advanced integration cases as input to the diff --git a/docs/api/request.rst b/docs/api/request.rst index 72abddb68..b7604020e 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -11,7 +11,9 @@ :exclude-members: add_response_callback, add_finished_callback, route_url, route_path, current_route_url, current_route_path, static_url, static_path, - model_url, resource_url, set_property + model_url, resource_url, set_property, + effective_principals, authenticated_userid, + unauthenticated_userid, has_permission .. attribute:: context @@ -161,6 +163,42 @@ request, the value of this attribute will be ``None``. See :ref:`matched_route`. + .. attribute:: authenticated_userid + + .. versionadded:: 1.5 + + A property which returns the userid of the currently authenticated user + or ``None`` if there is no :term:`authentication policy` in effect or + there is no currently authenticated user. This differs from + :attr:`~pyramid.request.Request.unauthenticated_userid`, because the + effective authentication policy will have ensured that a record + associated with the userid exists in persistent storage; if it has + not, this value will be ``None``. + + .. attribute:: unauthenticated_userid + + .. versionadded:: 1.5 + + A property which returns a value which represents the *claimed* (not + verified) user id of the credentials present in the request. ``None`` if + there is no :term:`authentication policy` in effect or there is no user + data associated with the current request. This differs from + :attr:`~pyramid.request.Request.authenticated_userid`, because the + effective authentication policy will not ensure that a record associated + with the userid exists in persistent storage. Even if the userid + does not exist in persistent storage, this value will be the value + of the userid *claimed* by the request data. + + .. attribute:: effective_principals + + .. versionadded:: 1.5 + + A property which returns the list of 'effective' :term:`principal` + identifiers for this request. This will include the userid of the + currently authenticated user if a user is currently authenticated. If no + :term:`authentication policy` is in effect, this will return a sequence + containing only the :attr:`pyramid.security.Everyone` principal. + .. method:: invoke_subrequest(request, use_tweens=False) .. versionadded:: 1.4a1 @@ -215,6 +253,8 @@ request provided by e.g. the ``pshell`` environment. For more information, see :ref:`subrequest_chapter`. + .. automethod:: has_permission + .. automethod:: add_response_callback .. automethod:: add_finished_callback diff --git a/docs/api/session.rst b/docs/api/session.rst index 31bc196ad..dde9d20e9 100644 --- a/docs/api/session.rst +++ b/docs/api/session.rst @@ -5,12 +5,16 @@ .. automodule:: pyramid.session - .. autofunction:: UnencryptedCookieSessionFactoryConfig - .. autofunction:: signed_serialize .. autofunction:: signed_deserialize .. autofunction:: check_csrf_token + .. autofunction:: SignedCookieSessionFactory + + .. autofunction:: UnencryptedCookieSessionFactoryConfig + + .. autofunction:: BaseCookieSessionFactory + diff --git a/docs/conf.py b/docs/conf.py index a7a4a441a..a447c9968 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,9 +73,6 @@ intersphinx_mapping = { 'http://docs.pylonsproject.org/projects/deform/en/latest', None), 'sqla': ('http://docs.sqlalchemy.org/en/latest', None), - 'beaker': ( - 'http://docs.pylonsproject.org/projects/pyramid_beaker/en/latest', - None), 'who': ('http://docs.repoze.org/who/latest', None), 'python': ('http://docs.python.org', None), 'python3': ('http://docs.python.org/3', None), @@ -92,6 +89,7 @@ intersphinx_mapping = { None), } + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -121,39 +119,19 @@ release = version # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' -# List of documents that shouldn't be included in the build. -#unused_docs = [] - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_themes/README.rst', ] -# List of directories, relative to source directories, that shouldn't be searched -# for source files. -#exclude_dirs = [] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = False -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - # The name of the Pygments (syntax highlighting) style to use. #pygments_style = book and 'bw' or 'tango' if book: pygments_style = 'bw' -# The default language to highlight source code in. -#highlight_language = 'guess' - # Options for HTML output # ----------------------- @@ -172,49 +150,17 @@ if 'sphinx-build' in ' '.join(sys.argv): # protect against dumb importers sys.path.append(os.path.abspath('_themes')) - parent = os.path.dirname(os.path.dirname(__file__)) - sys.path.append(os.path.abspath(parent)) - wd = os.getcwd() - os.chdir(parent) - os.system('%s setup.py test -q' % sys.executable) - os.chdir(wd) - - for item in os.listdir(parent): - if item.endswith('.egg'): - sys.path.append(os.path.join(parent, item)) - html_theme_path = ['_themes'] html_theme = 'pyramid' html_theme_options = dict( github_url='https://github.com/Pylons/pyramid', in_progress='true', ) -# The style sheet to use for HTML and HTML Help pages. A file of that name -# must exist either in Sphinx' static/ path, or in one of the custom paths -# given in html_static_path. -#html_style = 'pyramid.css' # The name for this set of Sphinx documents. If None, it defaults to # "<project> v<release> documentation". html_title = 'The Pyramid Web Framework v%s' % release -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = 'Home' - -# The name of an image file (within the static path) to place at the top of -# the sidebar. -#html_logo = '_static/pyramid.png' - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = '_static/pyramid.ico' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] - # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' @@ -223,33 +169,6 @@ html_last_updated_fmt = '%b %d, %Y' # typographically correct entities. html_use_smartypants = False # people use cutnpaste in some places -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_use_modindex = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, the reST sources are included in the HTML build as _sources/<name>. -#html_copy_source = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a <link> tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' - # Output file base name for HTML help builder. htmlhelp_basename = 'pyramid' @@ -272,20 +191,10 @@ latex_documents = [ 'Chris McDonough', 'manual'), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = '_static/pylons_small.png' - # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. latex_use_parts = True -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - # If false, no module index is generated. latex_use_modindex = False @@ -528,13 +437,6 @@ epub_identifier = '0615445675' # A unique identification for the text. epub_uid = 'The Pyramid Web Framework, Version %s' \ % release -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['_static/opensearch.xml', '_static/doctools.js', @@ -544,3 +446,5 @@ epub_exclude_files = ['_static/opensearch.xml', '_static/doctools.js', # The depth of the table of contents in toc.ncx. epub_tocdepth = 3 + +# For a list of all settings, visit http://sphinx-doc.org/config.html diff --git a/docs/glossary.rst b/docs/glossary.rst index 7dc69c7c4..406b81778 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -505,15 +505,20 @@ Glossary available as its ``__parent__`` attribute. root factory - The "root factory" of a :app:`Pyramid` application is called - on every request sent to the application. The root factory - returns the traversal root of an application. It is - conventionally named ``get_root``. An application may supply a - root factory to :app:`Pyramid` during the construction of a - :term:`Configurator`. If a root factory is not supplied, the - application uses a default root object. Use of the default root - object is useful in application which use :term:`URL dispatch` for - all URL-to-view code mappings. + The "root factory" of a :app:`Pyramid` application is called on every + request sent to the application. The root factory returns the traversal + root of an application. It is conventionally named ``get_root``. An + application may supply a root factory to :app:`Pyramid` during the + construction of a :term:`Configurator`. If a root factory is not + supplied, the application creates a default root object using the + :term:`default root factory`. + + default root factory + If an application does not register a :term:`root factory` at Pyramid + configuration time, a *default* root factory is used to created the + default root object. Use of the default root object is useful in + application which use :term:`URL dispatch` for all URL-to-view code + mappings, and does not (knowingly) use traversal otherwise. SQLAlchemy `SQLAlchemy <http://www.sqlalchemy.org/>`_ is an object @@ -1009,7 +1014,8 @@ Glossary Green Unicorn Aka ``gunicorn``, a fast :term:`WSGI` server that runs on UNIX under - Python 2.6+ or Python 3.1+. See http://gunicorn.org/ for detailed information. + Python 2.6+ or Python 3.1+. See http://gunicorn.org/ for detailed + information. predicate factory A callable which is used by a third party during the registration of a @@ -1021,3 +1027,9 @@ Glossary A Python :term:`distribution` that uses Pyramid's extensibility to plug into a Pyramid application and provide extra, configurable services. + + pyramid_redis_sessions + A package by Eric Rasmussen which allows you to store Pyramid session + data in a Redis database. See + https://pypi.python.org/pypi/pyramid_redis_sessions for more information. + diff --git a/docs/index.rst b/docs/index.rst index 2efe90cf7..78a00966d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,10 +44,14 @@ up to speed right away. :hidden: quick_tour + quick_tutorial/index * :doc:`quick_tour` goes through the major features in Pyramid, covering a little about a lot. +* :doc:`quick_tutorial/index` does the same, but in a tutorial format: + deeper treatment of each topic and with working code. + * To see a minimal Pyramid web application, check out :ref:`firstapp_chapter`. diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst index a04b38ae3..3cabbd8f4 100644 --- a/docs/narr/commandline.rst +++ b/docs/narr/commandline.rst @@ -474,6 +474,17 @@ input of the ``prequest`` process is used as the ``POST`` body:: $ $VENV/bin/prequest -mPOST development.ini / < somefile +Using Custom Arguments to Python when Running ``p*`` Scripts +------------------------------------------------------------ + +.. versionadded:: 1.5 + +Each of Pyramid's console scripts (``pserve``, ``pviews``, etc) can be run +directly using ``python -m``, allowing custom arguments to be sent to the +python interpreter at runtime. For example:: + + python -3 -m pyramid.scripts.pserve development.ini + Showing All Installed Distributions and their Versions ------------------------------------------------------ diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 0c450fad7..84dd2143c 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -963,8 +963,8 @@ For full details, please read the `Venusian documentation .. _registering_tweens: -Registering "Tweens" --------------------- +Registering Tweens +------------------ .. versionadded:: 1.2 Tweens @@ -976,23 +976,77 @@ feature that may be used by Pyramid framework extensions, to provide, for example, Pyramid-specific view timing support bookkeeping code that examines exceptions before they are returned to the upstream WSGI application. Tweens behave a bit like :term:`WSGI` :term:`middleware` but they have the benefit of -running in a context in which they have access to the Pyramid -:term:`application registry` as well as the Pyramid rendering machinery. +running in a context in which they have access to the Pyramid :term:`request`, +:term:`response` and :term:`application registry` as well as the Pyramid +rendering machinery. -Creating a Tween Factory -~~~~~~~~~~~~~~~~~~~~~~~~ +Creating a Tween +~~~~~~~~~~~~~~~~ -To make use of tweens, you must construct a "tween factory". A tween factory +To create a tween, you must write a "tween factory". A tween factory must be a globally importable callable which accepts two arguments: ``handler`` and ``registry``. ``handler`` will be the either the main Pyramid request handling function or another tween. ``registry`` will be the Pyramid :term:`application registry` represented by this Configurator. A -tween factory must return a tween when it is called. +tween factory must return the tween (a callable object) when it is called. -A tween is a callable which accepts a :term:`request` object and returns -a :term:`response` object. +A tween is called with a single argument, ``request``, which is the +:term:`request` created by Pyramid's router when it receives a WSGI request. +A tween should return a :term:`response`, usually the one generated by the +downstream Pyramid application. -Here's an example of a tween factory: +You can write the tween factory as a simple closure-returning function: + +.. code-block:: python + :linenos: + + def simple_tween_factory(handler, registry): + # one-time configuration code goes here + + def simple_tween(request): + # code to be executed for each request before + # the actual application code goes here + + response = handler(request) + + # code to be executed for each request after + # the actual application code goes here + + return response + + return handler + +Alternatively, the tween factory can be a class with the ``__call__`` magic +method: + +.. code-block:: python + :linenos: + + class simple_tween_factory(object): + def __init__(handler, registry): + self.handler = handler + self.registry = registry + + # one-time configuration code goes here + + def __call__(self, request): + # code to be executed for each request before + # the actual application code goes here + + response = self.handler(request) + + # code to be executed for each request after + # the actual application code goes here + + return response + +The closure style performs slightly better and enables you to conditionally +omit the tween from the request processing pipeline (see the following timing +tween example), whereas the class style makes it easier to have shared mutable +state, and it allows subclassing. + +Here's a complete example of a tween that logs the time spent processing each +request: .. code-block:: python :linenos: @@ -1022,12 +1076,6 @@ Here's an example of a tween factory: # handler return handler -If you remember, a tween is an object which accepts a :term:`request` object -and which returns a :term:`response` argument. The ``request`` argument to a -tween will be the request created by Pyramid's router when it receives a WSGI -request. The response object will be generated by the downstream Pyramid -application and it should be returned by the tween. - In the above example, the tween factory defines a ``timing_tween`` tween and returns it if ``asbool(registry.settings.get('do_timing'))`` is true. It otherwise simply returns the handler it was given. The ``registry.settings`` @@ -1132,8 +1180,10 @@ Allowable values for ``under`` or ``over`` (or both) are: fallbacks if the desired tween is not included, as well as compatibility with multiple other tweens. -Effectively, ``under`` means "closer to the main Pyramid application than", -``over`` means "closer to the request ingress than". +Effectively, ``over`` means "closer to the request ingress than" and +``under`` means "closer to the main Pyramid application than". +You can think of an onion with outer layers over the inner layers, +the application being under all the layers at the center. For example, the following call to :meth:`~pyramid.config.Configurator.add_tween` will attempt to place the diff --git a/docs/narr/install.rst b/docs/narr/install.rst index fb67b899b..e419a8b20 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -25,6 +25,10 @@ on :term:`PyPy` (1.9+). :app:`Pyramid` installation does not require the compilation of any C code, so you need only a Python interpreter that meets the requirements mentioned. +Some :app:`Pyramid` dependencies may attempt to build C extensions for +performance speedups. If a compiler or Python headers are unavailable the +dependency will fall back to using pure Python instead. + For Mac OS X Users ~~~~~~~~~~~~~~~~~~ @@ -285,13 +289,20 @@ Installing :app:`Pyramid` Into the Virtual Python Environment After you've got your virtualenv installed, you may install :app:`Pyramid` itself using the following commands: -.. code-block:: text - - $ $VENV/bin/easy_install pyramid +.. parsed-literal:: + + $ $VENV/bin/easy_install "pyramid==\ |release|\ " The ``easy_install`` command will take longer than the previous ones to complete, as it downloads and installs a number of dependencies. +.. note:: + + If you see any warnings and/or errors related to failing to compile the C + extensions, in most cases you may safely ignore those errors. If you wish + to use the C extensions, please verify that you have a functioning compiler + and the Python header files installed. + .. index:: single: installing on Windows @@ -357,9 +368,9 @@ You can use Pyramid on Windows under Python 2 or 3. #. Use ``easy_install`` to get :app:`Pyramid` and its direct dependencies installed: - .. code-block:: text - - c:\env> %VENV%\Scripts\easy_install pyramid + .. parsed-literal:: + + c:\\env> %VENV%\\Scripts\\easy_install "pyramid==\ |release|\ " What Gets Installed ------------------- diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 032f4be6b..a9c5fdfbd 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -176,8 +176,13 @@ static file server in production without changing any code. Example: :ref:`static_assets_section`. -Debug Toolbar -~~~~~~~~~~~~~ +Fully Interactive Development +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When developing a Pyramid application, several interactive features are +available. Pyramid can automatically utilize changed templates when rendering +pages and automatically restart the application to incorporate changed python +code. Plain old ``print()`` calls used for debugging can display to a console. Pyramid's debug toolbar comes activated when you use a Pyramid scaffold to render a project. This toolbar overlays your application in the browser, and @@ -321,7 +326,14 @@ assertion instead that the view returns "the right stuff" in the dictionary it returns. You can write "real" unit tests instead of functionally testing all of your views. -For example, instead of: +.. index:: + pair: renderer; explicitly calling + pair: view renderer; explictly calling + +.. _example_render_to_response_call: + +For example, instead of returning a ``Response`` object from a +``render_to_response`` call: .. code-block:: python :linenos: @@ -332,7 +344,7 @@ For example, instead of: return render_to_response('myapp:templates/mytemplate.pt', {'a':1}, request=request) -You can do this: +You can return a Python dictionary: .. code-block:: python :linenos: @@ -405,12 +417,12 @@ Sessions Pyramid has built-in HTTP sessioning. This allows you to associate data with otherwise anonymous users between requests. Lots of systems do this. But -Pyramid also allows you to plug in your own sessioning system by creating -some code that adheres to a documented interface. Currently there is a -binding package for the third-party Beaker sessioning system that does exactly -this. But if you have a specialized need (perhaps you want to store your -session data in MongoDB), you can. You can even switch between -implementations without changing your application code. +Pyramid also allows you to plug in your own sessioning system by creating some +code that adheres to a documented interface. Currently there is a binding +package for the third-party Redis sessioning system that does exactly this. +But if you have a specialized need (perhaps you want to store your session data +in MongoDB), you can. You can even switch between implementations without +changing your application code. Example: :ref:`sessions_chapter`. @@ -777,7 +789,7 @@ automate some of the tedium away: for method in ('GET', 'POST', 'HEAD'): view = getattr(module, 'xhr_%s_view' % method, None) if view is not None: - config.add_view(view, route_name='xhr_route', xhr=True, + config.add_view(view, route_name='xhr_route', xhr=True, permission='view', request_method=method) config = Configurator() diff --git a/docs/narr/paste.rst b/docs/narr/paste.rst index 3427b6d53..f1fb70869 100644 --- a/docs/narr/paste.rst +++ b/docs/narr/paste.rst @@ -87,7 +87,7 @@ configuration object and *returns* an instance of our application. .. _defaults_section_of_pastedeploy_file: -``[DEFAULTS]`` Section of a PasteDeploy ``.ini`` File +``[DEFAULT]`` Section of a PasteDeploy ``.ini`` File ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can add a ``[DEFAULT]`` section to your PasteDeploy ``.ini`` file. Such diff --git a/docs/narr/project.rst b/docs/narr/project.rst index f3050f805..d7292d187 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -144,9 +144,9 @@ contains no space characters, so it's wise to *avoid* a path that contains i.e. ``My Documents``. As a result, the author, when he uses Windows, just puts his projects in ``C:\projects``. -.. warning:: +.. warning:: - You’ll need to avoid using ``pcreate`` to create a project with the same + You'll need to avoid using ``pcreate`` to create a project with the same name as a Python standard library component. In particular, this means you should avoid using the names ``site`` or ``test``, both of which conflict with Python standard library packages. You should also avoid @@ -193,10 +193,10 @@ Elided output from a run of this command on UNIX is shown below: ... Finished processing dependencies for MyProject==0.0 -This will install a :term:`distribution` representing your project into the -interpreter's library set so it can be found by ``import`` statements and by -other console scripts such as ``pserve``, ``pshell``, ``proutes`` and -``pviews``. +This will install a :term:`distribution` representing your project +into the virtual environment interpreter's library set so it can be +found by ``import`` statements and by other console scripts such as +``pserve``, ``pshell``, ``proutes`` and ``pviews``. .. index:: single: running tests @@ -243,22 +243,24 @@ Here's sample output from a test run on UNIX: OK -.. note:: - - The ``-q`` option is passed to the ``setup.py test`` command to limit the - output to a stream of dots. If you don't pass ``-q``, you'll see more - verbose test result output (which normally isn't very useful). - The tests themselves are found in the ``tests.py`` module in your ``pcreate`` generated project. Within a project generated by the ``starter`` scaffold, a single sample test exists. +.. note:: + + The ``-q`` option is passed to the ``setup.py test`` command to limit the + output to a stream of dots. If you don't pass ``-q``, you'll see more + verbose test result output (which normally isn't very useful). + .. index:: single: running an application single: pserve single: reload single: startup +.. _running_the_project_application: + Running The Project Application ------------------------------- @@ -600,6 +602,8 @@ server which listens on TCP port 6543. It is configured to listen on all interfaces (``0.0.0.0``). This means that any remote system which has TCP access to your system can see your Pyramid application. +.. _MyProject_ini_logging: + The sections that live between the markers ``# Begin logging configuration`` and ``# End logging configuration`` represent Python's standard library :mod:`logging` module configuration for your application. The sections @@ -696,11 +700,11 @@ testing, packaging, and distributing your application. .. note:: - ``setup.py`` is the de facto standard which Python developers use to - distribute their reusable code. You can read more about ``setup.py`` files - and their usage in the `Setuptools documentation - <http://peak.telecommunity.com/DevCenter/setuptools>`_ and `The - Hitchhiker's Guide to Packaging <http://guide.python-distribute.org/>`_. + ``setup.py`` is the de facto standard which Python developers use to + distribute their reusable code. You can read more about ``setup.py`` files + and their usage in the `Setuptools documentation + <http://peak.telecommunity.com/DevCenter/setuptools>`_ and `The + Hitchhiker's Guide to Packaging <http://guide.python-distribute.org/>`_. Our generated ``setup.py`` looks like this: @@ -871,31 +875,48 @@ specification` that specifies the ``mytemplate.pt`` file within the ``templates`` directory of the ``myproject`` package. The asset specification could have also been specified as ``myproject:templates/mytemplate.pt``; the leading package name and colon is -optional. The template file it actually points to is a :term:`Chameleon` ZPT -template file. +optional. The template file pointed to is a :term:`Chameleon` ZPT +template file (``templates/my_template.pt``). This view callable function is handed a single piece of information: the :term:`request`. The *request* is an instance of the :term:`WebOb` ``Request`` class representing the browser's request to our server. -This view returns a dictionary. When this view is invoked, a -:term:`renderer` converts the dictionary returned by the view into HTML, and -returns the result as the :term:`response`. This view is configured to -invoke a renderer which uses a :term:`Chameleon` ZPT template -(``templates/my_template.pt``). - -See :ref:`views_which_use_a_renderer` for more information about how views, -renderers, and templates relate and cooperate. - -.. note:: Because our ``development.ini`` has a ``pyramid.reload_templates = - true`` directive indicating that templates should be reloaded when - they change, you won't need to restart the application server to - see changes you make to templates. During development, this is - handy. If this directive had been ``false`` (or if the directive - did not exist), you would need to restart the application server - for each template change. For production applications, you should - set your project's ``pyramid.reload_templates`` to ``false`` to increase - the speed at which templates may be rendered. +This view is configured to invoke a :term:`renderer` on a template. The +dictionary the view returns (on line 6) provides the value the renderer +substitutes into the template when generating HTML. The renderer then +returns the HTML in a :term:`response`. + +.. note:: Dictionaries provide values to :term:`template`\s. + +.. note:: When the application is run with the scaffold's :ref:`default + development.ini <MyProject_ini>` configuration :ref:`logging is set up + <MyProject_ini_logging>` to aid debugging. If an exception is raised, + uncaught tracebacks are displayed after the startup messages on :ref:`the + console running the server <running_the_project_application>`. Also + ``print()`` statements may be inserted into the application for debugging + to send output to this console. + +.. note:: ``development.ini`` has a setting that controls how templates are + reloaded, ``pyramid.reload_templates``. + + - When set to ``True`` (as in the scaffold ``development.ini``) changed + templates automatically reload without a server restart. This is + convenient while developing, but slows template rendering speed. + + - When set to ``False`` (the default value), changing templates requires + a server restart to reload them. Production applications should use + ``pyramid.reload_templates = False``. + +.. seealso:: See also :ref:`views_which_use_a_renderer` for more information + about how views, renderers, and templates relate and cooperate. + +.. seealso:: Pyramid can also dynamically reload changed Python files. For + more on this see :ref:`reloading_code`. + +.. seealso:: The :ref:`debug_toolbar` provides interactive access to your + application's internals and, should an exception occur, allows interactive + access to traceback execution stack frames from the Python interpreter. .. index:: single: static directory @@ -971,12 +992,15 @@ named ``views`` instead of within a single ``views.py`` file, you might: - Create a ``views`` directory inside your ``myproject`` package directory (the same directory which holds ``views.py``). -- *Move* the existing ``views.py`` file to a file inside the new ``views`` - directory named, say, ``blog.py``. +- Create a file within the new ``views`` directory named ``__init__.py``. (It + can be empty. This just tells Python that the ``views`` directory is a + *package*.) -- Create a file within the new ``views`` directory named ``__init__.py`` (it - can be empty, this just tells Python that the ``views`` directory is a - *package*. +- *Move* the content from the existing ``views.py`` file to a file inside the + new ``views`` directory named, say, ``blog.py``. Because the ``templates`` + directory remains in the ``myproject`` package, the template :term:`asset + specification` values in ``blog.py`` must now be fully qualified with the + project's package name (``myproject:templates/blog.pt``). You can then continue to add view callable functions to the ``blog.py`` module, but you can also add other ``.py`` files which contain view callable @@ -1025,7 +1049,7 @@ server. Waitress is a server that is suited for development and light production usage. It's not the fastest nor the most featureful WSGI server. Instead, its main feature is that it works on all platforms that Pyramid needs to run on, making it a good choice as a default server from the -perspective of Pyramid's developers. +perspective of Pyramid's developers. Any WSGI server is capable of running a :app:`Pyramid` application. But we suggest you stick with the default server for development, and that you wait diff --git a/docs/narr/renderers.rst b/docs/narr/renderers.rst index 3059aef35..4f8c4bf77 100644 --- a/docs/narr/renderers.rst +++ b/docs/narr/renderers.rst @@ -49,15 +49,19 @@ Writing View Callables Which Use a Renderer ------------------------------------------- As we've seen, a view callable needn't always return a Response object. -Instead, it may return an arbitrary Python object, with the expectation -that a :term:`renderer` will convert that object into a response instance on -your behalf. Some renderers use a templating system; other renderers use -object serialization techniques. - -View configuration can vary the renderer associated with a view callable via -the ``renderer`` attribute. For example, this call to -:meth:`~pyramid.config.Configurator.add_view` associates the ``json`` renderer -with a view callable: +Instead, it may return an arbitrary Python object, with the expectation that +a :term:`renderer` will convert that object into a response instance on your +behalf. Some renderers use a templating system; other renderers use object +serialization techniques. In practice, renderers obtain application data +values from Python dictionaries so, in practice, view callables which use +renderers return Python dictionaries. + +View callables can :ref:`explicitly call <example_render_to_response_call>` +renderers, but typically don't. Instead view configuration declares the +renderer used to render a view callable's results. This is done with the +``renderer`` attribute. For example, this call to +:meth:`~pyramid.config.Configurator.add_view` associates the ``json`` +renderer with a view callable: .. code-block:: python @@ -476,8 +480,11 @@ Adding a New Renderer You may add a new renderer by creating and registering a :term:`renderer factory`. -A renderer factory implementation is typically a class with the -following interface: +A renderer factory implementation should conform to the +:class:`pyramid.interfaces.IRendererFactory` interface. It should be capable +of creating an object that conforms to the +:class:`pyramid.interfaces.IRenderer` interface. A typical class that follows +this setup is as follows: .. code-block:: python :linenos: diff --git a/docs/narr/resources.rst b/docs/narr/resources.rst index b1bb611e5..34d75f2cc 100644 --- a/docs/narr/resources.rst +++ b/docs/narr/resources.rst @@ -201,7 +201,7 @@ location-aware resources. These APIs include (but are not limited to) :func:`~pyramid.traversal.resource_path`, :func:`~pyramid.traversal.resource_path_tuple`, or :func:`~pyramid.traversal.traverse`, :func:`~pyramid.traversal.virtual_root`, -and (usually) :func:`~pyramid.security.has_permission` and +and (usually) :meth:`~pyramid.request.Request.has_permission` and :func:`~pyramid.security.principals_allowed_by_permission`. In general, since so much :app:`Pyramid` infrastructure depends on @@ -695,10 +695,10 @@ The APIs provided by :ref:`location_module` are used against resources. These can be used to walk down a resource tree, or conveniently locate one resource "inside" another. -Some APIs in :ref:`security_module` accept a resource object as a parameter. -For example, the :func:`~pyramid.security.has_permission` API accepts a +Some APIs on the :class:`pyramid.request.Request` accept a resource object as a parameter. +For example, the :meth:`~pyramid.request.Request.has_permission` API accepts a resource object as one of its arguments; the ACL is obtained from this -resource or one of its ancestors. Other APIs in the :mod:`pyramid.security` -module also accept :term:`context` as an argument, and a context is always a -resource. +resource or one of its ancestors. Other security related APIs on the +:class:`pyramid.request.Request` class also accept :term:`context` as an argument, +and a context is always a resource. diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 6517fedf8..9e6fb6c82 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -550,7 +550,7 @@ also contain security debugging information in its body. Debugging Imperative Authorization Failures ------------------------------------------- -The :func:`pyramid.security.has_permission` API is used to check +The :meth:`pyramid.request.Request.has_permission` API is used to check security within view functions imperatively. It returns instances of objects that are effectively booleans. But these objects are not raw ``True`` or ``False`` objects, and have information attached to them @@ -563,7 +563,7 @@ one of :data:`pyramid.security.ACLAllowed`, ``msg`` attribute, which is a string indicating why the permission was denied or allowed. Introspecting this information in the debugger or via print statements when a call to -:func:`~pyramid.security.has_permission` fails is often useful. +:meth:`~pyramid.request.Request.has_permission` fails is often useful. .. index:: single: authentication policy (creating) @@ -669,3 +669,31 @@ following interface: After you do so, you can pass an instance of such a class into the :class:`~pyramid.config.Configurator.set_authorization_policy` method at configuration time to use it. + +.. _admonishment_against_secret_sharing: + +Admonishment Against Secret-Sharing +----------------------------------- + +A "secret" is required by various components of Pyramid. For example, the +:term:`authentication policy` below uses a secret value ``seekrit``:: + + authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512') + +A :term:`session factory` also requires a secret:: + + my_session_factory = SignedCookieSessionFactory('itsaseekreet') + +It is tempting to use the same secret for multiple Pyramid subsystems. For +example, you might be tempted to use the value ``seekrit`` as the secret for +both the authentication policy and the session factory defined above. This is +a bad idea, because in both cases, these secrets are used to sign the payload +of the data. + +If you use the same secret for two different parts of your application for +signing purposes, it may allow an attacker to get his chosen plaintext signed, +which would allow the attacker to control the content of the payload. Re-using +a secret across two different subsystems might drop the security of signing to +zero. Keys should not be re-used across different contexts where an attacker +has the possibility of providing a chosen plaintext. + diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index 358977089..fb5035373 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -43,24 +43,23 @@ limitations: It is digitally signed, however, and thus its data cannot easily be tampered with. -You can configure this session factory in your :app:`Pyramid` -application by using the ``session_factory`` argument to the -:class:`~pyramid.config.Configurator` class: +You can configure this session factory in your :app:`Pyramid` application +by using the :meth:`pyramid.config.Configurator.set_session_factory`` method. .. code-block:: python :linenos: - from pyramid.session import UnencryptedCookieSessionFactoryConfig - my_session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet') - + from pyramid.session import SignedCookieSessionFactory + my_session_factory = SignedCookieSessionFactory('itsaseekreet') + from pyramid.config import Configurator - config = Configurator(session_factory = my_session_factory) + config = Configurator() + config.set_session_factory(my_session_factory) .. warning:: - Note the very long, very explicit name for - ``UnencryptedCookieSessionFactoryConfig``. It's trying to tell you that - this implementation is, by default, *unencrypted*. You should not use it + By default the :func:`~pyramid.session.SignedCookieSessionFactory` + implementation is *unencrypted*. You should not use it when you keep sensitive information in the session object, as the information can be easily read by both users of your application and third parties who have access to your users' network traffic. And if you use this @@ -99,6 +98,11 @@ example: else: return Response('Fred was not in the session') +The first time this view is invoked produces ``Fred was not in the +session``. Subsequent invocations produce ``Fred was in the +session``, assuming of course that the client side maintains the +session's identity across multiple requests. + You can use a session much like a Python dictionary. It supports all dictionary methods, along with some extra attributes, and methods. @@ -146,8 +150,6 @@ Some gotchas: you've changed sessioning data. .. index:: - single: pyramid_beaker - single: Beaker single: pyramid_redis_sessions single: session factory (alternates) @@ -156,19 +158,10 @@ Some gotchas: Using Alternate Session Factories --------------------------------- -At the time of this writing, exactly two alternate session factories -exist. - -The first is named ``pyramid_redis_sessions``. It can be downloaded from PyPI. -It uses Redis as a backend. It is the recommended persistent session solution -at the time of this writing. - -The second is named ``pyramid_beaker``. This is a session factory that uses the -`Beaker <http://beaker.groovie.org/>`_ library as a backend. Beaker has -support for file-based sessions, database based sessions, and encrypted -cookie-based sessions. See `the pyramid_beaker documentation -<http://docs.pylonsproject.org/projects/pyramid_beaker/en/latest/>`_ for more -information about ``pyramid_beaker``. +At the time of this writing, exactly one project-endorsed alternate session +factory exists named :term:`pyramid_redis_sessions`. It can be downloaded from +PyPI. It uses the Redis database as a backend. It is the recommended +persistent session solution at the time of this writing. .. index:: single: session factory (custom) @@ -369,25 +362,27 @@ Or, include it as a header in a jQuery AJAX request: The handler for the URL that receives the request should then require that the correct CSRF token is supplied. -Using the ``session.check_csrf_token`` Method -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Checking CSRF Tokens Manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In request handling code, you can check the presence and validity of a CSRF -token with ``session.check_csrf_token(request)``. If the token is valid, -it will return True, otherwise it will raise ``HTTPBadRequest``. +token with :func:`pyramid.session.check_csrf_token(request)``. If the token is +valid, it will return ``True``, otherwise it will raise ``HTTPBadRequest``. +Optionally, you can specify ``raises=False`` to have the check return ``False`` +instead of raising an exception. By default, it checks for a GET or POST parameter named ``csrf_token`` or a header named ``X-CSRF-Token``. .. code-block:: python - def myview(request): - session = request.session + from pyramid.session import check_csrf_token + def myview(request): # Require CSRF Token - session.check_csrf_token(request): + check_csrf_token(request) - ... + # ... .. index:: single: session.new_csrf_token diff --git a/docs/narr/testing.rst b/docs/narr/testing.rst index 88d6904c7..5a5bf8fad 100644 --- a/docs/narr/testing.rst +++ b/docs/narr/testing.rst @@ -214,11 +214,10 @@ function. .. code-block:: python :linenos: - from pyramid.security import has_permission from pyramid.httpexceptions import HTTPForbidden def view_fn(request): - if not has_permission('edit', request.context, request): + if request.has_permission('edit'): raise HTTPForbidden return {'greeting':'hello'} @@ -229,15 +228,16 @@ function. otherwise it would fail when run normally. Without doing anything special during a unit test, the call to -:func:`~pyramid.security.has_permission` in this view function will always -return a ``True`` value. When a :app:`Pyramid` application starts normally, -it will populate a :term:`application registry` using :term:`configuration -declaration` calls made against a :term:`Configurator`. But if this -application registry is not created and populated (e.g. by initializing the -configurator with an authorization policy), like when you invoke application -code via a unit test, :app:`Pyramid` API functions will tend to either fail -or return default results. So how do you test the branch of the code in this -view function that raises :exc:`~pyramid.httpexceptions.HTTPForbidden`? +:meth:`~pyramid.request.Request.has_permission` in this view function will +always return a ``True`` value. When a :app:`Pyramid` application starts +normally, it will populate a :term:`application registry` using +:term:`configuration declaration` calls made against a :term:`Configurator`. +But if this application registry is not created and populated (e.g. by +initializing the configurator with an authorization policy), like when you +invoke application code via a unit test, :app:`Pyramid` API functions will tend +to either fail or return default results. So how do you test the branch of the +code in this view function that raises +:exc:`~pyramid.httpexceptions.HTTPForbidden`? The testing API provided by :app:`Pyramid` allows you to simulate various application registry registrations for use under a unit testing framework @@ -287,12 +287,12 @@ Its third line registers a "dummy" "non-permissive" authorization policy using the :meth:`~pyramid.config.Configurator.testing_securitypolicy` method, which is a special helper method for unit testing. -We then create a :class:`pyramid.testing.DummyRequest` object which simulates -a WebOb request object API. A :class:`pyramid.testing.DummyRequest` is a -request object that requires less setup than a "real" :app:`Pyramid` request. -We call the function being tested with the manufactured request. When the -function is called, :func:`pyramid.security.has_permission` will call the -"dummy" authentication policy we've registered through +We then create a :class:`pyramid.testing.DummyRequest` object which simulates a +WebOb request object API. A :class:`pyramid.testing.DummyRequest` is a request +object that requires less setup than a "real" :app:`Pyramid` request. We call +the function being tested with the manufactured request. When the function is +called, :meth:`pyramid.request.Request.has_permission` will call the "dummy" +authentication policy we've registered through :meth:`~pyramid.config.Configurator.testing_securitypolicy`, which denies access. We check that the view function raises a :exc:`~pyramid.httpexceptions.HTTPForbidden` error. diff --git a/docs/narr/threadlocals.rst b/docs/narr/threadlocals.rst index a90ee4905..afe56de3e 100644 --- a/docs/narr/threadlocals.rst +++ b/docs/narr/threadlocals.rst @@ -29,17 +29,16 @@ of a thread local or a global is usually just a way to avoid passing some value around between functions, which is itself usually a very bad idea, at least if code readability counts as an important concern. -For historical reasons, however, thread local variables are indeed -consulted by various :app:`Pyramid` API functions. For example, -the implementation of the :mod:`pyramid.security` function named -:func:`~pyramid.security.authenticated_userid` retrieves the thread -local :term:`application registry` as a matter of course to find an +For historical reasons, however, thread local variables are indeed consulted by +various :app:`Pyramid` API functions. For example, the implementation of the +:mod:`pyramid.security` function named +:func:`~pyramid.security.authenticated_userid` (deprecated as of 1.5) retrieves +the thread local :term:`application registry` as a matter of course to find an :term:`authentication policy`. It uses the -:func:`pyramid.threadlocal.get_current_registry` function to -retrieve the application registry, from which it looks up the -authentication policy; it then uses the authentication policy to -retrieve the authenticated user id. This is how :app:`Pyramid` -allows arbitrary authentication policies to be "plugged in". +:func:`pyramid.threadlocal.get_current_registry` function to retrieve the +application registry, from which it looks up the authentication policy; it then +uses the authentication policy to retrieve the authenticated user id. This is +how :app:`Pyramid` allows arbitrary authentication policies to be "plugged in". When they need to do so, :app:`Pyramid` internals use two API functions to retrieve the :term:`request` and :term:`application diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst index a60c5ba56..454bb5620 100644 --- a/docs/narr/traversal.rst +++ b/docs/narr/traversal.rst @@ -146,35 +146,15 @@ refer to a root factory defined in a different module. If no :term:`root factory` is passed to the :app:`Pyramid` :term:`Configurator` constructor, or if the ``root_factory`` value -specified is ``None``, a *default* root factory is used. The default +specified is ``None``, a :term:`default root factory` is used. The default root factory always returns a resource that has no child resources; it is effectively empty. Usually a root factory for a traversal-based application will be more -complicated than the above ``Root`` class; in particular it may be -associated with a database connection or another persistence mechanism. - -.. sidebar:: Emulating the Default Root Factory - - For purposes of understanding the default root factory better, we'll note - that you can emulate the default root factory by using this code as an - explicit root factory in your application setup: - - .. code-block:: python - :linenos: - - class Root(object): - def __init__(self, request): - pass - - config = Configurator(root_factory=Root) - - The default root factory is just a really stupid object that has no - behavior or state. Using :term:`traversal` against an application that - uses the resource tree supplied by the default root resource is not very - interesting, because the default root resource has no children. Its - availability is more useful when you're developing an application using - :term:`URL dispatch`. +complicated than the above ``Root`` class; in particular it may be associated +with a database connection or another persistence mechanism. The above +``Root`` class is analogous to the default root factory present in Pyramid. The +default root factory is very simple and not very useful. .. note:: diff --git a/docs/narr/upgrading.rst b/docs/narr/upgrading.rst index ca6dc565b..64343ca3e 100644 --- a/docs/narr/upgrading.rst +++ b/docs/narr/upgrading.rst @@ -150,7 +150,7 @@ do things the newer way: .. code-block:: python :linenos: - from pyramid.view. import view_config + from pyramid.view import view_config from pyramid.static import static_view myview = static_view('static', 'static', use_subpath=True) diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 7c76116f7..e5a2c1ade 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -435,7 +435,7 @@ configured view. If specified, this value should be a :term:`principal` identifier or a sequence of principal identifiers. If the - :func:`pyramid.security.effective_principals` method indicates that every + :meth:`pyramid.request.Request.effective_principals` method indicates that every principal named in the argument list is present in the current request, this predicate will return True; otherwise it will return False. For example: ``effective_principals=pyramid.security.Authenticated`` or diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 4b23f7858..2db18c8a7 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -4,73 +4,56 @@ Quick Tour of Pyramid ===================== -Pyramid lets you start small and finish big. This *Quick Tour* guide -walks you through many of Pyramid's key features. Let's put the -emphasis on *start* by doing a quick tour through Pyramid, with -snippets of code to illustrate major concepts. +Pyramid lets you start small and finish big. This *Quick Tour* of Pyramid is +for those who want to evaluate Pyramid, whether you are new to Python +web frameworks, or a pro in a hurry. For more detailed treatment of +each topic, give the :ref:`quick_tutorial` a try. -.. note:: - - We use Python 3 in our samples. Pyramid was one of the first - (October 2011) web frameworks to fully support Python 3. You can - use Python 3 as well for this guide, but you can also use Python 2.7. - -Python Setup +Installation ============ -First thing's first: we need our Python environment in ship-shape. -Pyramid encourages standard Python development practices (virtual -environments, packaging tools, logging, etc.) so let's get our working -area in place. For Python 3.3: - -.. code-block:: bash - - $ pyvenv-3.3 env33 - $ source env33/bin/activate - $ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | python - -If ``wget`` complains with a certificate error, run it with: - -.. code-block:: bash - - $ wget --no-check-certificate +Once you have a standard Python environment setup, getting started with +Pyramid is a breeze. Unfortunately "standard" is not so simple in Python. +For this Quick Tour, it means: +`Python <http://www.python.org/download/releases/>`_, a +`virtual environment <http://docs.python.org/dev/library/venv.html>`_ +(or `virtualenv for Python 2.7 <https://pypi.python.org/pypi/virtualenv>`_), +and `setuptools <https://pypi.python.org/pypi/setuptools/>`_. -In these steps above we first made a :term:`virtualenv` and then -"activated" it, which adjusted our path to look first in -``env33/bin`` for commands (such as ``python``). We next downloaded -Python's packaging support and installed it, giving us the -``easy_install`` command-line script for adding new packages. Python -2.7 users will need to use ``virtualenv`` instead of ``pyvenv`` to make -their virtual environment. +As an example, for Python 3.3+ on Linux: -.. note:: +.. parsed-literal:: - Why ``easy_install`` and not ``pip``? Pyramid encourages use of - namespace packages which, until recently, ``pip`` didn't permit. - Also, Pyramid has some optional C extensions for performance. With - ``easy_install``, Windows users can get these extensions without - needing a C compiler. + $ pyvenv env33 + $ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | env33/bin/python + $ env33/bin/easy_install "pyramid==\ |release|\ " -.. seealso:: See Also: Python 3's :mod:`venv module <python3:venv>`, - the ``setuptools`` `installation - instructions <https://pypi.python.org/pypi/setuptools/0.9.8#installation-instructions>`_, - `easy_install help <https://pypi.python.org/pypi/setuptools/0.9.8#using-setuptools-and-easyinstall>`_, - and Pyramid's :ref:`Before You Install <installing_chapter>`. +For Windows: -Pyramid Installation -==================== +.. parsed-literal:: -We now have a standard starting point for Python. Getting Pyramid -installed is easy: + # Use your browser to download: + # https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py + c:\\> c:\\Python33\\python -m venv env33 + c:\\> env33\\Scripts\\python ez_setup.py + c:\\> env33\\Scripts\\easy_install "pyramid==\ |release|\ " -.. code-block:: bash +Of course Pyramid runs fine on Python 2.6+, as do the examples in this +*Quick Tour*. We're just showing Python 3 a little love (Pyramid had +production support in October 2011.) - $ easy_install pyramid +.. note:: -Our virtual environment now has the Pyramid software available to its -Python. + Why ``easy_install`` and not ``pip``? Pyramid encourages use of namespace + packages which, until recently, ``pip`` didn't permit. Also, Pyramid has + some optional C extensions for performance. With ``easy_install``, Windows + users can get these extensions without needing a C compiler. -.. seealso:: See Also: :ref:`installing_unix` +.. seealso:: See Also: + :ref:`Quick Tutorial section on Requirements <qtut_requirements>`, + :ref:`installing_unix`, + :ref:`Before You Install <installing_chapter>`, and + :ref:`Installing Pyramid on a Windows System <installing_windows>` Hello World =========== @@ -109,7 +92,9 @@ in Pyramid development. Building an application from loosely-coupled parts via :doc:`../narr/configuration` is a central idea in Pyramid, one that we will revisit regurlarly in this *Quick Tour*. -.. seealso:: See Also: :ref:`firstapp_chapter` and +.. seealso:: See Also: + :ref:`Quick Tutorial Hello World <qtut_hello_world>`, + :ref:`firstapp_chapter`, and :ref:`Single File Tasks tutorial <tutorials:single-file-tutorial>` Handling Web Requests and Responses @@ -140,7 +125,10 @@ the name is included in the body of the response:: Finally, we set the response's content type and return the Response. -.. seealso:: See Also: :ref:`webob_chapter` +.. seealso:: See Also: + :ref:`Quick Tutorial Request and Response <qtut_request_response>` + and + :ref:`webob_chapter` Views ===== @@ -190,7 +178,9 @@ configuration`, in which a Python :term:`decorator` is placed on the line above the view. Both approaches result in the same final configuration, thus usually, it is simply a matter of taste. -.. seealso:: See Also: :doc:`../narr/views`, +.. seealso:: See Also: + :ref:`Quick Tutorial Views <qtut_views>`, + :doc:`../narr/views`, :doc:`../narr/viewconfig`, and :ref:`debugging_view_configuration` @@ -236,7 +226,9 @@ view: "replacement patterns" (the curly braces) in the route declaration. This information can then be used in your view. -.. seealso:: See Also: :doc:`../narr/urldispatch`, +.. seealso:: See Also: + :ref:`Quick Tutorial Routing <qtut_routing>`, + :doc:`../narr/urldispatch`, :ref:`debug_routematch_section`, and :doc:`../narr/router` @@ -270,9 +262,11 @@ Since our view returned ``dict(name=request.matchdict['name'])``, we can use ``name`` as a variable in our template via ``${name}``. -.. seealso:: See Also: :doc:`../narr/templates`, - :ref:`debugging_templates`, and - :ref:`available_template_system_bindings` +.. seealso:: See Also: + :ref:`Quick Tutorial Templating <qtut_templating>`, + :doc:`../narr/templates`, + :ref:`debugging_templates`, and + :ref:`available_template_system_bindings` Templating With ``jinja2`` ========================== @@ -303,7 +297,7 @@ The only change in our view...point the renderer at the ``.jinja2`` file: Our Jinja2 template is very similar to our previous template: .. literalinclude:: quick_tour/jinja2/hello_world.jinja2 - :language: jinja + :language: html Pyramid's templating add-ons register a new kind of renderer into your application. The renderer registration maps to different kinds of @@ -311,9 +305,10 @@ filename extensions. In this case, changing the extension from ``.pt`` to ``.jinja2`` passed the view response through the ``pyramid_jinja2`` renderer. -.. seealso:: See Also: `Jinja2 homepage <http://jinja.pocoo.org/>`_, - and - :ref:`pyramid_jinja2 Overview <jinja2:overview>` +.. seealso:: See Also: + :ref:`Quick Tutorial Jinja2 <qtut_jinja2>`, + `Jinja2 homepage <http://jinja.pocoo.org/>`_, and + :ref:`pyramid_jinja2 Overview <jinja2:overview>` Static Assets ============= @@ -358,9 +353,11 @@ By using ``request.static_url`` to generate the full URL to the static assets, you both ensure you stay in sync with the configuration and gain refactoring flexibility later. -.. seealso:: See Also: :doc:`../narr/assets`, - :ref:`preventing_http_caching`, and - :ref:`influencing_http_caching` +.. seealso:: See Also: + :ref:`Quick Tutorial Static Assets <qtut_static_assets>`, + :doc:`../narr/assets`, + :ref:`preventing_http_caching`, and + :ref:`influencing_http_caching` Returning JSON ============== @@ -377,9 +374,11 @@ This wires up a view that returns some data through the JSON :term:`renderer`, which calls Python's JSON support to serialize the data into JSON and set the appropriate HTTP headers. -.. seealso:: See Also: :ref:`views_which_use_a_renderer`, - :ref:`json_renderer`, and - :ref:`adding_and_overriding_renderers` +.. seealso:: See Also: + :ref:`Quick Tutorial JSON <qtut_json>`, + :ref:`views_which_use_a_renderer`, + :ref:`json_renderer`, and + :ref:`adding_and_overriding_renderers` View Classes ============ @@ -422,7 +421,20 @@ Only one route needed, stated in one place atop the view class. Also, the assignment of the ``name`` is done in the ``__init__``. Our templates can then use ``{{ view.name }}``. -.. seealso:: See Also: :ref:`class_as_view` +Pyramid view classes, combined with built-in and custom predicates, +have much more to offer: + +- All the same view configuration parameters as function views + +- One route leading to multiple views, based on information in the + request or data such as ``request_param``, ``request_method``, + ``accept``, ``header``, ``xhr``, ``containment``, and + ``custom_predicates`` + +.. seealso:: See Also: + :ref:`Quick Tutorial View Classes <qtut_view_classes>`, + :ref:`Quick Tutorial More View Classes <qtut_more_view_classes>`, and + :ref:`class_as_view` Quick Project Startup with Scaffolds ==================================== @@ -470,8 +482,10 @@ configuration. This includes a new way of running your application: Let's look at ``pserve`` and configuration in more depth. -.. seealso:: See Also: :ref:`project_narr` and - :doc:`../narr/scaffolding` +.. seealso:: See Also: + :ref:`Quick Tutorial Scaffolds <qtut_scaffolds>`, + :ref:`project_narr`, and + :doc:`../narr/scaffolding` Application Running with ``pserve`` =================================== @@ -499,7 +513,8 @@ Most of the work, though, comes from your project's wiring, as expressed in the configuration file you supply to ``pserve``. Let's take a look at this configuration file. -.. seealso:: See Also: :ref:`what_is_this_pserve_thing` +.. seealso:: See Also: + :ref:`what_is_this_pserve_thing` Configuration with ``.ini`` Files ================================= @@ -546,8 +561,10 @@ Additionally, the ``development.ini`` generated by this scaffold wired up Python's standard logging. We'll now see in the console, for example, a log on every request that comes in, as well traceback information. -.. seealso:: See Also: :ref:`environment_chapter` and - :doc:`../narr/paste` +.. seealso:: See Also: + :ref:`Quick Tutorial Application Configuration <qtut_ini>`, + :ref:`environment_chapter` and + :doc:`../narr/paste` Easier Development with ``debugtoolbar`` @@ -599,7 +616,10 @@ you want to disable this toolbar, no need to change code: you can remove it from ``pyramid.includes`` in the relevant ``.ini`` configuration file. -.. seealso:: See Also: :ref:`pyramid_debugtoolbar <toolbar:overview>` +.. seealso:: See Also: + :ref:`Quick Tutorial + pyramid_debugtoolbar <qtut_debugtoolbar>` and + :ref:`pyramid_debugtoolbar <toolbar:overview>` Unit Tests and ``nose`` ======================= @@ -650,7 +670,11 @@ Pyramid supplies helpers for test writing, which we use in the test setup and teardown. Our one test imports the view, makes a dummy request, and sees if the view returns what we expected. -.. seealso:: See Also: :ref:`testing_chapter` +.. seealso:: See Also: + :ref:`Quick Tutorial Unit Testing <qtut_unit_testing>`, + :ref:`Quick Tutorial Functional Testing <qtut_functional_testing>`, + and + :ref:`testing_chapter` Logging ======= @@ -693,7 +717,9 @@ visit ``http://localhost:6543`` your console will now show:: 2013-08-09 10:42:42,968 DEBUG [hello_world.views][MainThread] Some Message -.. seealso:: See Also: :ref:`logging_chapter` +.. seealso:: See Also: + :ref:`Quick Tutorial Logging <qtut_logging>` and + :ref:`logging_chapter` Sessions ======== @@ -703,8 +729,8 @@ that requires semi-permanent data to be saved. For example, a shopping cart. This is called a :term:`session`. Pyramid has basic built-in support for sessions, with add-ons such as -*Beaker* (or your own custom sessioning engine) that provide richer -session support. Let's take a look at the +``pyramid_redis_sessions`` (or your own custom sessioning engine) that provide +richer session support. Let's take a look at the :doc:`built-in sessioning support <../narr/sessions>`. In our ``__init__.py`` we first import the kind of sessioning we want: @@ -740,9 +766,9 @@ Jinja2 template: :end-before: End Sphinx Include 1 .. seealso:: See Also: - :ref:`sessions_chapter`, :ref:`flash_messages`, - :ref:`session_module`, and - :ref:`Beaker sessioning middleware <beaker:overview>` + :ref:`Quick Tutorial Sessions <qtut_sessions>`, + :ref:`sessions_chapter`, :ref:`flash_messages`, + :ref:`session_module`, and :term:`pyramid_redis_sessions`. Databases ========= @@ -787,10 +813,12 @@ of the system, can then easily get at the data thanks to SQLAlchemy: :start-after: Start Sphinx Include :end-before: End Sphinx Include -.. seealso:: See Also: `SQLAlchemy <http://www.sqlalchemy.org/>`_, - :ref:`making_a_console_script`, - :ref:`bfg_sql_wiki_tutorial`, and - :ref:`Application Transactions With pyramid_tm <tm:overview>` +.. seealso:: See Also: + :ref:`Quick Tutorial Databases <qtut_databases>`, + `SQLAlchemy <http://www.sqlalchemy.org/>`_, + :ref:`making_a_console_script`, + :ref:`bfg_sql_wiki_tutorial`, and + :ref:`Application Transactions With pyramid_tm <tm:overview>` Forms ===== @@ -849,9 +877,10 @@ widgets using attractive CSS from Bootstrap and more powerful widgets from Chosen. .. seealso:: See Also: - :ref:`Deform <deform:overview>`, - :ref:`Colander <colander:overview>`, and - `deform_bootstrap <https://pypi.python.org/pypi/deform_bootstrap>`_ + :ref:`Quick Tutorial Forms <qtut_forms>`, + :ref:`Deform <deform:overview>`, + :ref:`Colander <colander:overview>`, and + `deform_bootstrap <https://pypi.python.org/pypi/deform_bootstrap>`_ Conclusion ========== diff --git a/docs/quick_tour/jinja2/hello_world.jinja2 b/docs/quick_tour/jinja2/hello_world.jinja2 index e177744b5..7a902dd3a 100644 --- a/docs/quick_tour/jinja2/hello_world.jinja2 +++ b/docs/quick_tour/jinja2/hello_world.jinja2 @@ -1,4 +1,5 @@ -<html xmlns="http://www.w3.org/1999/xhtml"> +<!DOCTYPE html> +<html lang="en"> <head> <title>Hello World</title> </head> diff --git a/docs/quick_tour/package/hello_world/__init__.py b/docs/quick_tour/package/hello_world/__init__.py index 6e66bf40a..4a4fbec30 100644 --- a/docs/quick_tour/package/hello_world/__init__.py +++ b/docs/quick_tour/package/hello_world/__init__.py @@ -1,7 +1,7 @@ from pyramid.config import Configurator from pyramid_jinja2 import renderer_factory # Start Sphinx Include 1 -from pyramid.session import UnencryptedCookieSessionFactoryConfig +from pyramid.session import SignedCookieSessionFactory # End Sphinx Include 1 from hello_world.models import get_root @@ -16,7 +16,7 @@ def main(global_config, **settings): settings.setdefault('jinja2.i18n.domain', 'hello_world') # Start Sphinx Include 2 - my_session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet') + my_session_factory = SignedCookieSessionFactory('itsaseekreet') config = Configurator(root_factory=get_root, settings=settings, session_factory=my_session_factory) # End Sphinx Include 2 diff --git a/docs/quick_tour/package/hello_world/init.py b/docs/quick_tour/package/hello_world/init.py index 9d7ec43d8..5b5f6a118 100644 --- a/docs/quick_tour/package/hello_world/init.py +++ b/docs/quick_tour/package/hello_world/init.py @@ -1,7 +1,7 @@ from pyramid.config import Configurator from pyramid_jinja2 import renderer_factory # Start Sphinx 1 -from pyramid.session import UnencryptedCookieSessionFactoryConfig +from pyramid.session import SignedCookieSessionFactory # End Sphinx 1 from hello_world.models import get_root @@ -22,7 +22,7 @@ def main(global_config, **settings): # End Include # Start Sphinx Include 2 - my_session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet') + my_session_factory = SignedCookieSessionFactory('itsaseekreet') config = Configurator(session_factory=my_session_factory) # End Sphinx Include 2 diff --git a/docs/quick_tutorial/authentication.rst b/docs/quick_tutorial/authentication.rst new file mode 100644 index 000000000..8380a75ed --- /dev/null +++ b/docs/quick_tutorial/authentication.rst @@ -0,0 +1,134 @@ +============================== +20: Logins With Authentication +============================== + +Login views that authenticate a username/password against a list of +users. + +Background +========== + +Most web applications have URLs that allow people to add/edit/delete +content via a web browser. Time to add +:ref:`security <security_chapter>` +to the application. In this first step we introduce authentication. +That is, logging in and logging out using Pyramid's rich facilities for +pluggable user storages. + +In the next step we will introduce protection resources with +authorization security statements. + +Objectives +========== + +- Introduce the Pyramid concepts of authentication + +- Create login/logout views + +Steps +===== + +#. We are going to use the view classes step as our starting point: + + .. code-block:: bash + + $ cd ..; cp -r view_classes authentication; cd authentication + $ $VENV/bin/python setup.py develop + +#. Put the security hash in the ``authentication/development.ini`` + configuration file as ``tutorial.secret`` instead of putting it in + the code: + + .. literalinclude:: authentication/development.ini + :language: ini + :linenos: + +#. Get authentication (and for now, authorization policies) and login + route into the :term:`configurator` in + ``authentication/tutorial/__init__.py``: + + .. literalinclude:: authentication/tutorial/__init__.py + :linenos: + +#. Create a ``authentication/tutorial/security.py`` module that can find + our user information by providing an *authentication policy callback*: + + .. literalinclude:: authentication/tutorial/security.py + :linenos: + +#. Update the views in ``authentication/tutorial/views.py``: + + .. literalinclude:: authentication/tutorial/views.py + :linenos: + +#. Add a login template at ``authentication/tutorial/login.pt``: + + .. literalinclude:: authentication/tutorial/login.pt + :language: html + :linenos: + +#. Provide a login/logout box in ``authentication/tutorial/home.pt`` + + .. literalinclude:: authentication/tutorial/home.pt + :language: html + :linenos: + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in a browser. + +#. Click the "Log In" link. + +#. Submit the login form with the username ``editor`` and the password + ``editor``. + +#. Note that the "Log In" link has changed to "Logout". + +#. Click the "Logout" link. + +Analysis +======== + +Unlike many web frameworks, Pyramid includes a built-in (but optional) +security model for authentication and authorization. This security +system is intended to be flexible and support many needs. In this +security model, authentication (who are you) and authorization (what +are you allowed to do) are not just pluggable, but de-coupled. To learn +one step at a time, we provide a system that identifies users and lets +them log out. + +In this example we chose to use the bundled +:ref:`AuthTktAuthenticationPolicy <authentication_module>` +policy. We enabled it in our configuration and provided a +ticket-signing secret in our INI file. + +Our view class grew a login view. When you reached it via a GET, +it returned a login form. When reached via POST, it processed the +username and password against the "groupfinder" callable that we +registered in the configuration. + +In our template, we fetched the ``logged_in`` value from the view +class. We use this to calculate the logged-in user, +if any. In the template we can then choose to show a login link to +anonymous visitors or a logout link to logged-in users. + +Extra Credit +============ + +#. What is the difference between a user and a principal? + +#. Can I use a database behind my ``groupfinder`` to look up principals? + +#. Do I have to put a ``renderer`` in my ``@forbidden_view_config`` + decorator? + +#. Once I am logged in, does any user-centric information get jammed + onto each request? Use ``import pdb; pdb.set_trace()`` to answer + this. + +.. seealso:: :ref:`security_chapter`, + :ref:`AuthTktAuthenticationPolicy <authentication_module>` diff --git a/docs/quick_tutorial/authentication/development.ini b/docs/quick_tutorial/authentication/development.ini new file mode 100644 index 000000000..5d4580ff5 --- /dev/null +++ b/docs/quick_tutorial/authentication/development.ini @@ -0,0 +1,42 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar +tutorial.secret = 98zd + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/authentication/setup.py b/docs/quick_tutorial/authentication/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/authentication/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/__init__.py b/docs/quick_tutorial/authentication/tutorial/__init__.py new file mode 100644 index 000000000..efc09e760 --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/__init__.py @@ -0,0 +1,25 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.config import Configurator + +from .security import groupfinder + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + + # Security policies + authn_policy = AuthTktAuthenticationPolicy( + settings['tutorial.secret'], callback=groupfinder, + hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/home.pt b/docs/quick_tutorial/authentication/tutorial/home.pt new file mode 100644 index 000000000..6ecd0081b --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/home.pt @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> + +<div> + <a tal:condition="view.logged_in is None" + href="${request.application_url}/login">Log In</a> + <a tal:condition="view.logged_in is not None" + href="${request.application_url}/logout">Logout</a> +</div> + +<h1>Hi ${name}</h1> +<p>Visit <a href="${request.route_url('hello')}">hello</a></p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/login.pt b/docs/quick_tutorial/authentication/tutorial/login.pt new file mode 100644 index 000000000..4451fc4f8 --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/login.pt @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> +<h1>Login</h1> +<span tal:replace="message"/> + +<form action="${url}" method="post"> + <input type="hidden" name="came_from" + value="${came_from}"/> + <label for="login">Username</label> + <input type="text" id="login" + name="login" + value="${login}"/><br/> + <label for="password">Password</label> + <input type="password" id="password" + name="password" + value="${password}"/><br/> + <input type="submit" name="form.submitted" + value="Log In"/> +</form> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/security.py b/docs/quick_tutorial/authentication/tutorial/security.py new file mode 100644 index 000000000..ab90bab2c --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/security.py @@ -0,0 +1,8 @@ +USERS = {'editor': 'editor', + 'viewer': 'viewer'} +GROUPS = {'editor': ['group:editors']} + + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, [])
\ No newline at end of file diff --git a/docs/quick_tutorial/authentication/tutorial/views.py b/docs/quick_tutorial/authentication/tutorial/views.py new file mode 100644 index 000000000..ab46eb2dd --- /dev/null +++ b/docs/quick_tutorial/authentication/tutorial/views.py @@ -0,0 +1,64 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) + +from pyramid.view import ( + view_config, + view_defaults + ) + +from .security import USERS + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + self.logged_in = request.authenticated_userid + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} + + @view_config(route_name='login', renderer='login.pt') + def login(self): + request = self.request + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) + message = 'Failed login' + + return dict( + name='Login', + message=message, + url=request.application_url + '/login', + came_from=came_from, + login=login, + password=password, + ) + + @view_config(route_name='logout') + def logout(self): + request = self.request + headers = forget(request) + url = request.route_url('home') + return HTTPFound(location=url, + headers=headers) diff --git a/docs/quick_tutorial/authorization.rst b/docs/quick_tutorial/authorization.rst new file mode 100644 index 000000000..6b10d3409 --- /dev/null +++ b/docs/quick_tutorial/authorization.rst @@ -0,0 +1,112 @@ +=========================================== +21: Protecting Resources With Authorization +=========================================== + +Assign security statements to resources describing the permissions +required to perform an operation. + +Background +========== + +Our application has URLs that allow people to add/edit/delete content +via a web browser. Time to add security to the application. Let's +protect our add/edit views to require a login (username of +``editor`` and password of ``editor``.) We will allow the other views +to continue working without a password. + +Objectives +========== + +- Introduce the Pyramid concepts of authentication, authorization, + permissions, and access control lists (ACLs) + +- Make a :term:`root factory` that returns an instance of our + class for the top of the application + +- Assign security statements to our root resource + +- Add a permissions predicate on a view + +- Provide a :term:`Forbidden view` to handle visiting a URL without + adequate permissions + +Steps +===== + +#. We are going to use the authentication step as our starting point: + + .. code-block:: bash + + $ cd ..; cp -r authentication authorization; cd authorization + $ $VENV/bin/python setup.py develop + +#. Start by changing ``authorization/tutorial/__init__.py`` to + specify a root factory to the :term:`configurator`: + + .. literalinclude:: authorization/tutorial/__init__.py + :linenos: + +#. That means we need to implement + ``authorization/tutorial/resources.py`` + + .. literalinclude:: authorization/tutorial/resources.py + :linenos: + +#. Change ``authorization/tutorial/views.py`` to require the ``edit`` + permission on the ``hello`` view and implement the forbidden view: + + .. literalinclude:: authorization/tutorial/views.py + :linenos: + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in a browser. + +#. If you are still logged in, click the "Log Out" link. + +#. Visit http://localhost:6543/howdy in a browser. You should be + asked to login. + +Analysis +======== + +This simple tutorial step can be boiled down to the following: + +- A view can require a *permission* (``edit``) + +- The context for our view (the ``Root``) has an access control list + (ACL) + +- This ACL says that the ``edit`` permission is available on ``Root`` + to the ``group:editors`` *principal* + +- The registered ``groupfinder`` answers whether a particular user + (``editor``) has a particular group (``group:editors``) + +In summary: ``hello`` wants ``edit`` permission, ``Root`` says +``group:editors`` has ``edit`` permission. + +Of course, this only applies on ``Root``. Some other part of the site +(a.k.a. *context*) might have a different ACL. + +If you are not logged in and visit ``/hello``, you need to get +shown the login screen. How does Pyramid know what is the login page to +use? We explicitly told Pyramid that the ``login`` view should be used +by decorating the view with ``@forbidden_view_config``. + +Extra Credit +============ + +#. Perhaps you would like experience of not having enough permissions + (forbidden) to be richer. How could you change this? + +#. Perhaps we want to store security statements in a database and + allow editing via a browser. How might this be done? + +#. What if we want different security statements on different kinds of + objects? Or on the same kinds of objects, but in different parts of a + URL hierarchy? diff --git a/docs/quick_tutorial/authorization/development.ini b/docs/quick_tutorial/authorization/development.ini new file mode 100644 index 000000000..5d4580ff5 --- /dev/null +++ b/docs/quick_tutorial/authorization/development.ini @@ -0,0 +1,42 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar +tutorial.secret = 98zd + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/authorization/setup.py b/docs/quick_tutorial/authorization/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/authorization/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/__init__.py b/docs/quick_tutorial/authorization/tutorial/__init__.py new file mode 100644 index 000000000..8f7ab8277 --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/__init__.py @@ -0,0 +1,26 @@ +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.config import Configurator + +from .security import groupfinder + + +def main(global_config, **settings): + config = Configurator(settings=settings, + root_factory='.resources.Root') + config.include('pyramid_chameleon') + + # Security policies + authn_policy = AuthTktAuthenticationPolicy( + settings['tutorial.secret'], callback=groupfinder, + hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/home.pt b/docs/quick_tutorial/authorization/tutorial/home.pt new file mode 100644 index 000000000..6ecd0081b --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/home.pt @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> + +<div> + <a tal:condition="view.logged_in is None" + href="${request.application_url}/login">Log In</a> + <a tal:condition="view.logged_in is not None" + href="${request.application_url}/logout">Logout</a> +</div> + +<h1>Hi ${name}</h1> +<p>Visit <a href="${request.route_url('hello')}">hello</a></p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/login.pt b/docs/quick_tutorial/authorization/tutorial/login.pt new file mode 100644 index 000000000..4451fc4f8 --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/login.pt @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> +<h1>Login</h1> +<span tal:replace="message"/> + +<form action="${url}" method="post"> + <input type="hidden" name="came_from" + value="${came_from}"/> + <label for="login">Username</label> + <input type="text" id="login" + name="login" + value="${login}"/><br/> + <label for="password">Password</label> + <input type="password" id="password" + name="password" + value="${password}"/><br/> + <input type="submit" name="form.submitted" + value="Log In"/> +</form> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/resources.py b/docs/quick_tutorial/authorization/tutorial/resources.py new file mode 100644 index 000000000..0cb656f12 --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/resources.py @@ -0,0 +1,9 @@ +from pyramid.security import Allow, Everyone + + +class Root(object): + __acl__ = [(Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit')] + + def __init__(self, request): + pass
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/security.py b/docs/quick_tutorial/authorization/tutorial/security.py new file mode 100644 index 000000000..ab90bab2c --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/security.py @@ -0,0 +1,8 @@ +USERS = {'editor': 'editor', + 'viewer': 'viewer'} +GROUPS = {'editor': ['group:editors']} + + +def groupfinder(userid, request): + if userid in USERS: + return GROUPS.get(userid, [])
\ No newline at end of file diff --git a/docs/quick_tutorial/authorization/tutorial/views.py b/docs/quick_tutorial/authorization/tutorial/views.py new file mode 100644 index 000000000..43d14455a --- /dev/null +++ b/docs/quick_tutorial/authorization/tutorial/views.py @@ -0,0 +1,66 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.security import ( + remember, + forget, + ) + +from pyramid.view import ( + view_config, + view_defaults, + forbidden_view_config + ) + +from .security import USERS + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + self.logged_in = request.authenticated_userid + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello', permission='edit') + def hello(self): + return {'name': 'Hello View'} + + @view_config(route_name='login', renderer='login.pt') + @forbidden_view_config(renderer='login.pt') + def login(self): + request = self.request + login_url = request.route_url('login') + referrer = request.url + if referrer == login_url: + referrer = '/' # never use login form itself as came_from + came_from = request.params.get('came_from', referrer) + message = '' + login = '' + password = '' + if 'form.submitted' in request.params: + login = request.params['login'] + password = request.params['password'] + if USERS.get(login) == password: + headers = remember(request, login) + return HTTPFound(location=came_from, + headers=headers) + message = 'Failed login' + + return dict( + name='Login', + message=message, + url=request.application_url + '/login', + came_from=came_from, + login=login, + password=password, + ) + + @view_config(route_name='logout') + def logout(self): + request = self.request + headers = forget(request) + url = request.route_url('home') + return HTTPFound(location=url, + headers=headers) diff --git a/docs/quick_tutorial/conf.py b/docs/quick_tutorial/conf.py new file mode 100644 index 000000000..47b8fae41 --- /dev/null +++ b/docs/quick_tutorial/conf.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +# +# Getting Started with Pyramid and REST documentation build configuration file, created by +# sphinx-quickstart on Mon Aug 26 14:44:57 2013. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.intersphinx'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Getting Started with Pyramid and REST' +copyright = u'2013, Agendaless Consulting' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.0' +# The full version, including alpha/beta/rc tags. +release = '1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'GettingStartedwithPyramidandRESTdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + #'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'GettingStartedwithPyramidandREST.tex', + u'Getting Started with Pyramid and REST Documentation', + u'Agendaless Consulting', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'gettingstartedwithpyramidandrest', + u'Getting Started with Pyramid and REST Documentation', + [u'Agendaless Consulting'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'GettingStartedwithPyramidandREST', + u'Getting Started with Pyramid and REST Documentation', + u'Agendaless Consulting', 'GettingStartedwithPyramidandREST', + 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ( + 'http://docs.python.org/2', + None), + 'sqla': ( + 'http://docs.sqlalchemy.org/en/latest', + None), + 'pyramid': ( + 'http://docs.pylonsproject.org/projects/pyramid/en/latest/', + None), + 'jinja2': ( + 'http://docs.pylonsproject.org/projects/pyramid_jinja2/en/latest/', + None), + 'toolbar': ( + 'http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest', + None), + 'deform': ( + 'http://docs.pylonsproject.org/projects/deform/en/latest', + None), + 'colander': ( + 'http://docs.pylonsproject.org/projects/colander/en/latest', + None), + 'tutorials': ( + 'http://docs.pylonsproject.org/projects/pyramid_tutorials/en/latest/', + None), +} diff --git a/docs/quick_tutorial/databases.rst b/docs/quick_tutorial/databases.rst new file mode 100644 index 000000000..20b3cd46d --- /dev/null +++ b/docs/quick_tutorial/databases.rst @@ -0,0 +1,186 @@ +.. _qtut_databases: + +============================== +19: Databases Using SQLAlchemy +============================== + +Store/retrieve data using the SQLAlchemy ORM atop the SQLite database. + +Background +========== + +Our Pyramid-based wiki application now needs database-backed storage of +pages. This frequently means a SQL database. The Pyramid community +strongly supports the +:ref:`SQLAlchemy <sqla:index_toplevel>` project and its +:ref:`object-relational mapper (ORM) <sqla:ormtutorial_toplevel>` +as a convenient, Pythonic way to interface to databases. + +In this step we hook up SQLAlchemy to a SQLite database table, +providing storage and retrieval for the wikipages in the previous step. + +.. note:: + + The ``alchemy`` scaffold is really helpful for getting a + SQLAlchemy project going, including generation of the console + script. Since we want to see all the decisions, we will forgo + convenience in this tutorial and wire it up ourselves. + +Objectives +========== + +- Store pages in SQLite by using SQLAlchemy models + +- Use SQLAlchemy queries to list/add/view/edit pages + +- Provide a database-initialize command by writing a Pyramid *console + script* which can be run from the command line + +Steps +===== + +#. We are going to use the forms step as our starting point: + + .. code-block:: bash + + $ cd ..; cp -r forms databases; cd databases + +#. We need to add some dependencies in ``databases/setup.py`` as well + as an "entry point" for the command-line script: + + .. literalinclude:: databases/setup.py + :linenos: + + .. note:: + + We aren't yet doing ``python3.3 setup.py develop`` as we + are changing it later. + +#. Our configuration file at ``databases/development.ini`` wires + together some new pieces: + + .. literalinclude:: databases/development.ini + :language: ini + +#. This engine configuration now needs to be read into the application + through changes in ``databases/tutorial/__init__.py``: + + .. literalinclude:: databases/tutorial/__init__.py + :linenos: + +#. Make a command-line script at ``databases/tutorial/initialize_db.py`` + to initialize the database: + + .. literalinclude:: databases/tutorial/initialize_db.py + +#. Since ``setup.py`` changed, we now run it: + + .. code-block:: bash + + $ $VENV/bin/python setup.py develop + +#. The script references some models in ``databases/tutorial/models.py``: + + .. literalinclude:: databases/tutorial/models.py + :linenos: + +#. Let's run this console script, thus producing our database and table: + + .. code-block:: bash + + $ $VENV/bin/initialize_tutorial_db development.ini + 2013-09-06 15:54:08,050 INFO [sqlalchemy.engine.base.Engine][MainThread] PRAGMA table_info("wikipages") + 2013-09-06 15:54:08,050 INFO [sqlalchemy.engine.base.Engine][MainThread] () + 2013-09-06 15:54:08,051 INFO [sqlalchemy.engine.base.Engine][MainThread] + CREATE TABLE wikipages ( + uid INTEGER NOT NULL, + title TEXT, + body TEXT, + PRIMARY KEY (uid), + UNIQUE (title) + ) + +#. With our data now driven by SQLAlchemy queries, we need to update + our ``databases/tutorial/views.py``: + + .. literalinclude:: databases/tutorial/views.py + +#. Our tests in ``databases/tutorial/tests.py`` changed to include + SQLAlchemy bootstrapping: + + .. literalinclude:: databases/tutorial/tests.py + :linenos: + +#. Run the tests in your package using ``nose``: + + .. code-block:: bash + + $ $VENV/bin/nosetests . + .. + ----------------------------------------------------------------- + Ran 2 tests in 1.141s + + OK + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in a browser. + +Analysis +======== + +Let's start with the dependencies. We made the decision to use +``SQLAlchemy`` to talk to our database. We also, though, installed +``pyramid_tm`` and ``zope.sqlalchemy``. Why? + +Pyramid has a strong orientation towards support for ``transactions``. +Specifically, you can install a transaction manager into your app +application, either as middleware or a Pyramid "tween". Then, +just before you return the response, all transaction-aware parts of +your application are executed. + +This means Pyramid view code usually doesn't manage transactions. If +your view code or a template generates an error, the transaction manager +aborts the transaction. This is a very liberating way to write code. + +The ``pyramid_tm`` package provides a "tween" that is configured in the +``development.ini`` configuration file. That installs it. We then need +a package that makes SQLAlchemy and thus the RDBMS transaction manager +integrate with the Pyramid transaction manager. That's what +``zope.sqlalchemy`` does. + +Where do we point at the location on disk for the SQLite file? In the +configuration file. This lets consumers of our package change the +location in a safe (non-code) way. That is, in configuration. This +configuration-oriented approach isn't required in Pyramid; you can +still make such statements in your ``__init__.py`` or some companion +module. + +The ``initialize_tutorial_db`` is a nice example of framework support. +You point your setup at the location of some ``[console_scripts]`` and +these get generated into your virtualenv's ``bin`` directory. Our +console script follows the pattern of being fed a configuration file +with all the bootstrapping. It then opens SQLAlchemy and creates the +root of the wiki, which also makes the SQLite file. Note the +``with transaction.manager`` part that puts the work in the scope of a +transaction (as we aren't inside a web request where this is done +automatically.) + +The ``models.py`` does a little bit extra work to hook up SQLAlchemy +into the Pyramid transaction manager. It then declares the model for a +``Page``. + +Our views have changes primarily around replacing our dummy +dictionary-of-dictionaries data with proper database support: list the +rows, add a row, edit a row, and delete a row. + +Extra Credit +============ + +#. Why all this code? Why can't I just type 2 lines and have magic ensue? + +#. Give a try at a button that deletes a wiki page. diff --git a/docs/quick_tutorial/databases/development.ini b/docs/quick_tutorial/databases/development.ini new file mode 100644 index 000000000..270da960f --- /dev/null +++ b/docs/quick_tutorial/databases/development.ini @@ -0,0 +1,49 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + pyramid_tm + +sqlalchemy.url = sqlite:///%(here)s/sqltutorial.sqlite + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial, sqlalchemy + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/databases/setup.py b/docs/quick_tutorial/databases/setup.py new file mode 100644 index 000000000..238358fe4 --- /dev/null +++ b/docs/quick_tutorial/databases/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'deform', + 'sqlalchemy', + 'pyramid_tm', + 'zope.sqlalchemy' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + [console_scripts] + initialize_tutorial_db = tutorial.initialize_db:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/databases/sqltutorial.sqlite b/docs/quick_tutorial/databases/sqltutorial.sqlite Binary files differnew file mode 100644 index 000000000..b8bd856fd --- /dev/null +++ b/docs/quick_tutorial/databases/sqltutorial.sqlite diff --git a/docs/quick_tutorial/databases/tutorial/__init__.py b/docs/quick_tutorial/databases/tutorial/__init__.py new file mode 100644 index 000000000..74aa25740 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/__init__.py @@ -0,0 +1,21 @@ +from pyramid.config import Configurator + +from sqlalchemy import engine_from_config + +from .models import DBSession, Base + +def main(global_config, **settings): + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.bind = engine + + config = Configurator(settings=settings, + root_factory='tutorial.models.Root') + config.include('pyramid_chameleon') + config.add_route('wiki_view', '/') + config.add_route('wikipage_add', '/add') + config.add_route('wikipage_view', '/{uid}') + config.add_route('wikipage_edit', '/{uid}/edit') + config.add_static_view('deform_static', 'deform:static/') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/initialize_db.py b/docs/quick_tutorial/databases/tutorial/initialize_db.py new file mode 100644 index 000000000..98be524a1 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/initialize_db.py @@ -0,0 +1,37 @@ +import os +import sys +import transaction + +from sqlalchemy import engine_from_config + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from .models import ( + DBSession, + Page, + Base, + ) + + +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) + settings = get_appsettings(config_uri) + engine = engine_from_config(settings, 'sqlalchemy.') + DBSession.configure(bind=engine) + Base.metadata.create_all(engine) + with transaction.manager: + model = Page(title='Root', body='<p>Root</p>') + DBSession.add(model) diff --git a/docs/quick_tutorial/databases/tutorial/models.py b/docs/quick_tutorial/databases/tutorial/models.py new file mode 100644 index 000000000..b27c38417 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/models.py @@ -0,0 +1,35 @@ +from pyramid.security import Allow, Everyone + +from sqlalchemy import ( + Column, + Integer, + Text, + ) + +from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy.orm import ( + scoped_session, + sessionmaker, + ) + +from zope.sqlalchemy import ZopeTransactionExtension + +DBSession = scoped_session( + sessionmaker(extension=ZopeTransactionExtension())) +Base = declarative_base() + + +class Page(Base): + __tablename__ = 'wikipages' + uid = Column(Integer, primary_key=True) + title = Column(Text, unique=True) + body = Column(Text) + + +class Root(object): + __acl__ = [(Allow, Everyone, 'view'), + (Allow, 'group:editors', 'edit')] + + def __init__(self, request): + pass
\ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/tests.py b/docs/quick_tutorial/databases/tutorial/tests.py new file mode 100644 index 000000000..e18e70c8c --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/tests.py @@ -0,0 +1,58 @@ +import unittest +import transaction + +from pyramid import testing + + +def _initTestingDB(): + from sqlalchemy import create_engine + from .models import ( + DBSession, + Page, + Base + ) + engine = create_engine('sqlite://') + Base.metadata.create_all(engine) + DBSession.configure(bind=engine) + with transaction.manager: + model = Page(title='FrontPage', body='This is the front page') + DBSession.add(model) + return DBSession + + +class WikiViewTests(unittest.TestCase): + def setUp(self): + self.session = _initTestingDB() + self.config = testing.setUp() + + def tearDown(self): + self.session.remove() + testing.tearDown() + + def test_wiki_view(self): + from tutorial.views import WikiViews + + request = testing.DummyRequest() + inst = WikiViews(request) + response = inst.wiki_view() + self.assertEqual(response['title'], 'Wiki View') + + +class WikiFunctionalTests(unittest.TestCase): + def setUp(self): + self.session = _initTestingDB() + self.config = testing.setUp() + from pyramid.paster import get_app + app = get_app('development.ini') + from webtest import TestApp + self.testapp = TestApp(app) + + def tearDown(self): + self.session.remove() + testing.tearDown() + + def test_it(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'Wiki: View', res.body) + res = self.testapp.get('/add', status=200) + self.assertIn(b'Add/Edit', res.body) diff --git a/docs/quick_tutorial/databases/tutorial/views.py b/docs/quick_tutorial/databases/tutorial/views.py new file mode 100644 index 000000000..4608c6d43 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/views.py @@ -0,0 +1,96 @@ +import colander +import deform.widget + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from .models import DBSession, Page + + +class WikiPage(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + body = colander.SchemaNode( + colander.String(), + widget=deform.widget.RichTextWidget() + ) + + +class WikiViews(object): + def __init__(self, request): + self.request = request + + @property + def wiki_form(self): + schema = WikiPage() + return deform.Form(schema, buttons=('submit',)) + + @property + def reqts(self): + return self.wiki_form.get_widget_resources() + + @view_config(route_name='wiki_view', renderer='wiki_view.pt') + def wiki_view(self): + pages = DBSession.query(Page).order_by(Page.title) + return dict(title='Wiki View', pages=pages) + + @view_config(route_name='wikipage_add', + renderer='wikipage_addedit.pt') + def wikipage_add(self): + form = self.wiki_form.render() + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = self.wiki_form.validate(controls) + except deform.ValidationFailure as e: + # Form is NOT valid + return dict(form=e.render()) + + # Add a new page to the database + new_title = appstruct['title'] + new_body = appstruct['body'] + DBSession.add(Page(title=new_title, body=new_body)) + + # Get the new ID and redirect + page = DBSession.query(Page).filter_by(title=new_title).one() + new_uid = page.uid + + url = self.request.route_url('wikipage_view', uid=new_uid) + return HTTPFound(url) + + return dict(form=form) + + + @view_config(route_name='wikipage_view', renderer='wikipage_view.pt') + def wikipage_view(self): + uid = int(self.request.matchdict['uid']) + page = DBSession.query(Page).filter_by(uid=uid).one() + return dict(page=page) + + + @view_config(route_name='wikipage_edit', + renderer='wikipage_addedit.pt') + def wikipage_edit(self): + uid = int(self.request.matchdict['uid']) + page = DBSession.query(Page).filter_by(uid=uid).one() + + wiki_form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = wiki_form.validate(controls) + except deform.ValidationFailure as e: + return dict(page=page, form=e.render()) + + # Change the content and redirect to the view + page.title = appstruct['title'] + page.body = appstruct['body'] + url = self.request.route_url('wikipage_view', uid=uid) + return HTTPFound(url) + + form = self.wiki_form.render(dict( + uid=page.uid, title=page.title, body=page.body) + ) + + return dict(page=page, form=form) diff --git a/docs/quick_tutorial/databases/tutorial/wiki_view.pt b/docs/quick_tutorial/databases/tutorial/wiki_view.pt new file mode 100644 index 000000000..9e3afe495 --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/wiki_view.pt @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Wiki: View</title> +</head> +<body> +<h1>Wiki</h1> + +<a href="${request.route_url('wikipage_add')}">Add + WikiPage</a> +<ul> + <li tal:repeat="page pages"> + <a href="${request.route_url('wikipage_view', uid=page.uid)}"> + ${page.title} + </a> + </li> +</ul> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt new file mode 100644 index 000000000..d1fea0d7f --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/wikipage_addedit.pt @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: Add/Edit</title> + <tal:block tal:repeat="reqt view.reqts['css']"> + <link rel="stylesheet" type="text/css" + href="${request.static_url('deform:static/' + reqt)}"/> + </tal:block> + <tal:block tal:repeat="reqt view.reqts['js']"> + <script src="${request.static_url('deform:static/' + reqt)}" + type="text/javascript"></script> + </tal:block> +</head> +<body> +<h1>Wiki</h1> + +<p>${structure: form}</p> +<script type="text/javascript"> + deform.load() +</script> +</body> +</html> diff --git a/docs/quick_tutorial/databases/tutorial/wikipage_view.pt b/docs/quick_tutorial/databases/tutorial/wikipage_view.pt new file mode 100644 index 000000000..cb9ff526e --- /dev/null +++ b/docs/quick_tutorial/databases/tutorial/wikipage_view.pt @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: View</title> +</head> +<body> +<a href="${request.route_url('wiki_view')}"> + Up +</a> | +<a href="${request.route_url('wikipage_edit', uid=page.uid)}"> + Edit +</a> + +<h1>${page.title}</h1> +<p>${structure: page.body}</p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/debugtoolbar.rst b/docs/quick_tutorial/debugtoolbar.rst new file mode 100644 index 000000000..d25588c49 --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar.rst @@ -0,0 +1,89 @@ +.. _qtut_debugtoolbar: + +============================================ +04: Easier Development with ``debugtoolbar`` +============================================ + +Error-handling and introspection using the ``pyramid_debugtoolbar`` +add-on. + +Background +========== + +As we introduce the basics we also want to show how to be productive in +development and debugging. For example, we just discussed template +reloading and earlier we showed ``--reload`` for application reloading. + +``pyramid_debugtoolbar`` is a popular Pyramid add-on which makes +several tools available in your browser. Adding it to your project +illustrates several points about configuration. + +Objectives +========== + +- Install and enable the toolbar to help during development + +- Explain Pyramid add-ons + +- Show how an add-on gets configured into your application + +Steps +===== + +#. First we copy the results of the previous step, as well as install + the ``pyramid_debugtoolbar`` package: + + .. code-block:: bash + + $ cd ..; cp -r ini debugtoolbar; cd debugtoolbar + $ $VENV/bin/python setup.py develop + $ $VENV/bin/easy_install pyramid_debugtoolbar + + +#. Our ``debugtoolbar/development.ini`` gets a configuration entry for + ``pyramid.includes``: + + .. literalinclude:: debugtoolbar/development.ini + :language: ini + :linenos: + +#. Run the WSGI application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in your browser. See the handy + toolbar on the right. + +Analysis +======== + +``pyramid_debugtoolbar`` is a full-fledged Python package, +available on PyPI just like thousands of other Python packages. Thus we +start by installing the ``pyramid_debugtoolbar`` package into our +virtual environment using normal Python package installation commands. + +The ``pyramid_debugtoolbar`` Python package is also a Pyramid add-on, +which means we need to include its add-on configuration into our web +application. We could do this with imperative configuration in +``tutorial/__init__.py`` by using ``config.include``. Pyramid also +supports wiring in add-on configuration via our ``development.ini`` +using ``pyramid.includes``. We use this to load the configuration for +the debugtoolbar. + +You'll now see an attractive (and collapsible) menu in the right of +your browser, providing introspective access to debugging information. +Even better, if your web application generates an error, +you will see a nice traceback on the screen. When you want to disable +this toolbar, no need to change code: you can remove it from +``pyramid.includes`` in the relevant ``.ini`` configuration file (thus +showing why configuration files are handy.) + +Note that the toolbar mutates the HTML generated by our app and uses jQuery to +overlay itself. If you are using the toolbar while you're developing and you +start to experience otherwise inexplicable client-side weirdness, you can shut +it off by commenting out the ``pyramid_debugtoolbar`` line in +``pyramid.includes`` temporarily. + +.. seealso:: See Also: :ref:`pyramid_debugtoolbar <toolbar:overview>` diff --git a/docs/quick_tutorial/debugtoolbar/development.ini b/docs/quick_tutorial/debugtoolbar/development.ini new file mode 100644 index 000000000..470d92c57 --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar/development.ini @@ -0,0 +1,40 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/debugtoolbar/setup.py b/docs/quick_tutorial/debugtoolbar/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/debugtoolbar/tutorial/__init__.py b/docs/quick_tutorial/debugtoolbar/tutorial/__init__.py new file mode 100644 index 000000000..0993b25be --- /dev/null +++ b/docs/quick_tutorial/debugtoolbar/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return xResponse('<body><h1>Hello World!</h1></body>') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/forms.rst b/docs/quick_tutorial/forms.rst new file mode 100644 index 000000000..e8bc0c8b4 --- /dev/null +++ b/docs/quick_tutorial/forms.rst @@ -0,0 +1,148 @@ +.. _qtut_forms: + +==================================== +18: Forms and Validation With Deform +==================================== + +Schema-driven, autogenerated forms with validation. + +Background +========== + +Modern web applications deal extensively with forms. Developers, +though, have a wide range of philosophies about how frameworks should +help them with their forms. As such, Pyramid doesn't directly bundle +one particular form library. Instead, there are a variety of form +libraries that are easy to use in Pyramid. + +:ref:`Deform <deform:overview>` +is one such library. In this step, we introduce Deform for our +forms and validation. This also gives us the +:ref:`Colander <colander:overview>` for schemas and validation. + +Deform is getting a facelift, with styling from Twitter Bootstrap and +advanced widgets from popular JavaScript projects. The work began in +``deform_bootstrap`` and is being merged into an update to Deform. + +Objectives +========== + +- Make a schema using Colander, the companion to Deform + +- Create a form with Deform and change our views to handle validation + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes forms; cd forms + +#. Let's edit ``forms/setup.py`` to declare a dependency on Deform + (which then pulls in Colander as a dependency: + + .. literalinclude:: forms/setup.py + :linenos: + +#. We can now install our project in development mode: + + .. code-block:: bash + + $ $VENV/bin/python setup.py develop + +#. Register a static view in ``forms/tutorial/__init__.py`` for + Deform's CSS/JS etc. as well as our demo wikipage scenario's + views: + + .. literalinclude:: forms/tutorial/__init__.py + :linenos: + +#. Implement the new views, as well as the form schemas and some + dummy data, in ``forms/tutorial/views.py``: + + .. literalinclude:: forms/tutorial/views.py + :linenos: + +#. A template for the top of the "wiki" in + ``forms/tutorial/wiki_view.pt``: + + .. literalinclude:: forms/tutorial/wiki_view.pt + :language: html + :linenos: + +#. Another template for adding/editing in + ``forms/tutorial/wikipage_addedit.pt``: + + .. literalinclude:: forms/tutorial/wikipage_addedit.pt + :language: html + :linenos: + +#. Finally, a template at ``forms/tutorial/wikipage_view.pt`` + for viewing a wiki page: + + .. literalinclude:: forms/tutorial/wikipage_view.pt + :language: html + :linenos: + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in a browser. + + +Analysis +======== + +This step helps illustrate the utility of asset specifications for +static assets. We have an outside package called Deform with static +assets which need to be published. We don't have to know where on disk +it is located. We point at the package, then the path inside the package. + +We just need to include a call to ``add_static_view`` to make that +directory available at a URL. For Pyramid-specific pages, +Pyramid provides a facility (``config.include()``) which even makes +that unnecessary for consumers of a package. (Deform is not specific to +Pyramid.) + +Our forms have rich widgets which need the static CSS and JS just +mentioned. Deform has a :term:`resource registry` which allows widgets +to specify which JS and CSS are needed. Our ``wikipage_addedit.pt`` +template shows how we iterated over that data to generate markup that +includes the needed resources. + +Our add and edit views use a pattern called *self-posting forms*. +Meaning, the same URL is used to ``GET`` the form as is used to +``POST`` the form. The route, the view, and the template are the same +whether you are walking up to it the first time or you clicked a button. + +Inside the view we do ``if 'submit' in self.request.params:`` to see if +this form was a ``POST`` where the user clicked on a particular button +``<input name="submit">``. + +The form controller then follows a typical pattern: + +- If you are doing a GET, skip over and just return the form + +- If you are doing a POST, validate the form contents + +- If the form is invalid, bail out by re-rendering the form with the + supplied ``POST`` data + +- If the validation succeeded, perform some action and issue a + redirect via ``HTTPFound``. + +We are, in essence, writing our own form controller. Other +Pyramid-based systems, including ``pyramid_deform``, provide a +form-centric view class which automates much of this branching and +routing. + +Extra Credit +============ + +#. Give a try at a button that goes to a delete view for a + particular wiki page. diff --git a/docs/quick_tutorial/forms/development.ini b/docs/quick_tutorial/forms/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/forms/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/forms/setup.py b/docs/quick_tutorial/forms/setup.py new file mode 100644 index 000000000..361ade013 --- /dev/null +++ b/docs/quick_tutorial/forms/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'deform' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/forms/tutorial/__init__.py b/docs/quick_tutorial/forms/tutorial/__init__.py new file mode 100644 index 000000000..dff7457cf --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('wiki_view', '/') + config.add_route('wikipage_add', '/add') + config.add_route('wikipage_view', '/{uid}') + config.add_route('wikipage_edit', '/{uid}/edit') + config.add_static_view('deform_static', 'deform:static/') + config.scan('.views') + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/forms/tutorial/tests.py b/docs/quick_tutorial/forms/tutorial/tests.py new file mode 100644 index 000000000..5a2c40904 --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/tests.py @@ -0,0 +1,36 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import WikiViews + + request = testing.DummyRequest() + inst = WikiViews(request) + response = inst.wiki_view() + self.assertEqual(len(response['pages']), 3) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def tearDown(self): + testing.tearDown() + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<title>Wiki: View</title>', res.body) diff --git a/docs/quick_tutorial/forms/tutorial/views.py b/docs/quick_tutorial/forms/tutorial/views.py new file mode 100644 index 000000000..004d2aba9 --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/views.py @@ -0,0 +1,96 @@ +import colander +import deform.widget + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +pages = { + '100': dict(uid='100', title='Page 100', body='<em>100</em>'), + '101': dict(uid='101', title='Page 101', body='<em>101</em>'), + '102': dict(uid='102', title='Page 102', body='<em>102</em>') +} + +class WikiPage(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + body = colander.SchemaNode( + colander.String(), + widget=deform.widget.RichTextWidget() + ) + + +class WikiViews(object): + def __init__(self, request): + self.request = request + + @property + def wiki_form(self): + schema = WikiPage() + return deform.Form(schema, buttons=('submit',)) + + @property + def reqts(self): + return self.wiki_form.get_widget_resources() + + @view_config(route_name='wiki_view', renderer='wiki_view.pt') + def wiki_view(self): + return dict(pages=pages.values()) + + @view_config(route_name='wikipage_add', + renderer='wikipage_addedit.pt') + def wikipage_add(self): + form = self.wiki_form.render() + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = self.wiki_form.validate(controls) + except deform.ValidationFailure as e: + # Form is NOT valid + return dict(form=e.render()) + + # Form is valid, make a new identifier and add to list + last_uid = int(sorted(pages.keys())[-1]) + new_uid = str(last_uid + 1) + pages[new_uid] = dict( + uid=new_uid, title=appstruct['title'], + body=appstruct['body'] + ) + + # Now visit new page + url = self.request.route_url('wikipage_view', uid=new_uid) + return HTTPFound(url) + + return dict(form=form) + + @view_config(route_name='wikipage_view', renderer='wikipage_view.pt') + def wikipage_view(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + return dict(page=page) + + @view_config(route_name='wikipage_edit', + renderer='wikipage_addedit.pt') + def wikipage_edit(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + + wiki_form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = wiki_form.validate(controls) + except deform.ValidationFailure as e: + return dict(page=page, form=e.render()) + + # Change the content and redirect to the view + page['title'] = appstruct['title'] + page['body'] = appstruct['body'] + + url = self.request.route_url('wikipage_view', + uid=page['uid']) + return HTTPFound(url) + + form = wiki_form.render(page) + + return dict(page=page, form=form)
\ No newline at end of file diff --git a/docs/quick_tutorial/forms/tutorial/wiki_view.pt b/docs/quick_tutorial/forms/tutorial/wiki_view.pt new file mode 100644 index 000000000..9e3afe495 --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/wiki_view.pt @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Wiki: View</title> +</head> +<body> +<h1>Wiki</h1> + +<a href="${request.route_url('wikipage_add')}">Add + WikiPage</a> +<ul> + <li tal:repeat="page pages"> + <a href="${request.route_url('wikipage_view', uid=page.uid)}"> + ${page.title} + </a> + </li> +</ul> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt new file mode 100644 index 000000000..d1fea0d7f --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/wikipage_addedit.pt @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: Add/Edit</title> + <tal:block tal:repeat="reqt view.reqts['css']"> + <link rel="stylesheet" type="text/css" + href="${request.static_url('deform:static/' + reqt)}"/> + </tal:block> + <tal:block tal:repeat="reqt view.reqts['js']"> + <script src="${request.static_url('deform:static/' + reqt)}" + type="text/javascript"></script> + </tal:block> +</head> +<body> +<h1>Wiki</h1> + +<p>${structure: form}</p> +<script type="text/javascript"> + deform.load() +</script> +</body> +</html> diff --git a/docs/quick_tutorial/forms/tutorial/wikipage_view.pt b/docs/quick_tutorial/forms/tutorial/wikipage_view.pt new file mode 100644 index 000000000..cb9ff526e --- /dev/null +++ b/docs/quick_tutorial/forms/tutorial/wikipage_view.pt @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: View</title> +</head> +<body> +<a href="${request.route_url('wiki_view')}"> + Up +</a> | +<a href="${request.route_url('wikipage_edit', uid=page.uid)}"> + Edit +</a> + +<h1>${page.title}</h1> +<p>${structure: page.body}</p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing.rst b/docs/quick_tutorial/functional_testing.rst new file mode 100644 index 000000000..205ddf5cb --- /dev/null +++ b/docs/quick_tutorial/functional_testing.rst @@ -0,0 +1,70 @@ +.. _qtut_functional_testing: + +=================================== +06: Functional Testing with WebTest +=================================== + +Write end-to-end full-stack testing using ``webtest``. + +Background +========== + +Unit tests are a common and popular approach to test-driven development +(TDD.) In web applications, though, the templating and entire apparatus +of a web site are important parts of the delivered quality. We'd like a +way to test these. + +WebTest is a Python package that does functional testing. With WebTest +you can write tests which simulate a full HTTP request against a WSGI +application, then test the information in the response. For speed +purposes, WebTest skips the setup/teardown of an actual HTTP server, +providing tests that run fast enough to be part of TDD. + +Objectives +========== + +- Write a test which checks the contents of the returned HTML + +Steps +===== + +#. First we copy the results of the previous step, as well as install + the ``webtest`` package: + + .. code-block:: bash + + $ cd ..; cp -r unit_testing functional_testing; cd functional_testing + $ $VENV/bin/python setup.py develop + $ $VENV/bin/easy_install webtest + +#. Let's extend ``unit_testing/tutorial/tests.py`` to include a + functional test: + + .. literalinclude:: functional_testing/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + + $ $VENV/bin/nosetests tutorial + . + ---------------------------------------------------------------------- + Ran 2 tests in 0.141s + + OK + +Analysis +======== + +We now have the end-to-end testing we were looking for. WebTest lets us +simply extend our existing ``nose``-based test approach with functional +tests that are reported in the same output. These new tests not only +cover our templating, but they didn't dramatically increase the +execution time of our tests. + +Extra Credit +============ + +#. Why do our functional tests use ``b''``?
\ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing/development.ini b/docs/quick_tutorial/functional_testing/development.ini new file mode 100644 index 000000000..470d92c57 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/development.ini @@ -0,0 +1,40 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/functional_testing/setup.py b/docs/quick_tutorial/functional_testing/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing/tutorial/__init__.py b/docs/quick_tutorial/functional_testing/tutorial/__init__.py new file mode 100644 index 000000000..2b4e84f30 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('<body><h1>Hello World!</h1></body>') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/functional_testing/tutorial/tests.py b/docs/quick_tutorial/functional_testing/tutorial/tests.py new file mode 100644 index 000000000..4248acbe7 --- /dev/null +++ b/docs/quick_tutorial/functional_testing/tutorial/tests.py @@ -0,0 +1,31 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_hello_world(self): + from tutorial import hello_world + + request = testing.DummyRequest() + response = hello_world(request) + self.assertEqual(response.status_code, 200) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_hello_world(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hello World!</h1>', res.body) diff --git a/docs/quick_tutorial/hello_world.rst b/docs/quick_tutorial/hello_world.rst new file mode 100644 index 000000000..c7a8eaf5e --- /dev/null +++ b/docs/quick_tutorial/hello_world.rst @@ -0,0 +1,114 @@ +.. _qtut_hello_world: + +================================ +01: Single-File Web Applications +================================ + +What's the simplest way to get started in Pyramid? A single-file module. +No Python packages, no ``setup.py``, no other machinery. + +Background +========== + +Microframeworks are all the rage these days. "Microframework" is a +marketing term, not a technical one. They have a low mental overhead: +they do so little, the only things you have to worry about are *your +things*. + +Pyramid is special because it can act as a single-file module +microframework. You can have a single Python file that can be executed +directly by Python. But Pyramid also provides facilities to scale to +the largest of applications. + +Python has a standard called :term:`WSGI` that defines how +Python web applications plug into standard servers, getting passed +incoming requests and returning responses. Most modern Python web +frameworks obey an "MVC" (model-view-controller) application pattern, +where the data in the model has a view that mediates interaction with +outside systems. + +In this step we'll see a brief glimpse of WSGI servers, WSGI +applications, requests, responses, and views. + +Objectives +========== + +- Get a running Pyramid web application, as simply as possible + +- Use that as a well-understood base for adding each unit of complexity + +- Initial exposure to WSGI apps, requests, views, and responses + +Steps +===== + +#. Make sure you have followed the steps in :doc:`requirements`. + +#. Starting from your workspace directory + (``~/projects/quick_tutorial``), create a directory for this step: + + .. code-block:: bash + + $ mkdir hello_world; cd hello_world + +#. Copy the following into ``hello_world/app.py``: + + .. literalinclude:: hello_world/app.py + :linenos: + +#. Run the application: + + .. code-block:: bash + + $ $VENV/bin/python app.py + +#. Open http://localhost:6543/ in your browser. + +Analysis +======== + +New to Python web programming? If so, some lines in module merit +explanation: + +#. *Line 11*. The ``if __name__ == '__main__':`` is Python's way of + saying "Start here when running from the command line". + +#. *Lines 12-14*. Use Pyramid's :term:`configurator` to connect + :term:`view` code to a particular URL + :term:`route`. + +#. *Lines 6-7*. Implement the view code that generates the + :term:`response`. + +#. *Lines 15-17*. Publish a :term:`WSGI` app using an HTTP + server. + +As shown in this example, the :term:`configurator` plays a +central role in Pyramid development. Building an application from +loosely-coupled parts via :ref:`configuration_narr` is a +central idea in Pyramid, one that we will revisit regularly in this +*Quick Tour*. + +Extra Credit +============ + +#. Why do we do this: + + .. code-block:: python + + print ('Starting up server on http://localhost:6547') + + ...instead of: + + .. code-block:: python + + print 'Starting up server on http://localhost:6547' + +#. What happens if you return a string of HTML? A sequence of integers? + +#. Put something invalid, such as ``print xyz``, in the view function. + Kill your ``python app.py`` with ``cntrl-c`` and restart, + then reload your browser. See the exception in the console? + +#. The ``GI`` in ``WSGI`` stands for "Gateway Interface". What web + standard is this modelled after?
\ No newline at end of file diff --git a/docs/quick_tutorial/hello_world/app.py b/docs/quick_tutorial/hello_world/app.py new file mode 100644 index 000000000..210075023 --- /dev/null +++ b/docs/quick_tutorial/hello_world/app.py @@ -0,0 +1,17 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + print ('Incoming request') + return Response('<body><h1>Hello World!</h1></body>') + + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever()
\ No newline at end of file diff --git a/docs/quick_tutorial/index.rst b/docs/quick_tutorial/index.rst new file mode 100644 index 000000000..9373fe38a --- /dev/null +++ b/docs/quick_tutorial/index.rst @@ -0,0 +1,50 @@ +.. _quick_tutorial: + +========================== +Quick Tutorial for Pyramid +========================== + +Pyramid is a web framework for Python 2 and 3. This tutorial gives a +Python 3/2-compatible, high-level tour of the major features. + +This hands-on tutorial covers "a little about a lot": practical +introductions to the most common facilities. Fun, fast-paced, and most +certainly not aimed at experts of the Pyramid web framework. + +Contents +======== + +.. toctree:: + :maxdepth: 1 + + requirements + tutorial_approach + scaffolds + hello_world + package + ini + debugtoolbar + unit_testing + functional_testing + views + templating + view_classes + request_response + routing + jinja2 + static_assets + json + more_view_classes + logging + sessions + forms + databases + authentication + authorization + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/quick_tutorial/ini.rst b/docs/quick_tutorial/ini.rst new file mode 100644 index 000000000..618b8e5e8 --- /dev/null +++ b/docs/quick_tutorial/ini.rst @@ -0,0 +1,146 @@ +.. _qtut_ini: + +================================================= +03: Application Configuration with ``.ini`` Files +================================================= + +Use Pyramid's ``pserve`` command with a ``.ini`` configuration file for +simpler, better application running. + +Background +========== + +Pyramid has a first-class concept of +:ref:`configuration <configuration_narr>` distinct from code. +This approach is optional, but its presence makes it distinct from +other Python web frameworks. It taps into Python's ``setuptools`` +library, which establishes conventions for how Python projects can be +installed and provide "entry points". Pyramid uses an entry point to +let a Pyramid application it where to find the WSGI app. + +Objectives +========== + +- Modify our ``setup.py`` to have an entry point telling Pyramid the + location of the WSGI app + +- Create an application driven by a ``.ini`` file + +- Startup the application with Pyramid's ``pserve`` command + +- Move code into the package's ``__init__.py`` + +Steps +===== + +#. First we copy the results of the previous step: + + .. code-block:: bash + + $ cd ..; cp -r package ini; cd ini + +#. Our ``ini/setup.py`` needs a setuptools "entry point" in the + ``setup()`` function: + + .. literalinclude:: ini/setup.py + :linenos: + +#. We can now install our project, thus generating (or re-generating) an + "egg" at ``ini/tutorial.egg-info``: + + .. code-block:: bash + + $ $VENV/bin/python setup.py develop + +#. Let's make a file ``ini/development.ini`` for our configuration: + + .. literalinclude:: ini/development.ini + :language: ini + :linenos: + +#. We can refactor our startup code from the previous step's ``app.py`` + into ``ini/tutorial/__init__.py``: + + .. literalinclude:: ini/tutorial/__init__.py + :linenos: + +#. Now that ``ini/tutorial/app.py`` isn't used, let's remove it: + + .. code-block:: bash + + $ rm tutorial/app.py + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/. + +Analysis +======== + +Our ``development.ini`` file is read by ``pserve`` and serves to +bootstrap our application. Processing then proceeds as described in +the Pyramid chapter on +:ref:`application startup <startup_chapter>`: + +- ``pserve`` looks for ``[app:main]`` and finds ``use = egg:tutorial`` + +- The projects's ``setup.py`` has defined an "entry point" (lines 9-10) + for the project "main" entry point of ``tutorial:main`` + +- The ``tutorial`` package's ``__init__`` has a ``main`` function + +- This function is invoked, with the values from certain ``.ini`` + sections passed in + +The ``.ini`` file is also used for two other functions: + +- *Choice of WSGI server*. ``[server:main]`` wires up the choice of WSGI + *server* for your WSGI *application*. In this case, we are using + ``wsgiref`` bundled in the Python library. + +- *Python logging*. Pyramid uses Python standard logging, which needs a + number of configuration values. The ``.ini`` serves this function. + This provides the console log output that you see on startup and each + request. + +- *Port number*. ``port = 6543`` tells ``wsgiref`` to listen on port + 6543. + +We moved our startup code from ``app.py`` to the package's +``tutorial/__init__.py``. This isn't necessary, +but it is a common style in Pyramid to take the WSGI app bootstrapping +out of your module's code and put it in the package's ``__init__.py``. + +The ``pserve`` application runner has a number of command-line arguments +and options. We are using ``--reload`` which tells ``pserve`` to watch +the filesystem for changes to relevant code (Python files, the INI file, +etc.) and, when something changes, restart the application. Very handy +during development. + +Extra Credit +============ + +#. If you don't like configuration and/or ``.ini`` files, + could you do this yourself in Python code? + +#. Can we have multiple ``.ini`` configuration files for a project? Why + might you want to do that? + +#. The entry point in ``setup.py`` didn't mention ``__init__.py`` when + it the ``main`` function. Why not? + +.. seealso:: + :ref:`project_narr`, + :ref:`scaffolding_chapter`, + :ref:`what_is_this_pserve_thing`, + :ref:`environment_chapter`, + :ref:`paste_chapter` + +Extra Credit +============ + +#. What is the purpose of ``**settings``? What does the ``**`` signify? diff --git a/docs/quick_tutorial/ini/development.ini b/docs/quick_tutorial/ini/development.ini new file mode 100644 index 000000000..ca7d9bf81 --- /dev/null +++ b/docs/quick_tutorial/ini/development.ini @@ -0,0 +1,38 @@ +[app:main] +use = egg:tutorial + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/ini/setup.py b/docs/quick_tutorial/ini/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/ini/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/ini/tutorial/__init__.py b/docs/quick_tutorial/ini/tutorial/__init__.py new file mode 100644 index 000000000..2b4e84f30 --- /dev/null +++ b/docs/quick_tutorial/ini/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('<body><h1>Hello World!</h1></body>') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/jinja2.rst b/docs/quick_tutorial/jinja2.rst new file mode 100644 index 000000000..44d9f635b --- /dev/null +++ b/docs/quick_tutorial/jinja2.rst @@ -0,0 +1,97 @@ +.. _qtut_jinja2: + +============================== +12: Templating With ``jinja2`` +============================== + +We just said Pyramid doesn't prefer one templating language over +another. Time to prove it. Jinja2 is a popular templating system, +used in Flask and modelled after Django's templates. Let's add +``pyramid_jinja2``, a Pyramid :term:`add-on` which enables Jinja2 as a +:term:`renderer` in our Pyramid applications. + +Objectives +========== + +- Show Pyramid's support for different templating systems + +- Learn about installing Pyramid add-ons + +Steps +===== + +#. In this step let's start by installing the ``pyramid_jinja2`` + add-on, the copying the ``view_class`` step's directory: + + .. code-block:: bash + + $ cd ..; cp -r view_classes jinja2; cd jinja2 + $ $VENV/bin/python setup.py develop + $ $VENV/bin/easy_install pyramid_jinja2 + +#. We need to include ``pyramid_jinja2`` in + ``jinja2/tutorial/__init__.py``: + + .. literalinclude:: jinja2/tutorial/__init__.py + :linenos: + +#. Our ``jinja2/tutorial/views.py`` simply changes its ``renderer``: + + .. literalinclude:: jinja2/tutorial/views.py + :linenos: + +#. Add ``jinja2/tutorial/home.jinja2`` as a template: + + .. literalinclude:: jinja2/tutorial/home.jinja2 + :language: html + +#. Get the ``pyramid.includes`` into the functional test setup in + ``jinja2/tutorial/tests.py``: + + .. literalinclude:: jinja2/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + $ $VENV/bin/nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in your browser. + +Analysis +======== + +Getting a Pyramid add-on into Pyramid is simple. First you use normal +Python package installation tools to install the add-on package into +your Python. You then tell Pyramid's configurator to run the setup code +in the add-on. In this case the setup code told Pyramid to make a new +"renderer" available that looked for ``.jinja2`` file extensions. + +Our view code stayed largely the same. We simply changed the file +extension on the renderer. For the template, the syntax for Chameleon +and Jinja2's basic variable insertion is very similar. + +Our functional tests don't have ``development.ini`` so they needed the +``pyramid.includes`` to be setup in the test setup. + +Extra Credit +============ + +#. Our project now depends on ``pyramid_jinja2``. We installed that + dependency manually. What is another way we could have made the + association? + +#. We used ``development.ini`` to get the :term:`configurator` to + load ``pyramid_jinja2``'s configuration. What is another way could + include it into the config? + +.. seealso:: `Jinja2 homepage <http://jinja.pocoo.org/>`_, + and + :ref:`pyramid_jinja2 Overview <jinja2:overview>` diff --git a/docs/quick_tutorial/jinja2/development.ini b/docs/quick_tutorial/jinja2/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/jinja2/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/jinja2/setup.py b/docs/quick_tutorial/jinja2/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/jinja2/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/jinja2/tutorial/__init__.py b/docs/quick_tutorial/jinja2/tutorial/__init__.py new file mode 100644 index 000000000..1f6783c06 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/jinja2/tutorial/home.jinja2 b/docs/quick_tutorial/jinja2/tutorial/home.jinja2 new file mode 100644 index 000000000..975323169 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/home.jinja2 @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: {{ name }}</title> +</head> +<body> +<h1>Hi {{ name }}</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/jinja2/tutorial/tests.py b/docs/quick_tutorial/jinja2/tutorial/tests.py new file mode 100644 index 000000000..0b22946f3 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/tests.py @@ -0,0 +1,50 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + + settings = { + 'pyramid.includes': [ + 'pyramid_jinja2' + ] + } + app = main({}, **settings) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/jinja2/tutorial/views.py b/docs/quick_tutorial/jinja2/tutorial/views.py new file mode 100644 index 000000000..fa9ec5121 --- /dev/null +++ b/docs/quick_tutorial/jinja2/tutorial/views.py @@ -0,0 +1,18 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.jinja2') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/json.rst b/docs/quick_tutorial/json.rst new file mode 100644 index 000000000..ece8a61c0 --- /dev/null +++ b/docs/quick_tutorial/json.rst @@ -0,0 +1,103 @@ +.. _qtut_json: + +======================================== +14: Ajax Development With JSON Renderers +======================================== + +Modern web apps are more than rendered HTML. Dynamic pages now use +JavaScript to update the UI in the browser by requesting server data as +JSON. Pyramid supports this with a *JSON renderer*. + +Background +========== + +As we saw in :doc:`templating`, view declarations can specify a +renderer. Output from the view is then run through the renderer, +which generates and returns the ``Response``. We first used a Chameleon +renderer, then a Jinja2 renderer. + +Renderers aren't limited, however, to templates that generate HTML. +Pyramid supplies a JSON renderer which takes Python data, +serializes it to JSON, and performs some other functions such as +setting the content type. In fact, you can write your own renderer (or +extend a built-in renderer) containing custom logic for your unique +application. + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes json; cd json + $ $VENV/bin/python setup.py develop + +#. We add a new route for ``hello_json`` in + ``json/tutorial/__init__.py``: + + .. literalinclude:: json/tutorial/__init__.py + :linenos: + +#. Rather than implement a new view, we will "stack" another decorator + on the ``hello`` view: + + .. literalinclude:: json/tutorial/views.py + :linenos: + +#. We need a new functional test at the end of + ``json/tutorial/tests.py``: + + .. literalinclude:: json/tutorial/tests.py + :linenos: + +#. Run the tests: + + .. code-block:: bash + + $ $VENV/bin/nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/howdy.json in your browser and you + will see the resulting JSON response. + +Analysis +======== + +Earlier we changed our view functions and methods to return Python +data. This change to a data-oriented view layer made test writing +easier, decoupling the templating from the view logic. + +Since Pyramid has a JSON renderer as well as the templating renderers, +it is an easy step to return JSON. In this case we kept the exact same +view and arranged to return a JSON encoding of the view data. We did +this by: + +- Adding a route to map ``/howdy.json`` to a route name + +- Providing a ``@view_config`` that associated that route name with an + existing view + +- *overriding* the view defaults in the view config that mentions the + ``hello_json`` route, so that when the route is matched, we use the JSON + renderer rather than the ``home.pt`` template renderer that would otherwise + be used. + +In fact, for pure Ajax-style web applications, we could re-use the existing +route by using Pyramid's view predicates to match on the +``Accepts:`` header sent by modern Ajax implementation. + +Pyramid's JSON renderer uses the base Python JSON encoder, +thus inheriting its strengths and weaknesses. For example, +Python can't natively JSON encode DateTime objects. There are a number +of solutions for this in Pyramid, including extending the JSON renderer +with a custom renderer. + +.. seealso:: :ref:`views_which_use_a_renderer`, + :ref:`json_renderer`, and + :ref:`adding_and_overriding_renderers` diff --git a/docs/quick_tutorial/json/development.ini b/docs/quick_tutorial/json/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/json/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/json/setup.py b/docs/quick_tutorial/json/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/json/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/json/tutorial/__init__.py b/docs/quick_tutorial/json/tutorial/__init__.py new file mode 100644 index 000000000..6652544c3 --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/__init__.py @@ -0,0 +1,11 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_route('hello_json', 'howdy.json') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/json/tutorial/home.pt b/docs/quick_tutorial/json/tutorial/home.pt new file mode 100644 index 000000000..a0cc08e7a --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/home.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/json/tutorial/tests.py b/docs/quick_tutorial/json/tutorial/tests.py new file mode 100644 index 000000000..c3cdacbdb --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/tests.py @@ -0,0 +1,50 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) + + def test_hello_json(self): + res = self.testapp.get('/howdy.json', status=200) + self.assertIn(b'{"name": "Hello View"}', res.body) + self.assertEqual(res.content_type, 'application/json') + diff --git a/docs/quick_tutorial/json/tutorial/views.py b/docs/quick_tutorial/json/tutorial/views.py new file mode 100644 index 000000000..f15e55d1b --- /dev/null +++ b/docs/quick_tutorial/json/tutorial/views.py @@ -0,0 +1,19 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + @view_config(route_name='hello_json', renderer='json') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/logging.rst b/docs/quick_tutorial/logging.rst new file mode 100644 index 000000000..0167e5249 --- /dev/null +++ b/docs/quick_tutorial/logging.rst @@ -0,0 +1,79 @@ +.. _qtut_logging: + +============================================ +16: Collecting Application Info With Logging +============================================ + +Capture debugging and error output from your web applications using +standard Python logging. + +Background +========== + +It's important to know what is going on inside our web application. +In development we might need to collect some output. In production, +we might need to detect problems when other people use the site. We +need *logging*. + +Fortunately Pyramid uses the normal Python approach to logging. The +scaffold generated, in your ``development.ini``, a number of lines that +configure the logging for you to some reasonable defaults. You then see +messages sent by Pyramid (for example, when a new request comes in.) + +Objectives +========== + +- Inspect the configuration setup used for logging + +- Add logging statements to your view code + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes logging; cd logging + $ $VENV/bin/python setup.py develop + +#. Extend ``logging/tutorial/views.py`` to log a message: + + .. literalinclude:: logging/tutorial/views.py + :linenos: + +#. Make sure the tests still pass: + + .. code-block:: bash + + $ $VENV/bin/nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy + in your browser. Note, both in the console and in the debug + toolbar, the message that you logged. + +Analysis +======== + +Our ``development.ini`` configuration file wires up Python standard +logging for our Pyramid application: + +.. literalinclude:: logging/development.ini + :language: ini + +In this, our ``tutorial`` Python package is setup as a logger +and configured to log messages at a ``DEBUG`` or higher level. When you +visit http://localhost:6543 your console will now show:: + + 2013-08-09 10:42:42,968 DEBUG [tutorial.views][MainThread] In home view + +Also, if you have configured your Pyramid application to use the +``pyramid_debugtoolbar``, logging statements appear in one of its menus. + +.. seealso:: See Also: :ref:`logging_chapter` diff --git a/docs/quick_tutorial/logging/development.ini b/docs/quick_tutorial/logging/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/logging/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/logging/setup.py b/docs/quick_tutorial/logging/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/logging/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/logging/tutorial/__init__.py b/docs/quick_tutorial/logging/tutorial/__init__.py new file mode 100644 index 000000000..c3e1c9eef --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/logging/tutorial/home.pt b/docs/quick_tutorial/logging/tutorial/home.pt new file mode 100644 index 000000000..a0cc08e7a --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/home.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/logging/tutorial/tests.py b/docs/quick_tutorial/logging/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/logging/tutorial/views.py b/docs/quick_tutorial/logging/tutorial/views.py new file mode 100644 index 000000000..63d95f405 --- /dev/null +++ b/docs/quick_tutorial/logging/tutorial/views.py @@ -0,0 +1,23 @@ +import logging +log = logging.getLogger(__name__) + +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + log.debug('In home view') + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + log.debug('In hello view') + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/more_view_classes.rst b/docs/quick_tutorial/more_view_classes.rst new file mode 100644 index 000000000..21b353b7c --- /dev/null +++ b/docs/quick_tutorial/more_view_classes.rst @@ -0,0 +1,182 @@ +.. _qtut_more_view_classes: + +========================== +15: More With View Classes +========================== + +Group views into a class, sharing configuration, state, and logic. + +Background +========== + +As part of its mission to help build more ambitious web applications, +Pyramid provides many more features for views and view classes. + +The Pyramid documentation discusses views as a Python "callable". This +callable can be a function, an object with an ``__call__``, +or a Python class. In this last case, methods on the class can be +decorated with ``@view_config`` to register the class methods with the +:term:`configurator` as a view. + +So far our views have been simple, free-standing functions. Many times +your views are related: different ways to look at or work on the same +data or a REST API that handles multiple operations. Grouping these +together as a +:ref:`view class <class_as_view>` makes sense: + +- Group views + +- Centralize some repetitive defaults + +- Share some state and helpers + +Pyramid views have +:ref:`view predicates <view_configuration_parameters>` that +help determine which view is matched to a request. These predicates +provide many axes of flexibility. + +The following shows a simple example with four operations operations: +view a home page which leads to a form, save a change, +and press the delete button. + +Objectives +========== + +- Group related views into a view class + +- Centralize configuration with class-level ``@view_defaults`` + +- Dispatch one route/URL to multiple views based on request data + +- Share stated and logic between views and templates via the view class + +Steps +===== + +#. First we copy the results of the previous step: + + .. code-block:: bash + + $ cd ..; cp -r templating more_view_classes; cd more_view_classes + $ $VENV/bin/python setup.py develop + +#. Our route in ``more_view_classes/tutorial/__init__.py`` needs some + replacement patterns: + + .. literalinclude:: more_view_classes/tutorial/__init__.py + :linenos: + +#. Our ``more_view_classes/tutorial/views.py`` now has a view class with + several views: + + .. literalinclude:: more_view_classes/tutorial/views.py + :linenos: + +#. Our primary view needs a template at + ``more_view_classes/tutorial/home.pt``: + + .. literalinclude:: more_view_classes/tutorial/home.pt + :language: html + +#. Ditto for our other view from the previous section at + ``more_view_classes/tutorial/hello.pt``: + + .. literalinclude:: more_view_classes/tutorial/hello.pt + :language: html + +#. We have an edit view that also needs a template at + ``more_view_classes/tutorial/edit.pt``: + + .. literalinclude:: more_view_classes/tutorial/edit.pt + :language: html + +#. And finally the delete view's template at + ``more_view_classes/tutorial/delete.pt``: + + .. literalinclude:: more_view_classes/tutorial/delete.pt + :language: html + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/howdy/jane/doe in your browser. Click + the ``Save`` and ``Delete`` buttons and watch the output in the + console window. + +Analysis +======== + +As you can see, the four views are logically grouped together. +Specifically: + +- We have a ``home`` view available at http://localhost:6543/ with + a clickable link to the ``hello`` view. + +- The second view is returned when you go to ``/howdy/jane/doe``. This + URL is + mapped to the ``hello`` route that we centrally set using the optional + ``@view_defaults``. + +- The third view is returned when the form is submitted with a ``POST`` + method. This rule is specified in the ``@view_config`` for that view. + +- The fourth view is returned when clicking on a button such + as ``<input type="submit" name="form.delete" value="Delete"/>``. + +In this step we show using the following information as criteria to +decide which view to use: + +- Method of the HTTP request (``GET``, ``POST``, etc.) + +- Parameter information in the request (submitted form field names) + +We also centralize part of the view configuration to the class level +with ``@view_defaults``, then in one view, override that default just +for that one view. Finally, we put this commonality between views to +work in the view class by sharing: + +- State assigned in ``TutorialViews.__init__`` + +- A computed value + +These are then available both in the view methods but also in the +templates (e.g. ``${view.view_name}`` and ``${view.full_name}``. + +As a note, we made a switch in our templates on how we generate URLs. +We previously hardcode the URLs, such as:: + + <a href="/howdy/jane/doe">Howdy</a> + +In ``home.pt`` we switched to:: + + <a href="${request.route_url('hello', first='jane', + last='doe')}">form</a> + +Pyramid has rich facilities to help generate URLs in a flexible, +non-error-prone fashion. + +Extra Credit +============ + +#. Why could our template do ``${view.full_name}`` and not have to do + ``${view.full_name()}``? + +#. The ``edit`` and ``delete`` views are both submitted to with + ``POST``. Why does the ``edit`` view configuration not catch the + ``POST`` used by ``delete``? + +#. We used Python ``@property`` on ``full_name``. If we reference this + many times in a template or view code, it would re-compute this + every time. Does Pyramid provide something that will cache the initial + computation on a property? + +#. Can you associate more than one route with the same view? + +#. There is also a ``request.route_path`` API. How does this differ from + ``request.route_url``? + +.. seealso:: :ref:`class_as_view`, `Weird Stuff You Can Do With + URL Dispatch <http://www.plope.com/weird_pyramid_urldispatch>`_ diff --git a/docs/quick_tutorial/more_view_classes/development.ini b/docs/quick_tutorial/more_view_classes/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/more_view_classes/setup.py b/docs/quick_tutorial/more_view_classes/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/__init__.py b/docs/quick_tutorial/more_view_classes/tutorial/__init__.py new file mode 100644 index 000000000..9c1bcec06 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy/{first}/{last}') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/delete.pt b/docs/quick_tutorial/more_view_classes/tutorial/delete.pt new file mode 100644 index 000000000..67cc8bf09 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/delete.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${page_title}</title> +</head> +<body> +<h1>${view.view_name} - ${page_title}</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/edit.pt b/docs/quick_tutorial/more_view_classes/tutorial/edit.pt new file mode 100644 index 000000000..1bd204065 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/edit.pt @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${view.view_name} - ${page_title}</title> +</head> +<body> +<h1>${view.view_name} - ${page_title}</h1> +<p>You submitted <code>${new_name}</code></p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/hello.pt b/docs/quick_tutorial/more_view_classes/tutorial/hello.pt new file mode 100644 index 000000000..8a39aed09 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/hello.pt @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${view.view_name} - ${page_title}</title> +</head> +<body> +<h1>${view.view_name} - ${page_title}</h1> +<p>Welcome, ${view.full_name}</p> +<form method="POST" + action="${request.current_route_url()}"> + <input name="new_name"/> + <input type="submit" name="form.edit" value="Save"/> + <input type="submit" name="form.delete" value="Delete"/> +</form> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/home.pt b/docs/quick_tutorial/more_view_classes/tutorial/home.pt new file mode 100644 index 000000000..fa9016705 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/home.pt @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${view.view_name} - ${page_title}</title> +</head> +<body> +<h1>${view.view_name} - ${page_title}</h1> + +<p>Go to the <a href="${request.route_url('hello', first='jane', + last='doe')}">form</a>.</p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/more_view_classes/tutorial/tests.py b/docs/quick_tutorial/more_view_classes/tutorial/tests.py new file mode 100644 index 000000000..dca8d7f7b --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/tests.py @@ -0,0 +1,31 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['page_title']) + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'TutorialViews - Home View', res.body) diff --git a/docs/quick_tutorial/more_view_classes/tutorial/views.py b/docs/quick_tutorial/more_view_classes/tutorial/views.py new file mode 100644 index 000000000..fdba04ba8 --- /dev/null +++ b/docs/quick_tutorial/more_view_classes/tutorial/views.py @@ -0,0 +1,39 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(route_name='hello') +class TutorialViews: + def __init__(self, request): + self.request = request + self.view_name = 'TutorialViews' + + @property + def full_name(self): + first = self.request.matchdict['first'] + last = self.request.matchdict['last'] + return first + ' ' + last + + @view_config(route_name='home', renderer='home.pt') + def home(self): + return {'page_title': 'Home View'} + + + # Retrieving /howdy/first/last the first time + @view_config(renderer='hello.pt') + def hello(self): + return {'page_title': 'Hello View'} + + # Posting to /home via the "Edit" submit button + @view_config(request_method='POST', renderer='edit.pt') + def edit(self): + new_name = self.request.params['new_name'] + return {'page_title': 'Edit View', 'new_name': new_name} + + # Posting to /home via the "Delete" submit button + @view_config(request_param='form.delete', renderer='delete.pt') + def delete(self): + print ('Deleted') + return {'page_title': 'Delete View'} diff --git a/docs/quick_tutorial/package.rst b/docs/quick_tutorial/package.rst new file mode 100644 index 000000000..90d022b29 --- /dev/null +++ b/docs/quick_tutorial/package.rst @@ -0,0 +1,112 @@ +============================================ +02: Python Packages for Pyramid Applications +============================================ + +Most modern Python development is done using Python packages, an approach +Pyramid puts to good use. In this step we re-do "Hello World" as a +minimum Python package inside a minimum Python project. + +Background +========== + +Python developers can organize a collection of modules and files into a +namespaced unit called a :ref:`package <python:tut-packages>`. If a +directory is on ``sys.path`` and has a special file named +``__init__.py``, it is treated as a Python package. + +Packages can be bundled up, made available for installation, +and installed through a (muddled, but improving) toolchain oriented +around a ``setup.py`` file for a +`setuptools project <http://pythonhosted.org/setuptools/setuptools.html>`_. +Explaining it all in this +tutorial will induce madness. For this tutorial, this is all you need to +know: + +- We will have a directory for each tutorial step as a + setuptools *project* + +- This project will contain a ``setup.py`` which injects the features + of the setuptool's project machinery into the directory + +- In this project we will make a ``tutorial`` subdirectory into a Python + *package* using an ``__init__.py`` Python module file + +- We will run ``python setup.py develop`` to install our project in + development mode + +In summary: + +- You'll do your development in a Python *package* + +- That package will be part of a setuptools *project* + +Objectives +========== + +- Make a Python "package" directory with an ``__init__.py`` + +- Get a minimum Python "project" in place by making a ``setup.py`` + +- Install our ``tutorial`` project in development mode + +Steps +===== + +#. Make an area for this tutorial step: + + .. code-block:: bash + + $ cd ..; mkdir package; cd package + +#. In ``package/setup.py``, enter the following: + + .. literalinclude:: package/setup.py + +#. Make the new project installed for development then make a directory + for the actual code: + + .. code-block:: bash + + $ $VENV/bin/python setup.py develop + $ mkdir tutorial + +#. Enter the following into ``package/tutorial/__init__.py``: + + .. literalinclude:: package/tutorial/__init__.py + +#. Enter the following into ``package/tutorial/app.py``: + + .. literalinclude:: package/tutorial/app.py + +#. Run the WSGI application with: + + .. code-block:: bash + + $ $VENV/bin/python tutorial/app.py + +#. Open http://localhost:6543/ in your browser. + +Analysis +======== + +Python packages give us an organized unit of project development. +Python projects, via ``setup.py``, gives us special features when +our package is installed (in this case, in local development mode.) + +In this step we have a Python package called ``tutorial``. We use the +same name in each step of the tutorial, to avoid unnecessary re-typing. + +Above this ``tutorial`` directory we have the files that handle the +packaging of this, well, package. At the moment, all we need is a +bare-bones ``ini/setup.py``. + +Everything else is the same about our application. We simply made a +Python package with a ``setup.py`` and installed it in development mode. + +Note that the way we're running the app (``python tutorial/app.py``) is a bit +of an odd duck. We would never do this unless we were writing a tutorial that +tries to capture how this stuff works a step at a time. It's generally a bad +idea to run a Python module inside a package directly as a script. + +.. seealso:: :ref:`Python Packages <python:tut-packages>`, + `setuptools Entry Points <http://pythonhosted.org/setuptools/pkg_resources.html#entry-points>`_ diff --git a/docs/quick_tutorial/package/setup.py b/docs/quick_tutorial/package/setup.py new file mode 100644 index 000000000..bcfcfa684 --- /dev/null +++ b/docs/quick_tutorial/package/setup.py @@ -0,0 +1,9 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/package/tutorial/__init__.py b/docs/quick_tutorial/package/tutorial/__init__.py new file mode 100644 index 000000000..d310fdde9 --- /dev/null +++ b/docs/quick_tutorial/package/tutorial/__init__.py @@ -0,0 +1 @@ +# package
\ No newline at end of file diff --git a/docs/quick_tutorial/package/tutorial/app.py b/docs/quick_tutorial/package/tutorial/app.py new file mode 100644 index 000000000..210075023 --- /dev/null +++ b/docs/quick_tutorial/package/tutorial/app.py @@ -0,0 +1,17 @@ +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + print ('Incoming request') + return Response('<body><h1>Hello World!</h1></body>') + + +if __name__ == '__main__': + config = Configurator() + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 6543, app) + server.serve_forever()
\ No newline at end of file diff --git a/docs/quick_tutorial/request_response.rst b/docs/quick_tutorial/request_response.rst new file mode 100644 index 000000000..504803804 --- /dev/null +++ b/docs/quick_tutorial/request_response.rst @@ -0,0 +1,103 @@ +.. _qtut_request_response: + +======================================= +10: Handling Web Requests and Responses +======================================= + +Web applications handle incoming requests and return outgoing responses. +Pyramid makes working with requests and responses convenient and +reliable. + +Objectives +========== + +- Learn the background on Pyramid's choices for requests and responses + +- Grab data out of the request + +- Change information in the response headers + +Background +========== + +Developing for the web means processing web requests. As this is a +critical part of a web application, web developers need a robust, +mature set of software for web requests and returning web +responses. + +Pyramid has always fit nicely into the existing world of Python web +development (virtual environments, packaging, scaffolding, +first to embrace Python 3, etc.) For request handling, Pyramid turned +to the well-regarded :term:`WebOb` Python library for request and +response handling. In our example +above, Pyramid hands ``hello_world`` a ``request`` that is +:ref:`based on WebOb <webob_chapter>`. + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes request_response; cd request_response + $ $VENV/bin/python setup.py develop + +#. Simplify the routes in ``request_response/tutorial/__init__.py``: + + .. literalinclude:: request_response/tutorial/__init__.py + +#. We only need one view in ``request_response/tutorial/views.py``: + + .. literalinclude:: request_response/tutorial/views.py + +#. Update the tests in ``request_response/tutorial/tests.py``: + + .. literalinclude:: request_response/tutorial/tests.py + +#. Now run the tests: + + .. code-block:: bash + + $ $VENV/bin/nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in your browser. You will be + redirected to http://localhost:6543/plain + +#. Open http://localhost:6543/plain?name=alice in your browser. + +Analysis +======== + +In this view class we have two routes and two views, with the first +leading to the second by an HTTP redirect. Pyramid can +:ref:`generate redirects <http_redirect>` by returning a +special object from a view or raising a special exception. + +In this Pyramid view, we get the URL being visited from ``request.url``. +Also, if you visited http://localhost:6543/plain?name=alice, +the name is included in the body of the response:: + + URL http://localhost:6543/plain?name=alice with name: alice + +Finally, we set the response's content type and body, then return the +Response. + +We updated the unit and functional tests to prove that our code +does the redirection, but also handles sending and not sending +``/plain?name``. + +Extra Credit +============ + +#. Could we also ``raise HTTPFound(location='/plain')`` instead of + returning it? If so, what's the difference? + +.. seealso:: :ref:`webob_chapter`, + :ref:`generate redirects <http_redirect>` diff --git a/docs/quick_tutorial/request_response/development.ini b/docs/quick_tutorial/request_response/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/request_response/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/request_response/setup.py b/docs/quick_tutorial/request_response/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/request_response/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/request_response/tutorial/__init__.py b/docs/quick_tutorial/request_response/tutorial/__init__.py new file mode 100644 index 000000000..77a172888 --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('home', '/') + config.add_route('plain', '/plain') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/request_response/tutorial/tests.py b/docs/quick_tutorial/request_response/tutorial/tests.py new file mode 100644 index 000000000..7486c2b2d --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/tests.py @@ -0,0 +1,54 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual(response.status, '302 Found') + + def test_plain_without_name(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.plain() + self.assertIn(b'No Name Provided', response.body) + + def test_plain_with_name(self): + from .views import TutorialViews + + request = testing.DummyRequest() + request.GET['name'] = 'Jane Doe' + inst = TutorialViews(request) + response = inst.plain() + self.assertIn(b'Jane Doe', response.body) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_plain_without_name(self): + res = self.testapp.get('/plain', status=200) + self.assertIn(b'No Name Provided', res.body) + + def test_plain_with_name(self): + res = self.testapp.get('/plain?name=Jane%20Doe', status=200) + self.assertIn(b'Jane Doe', res.body) diff --git a/docs/quick_tutorial/request_response/tutorial/views.py b/docs/quick_tutorial/request_response/tutorial/views.py new file mode 100644 index 000000000..8c7ff5f37 --- /dev/null +++ b/docs/quick_tutorial/request_response/tutorial/views.py @@ -0,0 +1,22 @@ +from pyramid.httpexceptions import HTTPFound +from pyramid.response import Response +from pyramid.view import view_config + + +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return HTTPFound(location='/plain') + + @view_config(route_name='plain') + def plain(self): + name = self.request.params.get('name', 'No Name Provided') + + body = 'URL %s with name: %s' % (self.request.url, name) + return Response( + content_type='text/plain', + body=body + ) diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst new file mode 100644 index 000000000..234e4aa0d --- /dev/null +++ b/docs/quick_tutorial/requirements.rst @@ -0,0 +1,250 @@ +.. _qtut_requirements: + +============ +Requirements +============ + +Let's get our tutorial environment setup. Most of the setup work is in +standard Python development practices (install Python, +make an isolated environment, and setup packaging tools.) + +.. note:: + + Pyramid encourages standard Python development practices with + packaging tools, virtual environments, logging, and so on. There + are many variations, implementations, and opinions across the Python + community. For consistency, ease of documentation maintenance, + and to minimize confusion, the Pyramid *documentation* has adopted + specific conventions. + +This *Quick Tutorial* is based on: + +* **Python 3.3**. Pyramid fully supports Python 3.2+ and Python 2.6+. + This tutorial uses **Python 3.3** but runs fine under Python 2.7. + +* **pyvenv**. We believe in virtual environments. For this tutorial, + we use Python 3.3's built-in solution, the ``pyvenv`` command. + For Python 2.7, you can install ``virtualenv``. + +* **setuptools and easy_install**. We use + `setuptools <https://pypi.python.org/pypi/setuptools/>`_ + and its ``easy_install`` for package management. + +* **Workspaces, projects, and packages.** Our home directory + will contain a *tutorial workspace* with our Python virtual + environment(s) and *Python projects* (a directory with packaging + information and *Python packages* of working code.) + +* **Unix commands**. Commands in this tutorial use UNIX syntax and + paths. Windows users should adjust commands accordingly. + +.. note:: + + Pyramid was one of the first web frameworks to fully support Python 3 in + October 2011. + +Steps +===== + +#. :ref:`install-python-3.3-or-greater` +#. :ref:`create-a-project-directory-structure` +#. :ref:`set-an-environment-variable` +#. :ref:`create-a-virtual-environment` +#. :ref:`install-setuptools-(python-packaging-tools)` +#. :ref:`install-pyramid` + +.. _install-python-3.3-or-greater: + +Install Python 3.3 or greater +----------------------------- + +Download the latest standard Python 3.3+ release (not development +release) from +`python.org <http://www.python.org/download/releases/>`_. On that page, you +must click the latest version, then scroll down to the "Downloads" section +for your operating system. + +Windows and Mac OS X users can download and run an installer. + +Windows users should also install the `Python for Windows extensions +<http://sourceforge.net/projects/pywin32/files/pywin32/>`_. Carefully read the +``README.txt`` file at the end of the list of builds, and follow its +directions. Make sure you get the proper 32- or 64-bit build and Python +version. + +Linux users can either use their package manager to install Python 3.3 +or may +`build Python 3.3 from source +<http://pyramid.readthedocs.org/en/master/narr/install.html#package-manager- +method>`_. + + +.. _create-a-project-directory-structure: + +Create a project directory structure +------------------------------------ + +We will arrive at a directory structure of +``workspace->project->package``, with our workspace named +``quick_tutorial``. The following diagram shows how this is structured +and where our virtual environment will reside: + +.. figure:: ../_static/directory_structure_pyramid.png + :alt: Final directory structure + + Final directory structure. + +For Linux, the commands to do so are as follows: + +.. code-block:: bash + + # Mac and Linux + $ cd ~ + $ mkdir -p projects/quick_tutorial + $ cd projects/quick_tutorial + +For Windows: + +.. code-block:: posh + + # Windows + c:\> cd \ + c:\> mkdir projects\quick_tutorial + c:\> cd projects\quick_tutorial + +In the above figure, your user home directory is represented by ``~``. In +your home directory, all of your projects are in the ``projects`` directory. +This is a general convention not specific to Pyramid that many developers use. +Windows users will do well to use ``c:\`` as the location for ``projects`` in +order to avoid spaces in any of the path names. + +Next within ``projects`` is your workspace directory, here named +``quick_tutorial``. A workspace is a common term used by integrated +development environments (IDE) like PyCharm and PyDev that stores +isolated Python environments (virtualenvs) and specific project files +and repositories. + + +.. _set-an-environment-variable: + +Set an Environment Variable +--------------------------- + +This tutorial will refer frequently to the location of the virtual +environment. We set an environment variable to save typing later. + +.. code-block:: bash + + # Mac and Linux + $ export VENV=~/projects/quick_tutorial/env33/ + + # Windows + # TODO: This command does not work + c:\> set VENV=c:\projects\quick_tutorial\env33 + + +.. _create-a-virtual-environment: + +Create a Virtual Environment +---------------------------- + +.. warning:: The current state of isolated Python environments using + ``pyvenv`` on Windows is suboptimal in comparison to Mac and Linux. See + http://stackoverflow.com/q/15981111/95735 for a discussion of the issue + and `PEP 453 <http://www.python.org/dev/peps/pep-0453/>`_ for a proposed + resolution. + +``pyvenv`` is a tool to create isolated Python 3.3 environments, each +with its own Python binary and independent set of installed Python +packages in its site directories. Let's create one, using the location +we just specified in the environment variable. + +.. code-block:: bash + + # Mac and Linux + $ pyvenv $VENV + + # Windows + c:\> c:\Python33\python -m venv %VENV% + +.. seealso:: See also Python 3's :mod:`venv module <python3:venv>`, + Python 2's `virtualenv <http://www.virtualenv.org/en/latest/>`_ + package, + :ref:`Installing Pyramid on a Windows System <installing_windows>` + + +.. _install-setuptools-(python-packaging-tools): + +Install ``setuptools`` (Python packaging tools) +----------------------------------------------- + +The following command will download a script to install ``setuptools``, then +pipe it to your environment's version of Python. + +.. code-block:: bash + + # Mac and Linux + $ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | $VENV/bin/python + + # Windows + # Use your browser to download: + # https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.p + # ...into c:\projects\quick_tutorial\ez_setup.py + c:\> %VENV%\Scripts\python ez_setup.py + +If ``wget`` complains with a certificate error, then run this command instead: + +.. code-block:: bash + + # Mac and Linux + $ wget --no-check-certificate https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | $VENV/bin/python + + +.. _install-pyramid: + +Install Pyramid +--------------- + +We have our Python standard prerequisites out of the way. The Pyramid +part is pretty easy: + +.. parsed-literal:: + + # Mac and Linux + $ $VENV/bin/easy_install "pyramid==\ |release|\ " + + # Windows + c:\\> %VENV%\\Scripts\\easy_install "pyramid==\ |release|\ " + +Our Python virtual environment now has the Pyramid software available. + +You can optionally install some of the extra Python packages used +during this tutorial: + +.. code-block:: bash + + # Mac and Linux + $ $VENV/bin/easy_install nose webtest deform sqlalchemy \ + pyramid_chameleon pyramid_debugtoolbar waitress \ + pyramid_jinja2 pyramid_tm zope.sqlalchemy + + # Windows + c:\> %VENV%\Scripts\easy_install nose webtest deform sqlalchemy pyramid_chameleon + + + +.. note:: + + Why ``easy_install`` and not ``pip``? Pyramid encourages use of namespace + packages which, until recently, ``pip`` didn't permit. Also, Pyramid has + some optional C extensions for performance. With ``easy_install``, Windows + users can get these extensions without needing a C compiler. + +.. seealso:: See Also: :ref:`installing_unix`. For instructions to set up your + Python environment for development using Windows or Python 2, see Pyramid's + :ref:`Before You Install <installing_chapter>`. + + See also Python 3's :mod:`venv module <python3:venv>`, the `setuptools` `installation instructions + <https://pypi.python.org/pypi/setuptools/0.9.8#installation-instructions>`_, + and `easy_install help <https://pypi.python.org/pypi/setuptools/0.9.8#using-setuptools-and-easyinstall>`_. + diff --git a/docs/quick_tutorial/retail_forms/development.ini b/docs/quick_tutorial/retail_forms/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/retail_forms/setup.py b/docs/quick_tutorial/retail_forms/setup.py new file mode 100644 index 000000000..361ade013 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'deform' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/retail_forms/tutorial/__init__.py b/docs/quick_tutorial/retail_forms/tutorial/__init__.py new file mode 100644 index 000000000..dff7457cf --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('wiki_view', '/') + config.add_route('wikipage_add', '/add') + config.add_route('wikipage_view', '/{uid}') + config.add_route('wikipage_edit', '/{uid}/edit') + config.add_static_view('deform_static', 'deform:static/') + config.scan('.views') + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/retail_forms/tutorial/tests.py b/docs/quick_tutorial/retail_forms/tutorial/tests.py new file mode 100644 index 000000000..5a2c40904 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/tests.py @@ -0,0 +1,36 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import WikiViews + + request = testing.DummyRequest() + inst = WikiViews(request) + response = inst.wiki_view() + self.assertEqual(len(response['pages']), 3) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def tearDown(self): + testing.tearDown() + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<title>Wiki: View</title>', res.body) diff --git a/docs/quick_tutorial/retail_forms/tutorial/views.py b/docs/quick_tutorial/retail_forms/tutorial/views.py new file mode 100644 index 000000000..2737ebdc4 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/views.py @@ -0,0 +1,96 @@ +import colander +import deform.widget + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +pages = { + '100': dict(uid='100', title='Page 100', body='<em>100</em>'), + '101': dict(uid='101', title='Page 101', body='<em>101</em>'), + '102': dict(uid='102', title='Page 102', body='<em>102</em>') +} + +class WikiPage(colander.MappingSchema): + title = colander.SchemaNode(colander.String()) + body = colander.SchemaNode( + colander.String(), + widget=deform.widget.RichTextWidget() + ) + + +class WikiViews(object): + def __init__(self, request): + self.request = request + + @property + def wiki_form(self): + schema = WikiPage() + return deform.Form(schema, buttons=('submit',)) + + @property + def reqts(self): + return self.wiki_form.get_widget_resources() + + @view_config(route_name='wiki_view', renderer='wiki_view.pt') + def wiki_view(self): + return dict(pages=pages.values()) + + @view_config(route_name='wikipage_add', + renderer='wikipage_addedit.pt') + def wikipage_add(self): + form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = self.wiki_form.validate(controls) + except deform.ValidationFailure as e: + # Form is NOT valid + return dict(form=e.render()) + + # Form is valid, make a new identifier and add to list + last_uid = int(sorted(pages.keys())[-1]) + new_uid = str(last_uid + 1) + pages[new_uid] = dict( + uid=new_uid, title=appstruct['title'], + body=appstruct['body'] + ) + + # Now visit new page + url = self.request.route_url('wikipage_view', uid=new_uid) + return HTTPFound(url) + + return dict(form=form) + + @view_config(route_name='wikipage_view', renderer='wikipage_view.pt') + def wikipage_view(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + return dict(page=page) + + @view_config(route_name='wikipage_edit', + renderer='wikipage_addedit.pt') + def wikipage_edit(self): + uid = self.request.matchdict['uid'] + page = pages[uid] + + wiki_form = self.wiki_form + + if 'submit' in self.request.params: + controls = self.request.POST.items() + try: + appstruct = wiki_form.validate(controls) + except deform.ValidationFailure as e: + return dict(page=page, form=e.render()) + + # Change the content and redirect to the view + page['title'] = appstruct['title'] + page['body'] = appstruct['body'] + + url = self.request.route_url('wikipage_view', + uid=page['uid']) + return HTTPFound(url) + + form = wiki_form.render(page) + + return dict(page=page, form=form)
\ No newline at end of file diff --git a/docs/quick_tutorial/retail_forms/tutorial/wiki_view.pt b/docs/quick_tutorial/retail_forms/tutorial/wiki_view.pt new file mode 100644 index 000000000..9e3afe495 --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/wiki_view.pt @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Wiki: View</title> +</head> +<body> +<h1>Wiki</h1> + +<a href="${request.route_url('wikipage_add')}">Add + WikiPage</a> +<ul> + <li tal:repeat="page pages"> + <a href="${request.route_url('wikipage_view', uid=page.uid)}"> + ${page.title} + </a> + </li> +</ul> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/retail_forms/tutorial/wikipage_addedit.pt b/docs/quick_tutorial/retail_forms/tutorial/wikipage_addedit.pt new file mode 100644 index 000000000..586f4c44b --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/wikipage_addedit.pt @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: Add/Edit</title> + <tal:block tal:repeat="reqt view.reqts['css']"> + <link rel="stylesheet" type="text/css" + href="${request.static_url('deform:static/' + reqt)}"/> + </tal:block> + <tal:block tal:repeat="reqt view.reqts['js']"> + <script src="${request.static_url('deform:static/' + reqt)}" + type="text/javascript"></script> + </tal:block> +</head> +<body> +<h1>Wiki</h1> + +<div class="row" + tal:repeat="field form"> + <div class="span2"> + ${structure:field.title} + <span class="req" tal:condition="field.required">*</span> + </div> + <div class="span2"> + ${structure:field.serialize()} + </div> + <ul tal:condition="field.error"> + <li tal:repeat="error field.error.messages()"> + ${structure:error} + </li> + </ul> +</div> + +<script type="text/javascript"> + deform.load() +</script> +</body> +</html> diff --git a/docs/quick_tutorial/retail_forms/tutorial/wikipage_view.pt b/docs/quick_tutorial/retail_forms/tutorial/wikipage_view.pt new file mode 100644 index 000000000..cb9ff526e --- /dev/null +++ b/docs/quick_tutorial/retail_forms/tutorial/wikipage_view.pt @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>WikiPage: View</title> +</head> +<body> +<a href="${request.route_url('wiki_view')}"> + Up +</a> | +<a href="${request.route_url('wikipage_edit', uid=page.uid)}"> + Edit +</a> + +<h1>${page.title}</h1> +<p>${structure: page.body}</p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/routing.rst b/docs/quick_tutorial/routing.rst new file mode 100644 index 000000000..54dff5c39 --- /dev/null +++ b/docs/quick_tutorial/routing.rst @@ -0,0 +1,121 @@ +.. _qtut_routing: + +========================================== +11: Dispatching URLs To Views With Routing +========================================== + +Routing matches incoming URL patterns to view code. Pyramid's routing +has a number of useful features. + +Background +========== + +Writing web applications usually means sophisticated URL design. We +just saw some Pyramid machinery for requests and views. Let's look at +features that help in routing. + +Previously we saw the basics of routing URLs to views in + +- Your project's "setup" code registers a route name to be used when + matching part of the URL + +- Elsewhere, a view is configured to be called for that route name + +.. note:: + + Why do this twice? Other Python web frameworks let you create a + route and associate it with a view in one step. As + illustrated in :ref:`routes_need_ordering`, multiple routes might match the + same URL pattern. Rather than provide ways to help guess, Pyramid lets you + be explicit in ordering. Pyramid also gives facilities to avoid the + problem. It's relatively easy to build a system that uses implicit route + ordering with Pyramid too. See `The Groundhog series of screencasts + <http://bfg.repoze.org/videos#groundhog1>`_ if you're interested in + doing so. + +Objectives +========== + +- Define a route that extracts part of the URL into a Python dictionary + +- Use that dictionary data in a view + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes routing; cd routing + $ $VENV/bin/python setup.py develop + +#. Our ``routing/tutorial/__init__.py`` needs a route with a replacement + pattern: + + .. literalinclude:: routing/tutorial/__init__.py + :linenos: + +#. We just need one view in ``routing/tutorial/views.py``: + + .. literalinclude:: routing/tutorial/views.py + :linenos: + +#. We just need one view in ``routing/tutorial/home.pt``: + + .. literalinclude:: routing/tutorial/home.pt + :language: html + :linenos: + +#. Update ``routing/tutorial/tests.py``: + + .. literalinclude:: routing/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + $ $VENV/bin/nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/howdy/amy/smith in your browser. + +Analysis +======== + +In ``__init__.py`` we see an important change in our route declaration: + +.. code-block:: python + + config.add_route('hello', '/howdy/{first}/{last}') + +With this we tell the :term:`configurator` that our URL has +a "replacement pattern". With this, URLs such as ``/howdy/amy/smith`` +will assign ``amy`` to ``first`` and ``smith`` to ``last``. We can then +use this data in our view: + +.. code-block:: python + + self.request.matchdict['first'] + self.request.matchdict['last'] + +``request.matchdict`` contains values from the URL that match the +"replacement patterns" (the curly braces) in the route declaration. +This information can then be used anywhere in Pyramid that has access +to the request. + +Extra Credit +============ + +#. What happens if you to go the URL + http://localhost:6543/howdy? Is this the result that you + expected? + +.. seealso:: `Weird Stuff You Can Do With URL + Dispatch <http://www.plope.com/weird_pyramid_urldispatch>`_ diff --git a/docs/quick_tutorial/routing/development.ini b/docs/quick_tutorial/routing/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/routing/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/routing/setup.py b/docs/quick_tutorial/routing/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/routing/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/routing/tutorial/__init__.py b/docs/quick_tutorial/routing/tutorial/__init__.py new file mode 100644 index 000000000..4b2dac36d --- /dev/null +++ b/docs/quick_tutorial/routing/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/howdy/{first}/{last}') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/routing/tutorial/home.pt b/docs/quick_tutorial/routing/tutorial/home.pt new file mode 100644 index 000000000..f2b991059 --- /dev/null +++ b/docs/quick_tutorial/routing/tutorial/home.pt @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> +<h1>${name}</h1> +<p>First: ${first}, Last: ${last}</p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/routing/tutorial/tests.py b/docs/quick_tutorial/routing/tutorial/tests.py new file mode 100644 index 000000000..572f389fb --- /dev/null +++ b/docs/quick_tutorial/routing/tutorial/tests.py @@ -0,0 +1,36 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + request.matchdict['first'] = 'First' + request.matchdict['last'] = 'Last' + inst = TutorialViews(request) + response = inst.home() + self.assertEqual(response['first'], 'First') + self.assertEqual(response['last'], 'Last') + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/howdy/Jane/Doe', status=200) + self.assertIn(b'Jane', res.body) + self.assertIn(b'Doe', res.body) diff --git a/docs/quick_tutorial/routing/tutorial/views.py b/docs/quick_tutorial/routing/tutorial/views.py new file mode 100644 index 000000000..8a9211e92 --- /dev/null +++ b/docs/quick_tutorial/routing/tutorial/views.py @@ -0,0 +1,20 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + first = self.request.matchdict['first'] + last = self.request.matchdict['last'] + return { + 'name': 'Home View', + 'first': first, + 'last': last + } diff --git a/docs/quick_tutorial/scaffolds.rst b/docs/quick_tutorial/scaffolds.rst new file mode 100644 index 000000000..8ca2d27df --- /dev/null +++ b/docs/quick_tutorial/scaffolds.rst @@ -0,0 +1,86 @@ +.. _qtut_scaffolds: + +============================================= +Prelude: Quick Project Startup with Scaffolds +============================================= + +To ease the process of getting started, Pyramid provides *scaffolds* +that generate sample projects from templates in Pyramid and Pyramid +add-ons. + +Background +========== + +We're going to cover a lot in this tutorial, focusing on one topic at a +time and writing everything from scratch. As a warmup, though, +it sure would be nice to see some pixels on a screen. + +Like other web development frameworks, Pyramid provides a number of +"scaffolds" that generate working Python, template, and CSS code for +sample applications. In this step we'll use a built-in scaffold to let +us preview a Pyramid application, before starting from scratch on Step 1. + +Objectives +========== + +- Use Pyramid's ``pcreate`` command to list scaffolds and make a new + project + +- Start up a Pyramid application and visit it in a web browser + +Steps +===== + +#. Pyramid's ``pcreate`` command can list the available scaffolds: + + .. code-block:: bash + + $ $VENV/bin/pcreate --list + Available scaffolds: + alchemy: Pyramid SQLAlchemy project using url dispatch + starter: Pyramid starter project + zodb: Pyramid ZODB project using traversal + +#. Tell ``pcreate`` to use the ``starter`` scaffold to make our project: + + .. code-block:: bash + + $ $VENV/bin/pcreate --scaffold starter scaffolds + +#. Use normal Python development to setup our project for development: + + .. code-block:: bash + + $ cd scaffolds + $ $VENV/bin/python setup.py develop + +#. Startup the application by pointing Pyramid's ``pserve`` command at + the project's (generated) configuration file: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + + On startup, ``pserve`` logs some output: + + .. code-block:: bash + + Starting subprocess with file monitor + Starting server in PID 72213. + Starting HTTP server on http://0.0.0.0:6543 + +#. Open http://localhost:6543/ in your browser. + +Analysis +======== + +Rather than starting from scratch, ``pcreate`` can make getting a +Python project containing a Pyramid application a quick matter. +Pyramid ships with a few scaffolds. But installing a Pyramid add-on can +give you new scaffolds from that add-on. + +``pserve`` is Pyramid's application runner, separating operational +details from your code. When you install Pyramid, a small command +program called ``pserve`` is written to your ``bin`` directory. This +program is an executable Python module. It is passed a configuration +file (in this case, ``development.ini``.) diff --git a/docs/quick_tutorial/scaffolds/CHANGES.txt b/docs/quick_tutorial/scaffolds/CHANGES.txt new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/CHANGES.txt @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/docs/quick_tutorial/scaffolds/MANIFEST.in b/docs/quick_tutorial/scaffolds/MANIFEST.in new file mode 100644 index 000000000..91d3f763b --- /dev/null +++ b/docs/quick_tutorial/scaffolds/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include scaffolds *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/docs/quick_tutorial/scaffolds/README.txt b/docs/quick_tutorial/scaffolds/README.txt new file mode 100644 index 000000000..7776dd2d5 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/README.txt @@ -0,0 +1 @@ +scaffolds README diff --git a/docs/quick_tutorial/scaffolds/development.ini b/docs/quick_tutorial/scaffolds/development.ini new file mode 100644 index 000000000..b31d06194 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/development.ini @@ -0,0 +1,60 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:scaffolds + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, scaffolds + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_scaffolds] +level = DEBUG +handlers = +qualname = scaffolds + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/docs/quick_tutorial/scaffolds/production.ini b/docs/quick_tutorial/scaffolds/production.ini new file mode 100644 index 000000000..1418e6bf6 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/production.ini @@ -0,0 +1,54 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:scaffolds + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +host = 0.0.0.0 +port = 6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, scaffolds + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_scaffolds] +level = WARN +handlers = +qualname = scaffolds + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s diff --git a/docs/quick_tutorial/scaffolds/scaffolds/__init__.py b/docs/quick_tutorial/scaffolds/scaffolds/__init__.py new file mode 100644 index 000000000..ad5ecbc6f --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/__init__.py @@ -0,0 +1,12 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.scan() + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/favicon.ico b/docs/quick_tutorial/scaffolds/scaffolds/static/favicon.ico Binary files differnew file mode 100644 index 000000000..71f837c9e --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/favicon.ico diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/footerbg.png b/docs/quick_tutorial/scaffolds/scaffolds/static/footerbg.png Binary files differnew file mode 100644 index 000000000..1fbc873da --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/footerbg.png diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/headerbg.png b/docs/quick_tutorial/scaffolds/scaffolds/static/headerbg.png Binary files differnew file mode 100644 index 000000000..0596f2020 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/headerbg.png diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/ie6.css b/docs/quick_tutorial/scaffolds/scaffolds/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/middlebg.png b/docs/quick_tutorial/scaffolds/scaffolds/static/middlebg.png Binary files differnew file mode 100644 index 000000000..2369cfb7d --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/middlebg.png diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/pylons.css b/docs/quick_tutorial/scaffolds/scaffolds/static/pylons.css new file mode 100644 index 000000000..4b1c017cd --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/pylons.css @@ -0,0 +1,372 @@ +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td +{ + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; /* 16px */ + vertical-align: baseline; + background: transparent; +} + +body +{ + line-height: 1; +} + +ol, ul +{ + list-style: none; +} + +blockquote, q +{ + quotes: none; +} + +blockquote:before, blockquote:after, q:before, q:after +{ + content: ''; + content: none; +} + +:focus +{ + outline: 0; +} + +ins +{ + text-decoration: none; +} + +del +{ + text-decoration: line-through; +} + +table +{ + border-collapse: collapse; + border-spacing: 0; +} + +sub +{ + vertical-align: sub; + font-size: smaller; + line-height: normal; +} + +sup +{ + vertical-align: super; + font-size: smaller; + line-height: normal; +} + +ul, menu, dir +{ + display: block; + list-style-type: disc; + margin: 1em 0; + padding-left: 40px; +} + +ol +{ + display: block; + list-style-type: decimal-leading-zero; + margin: 1em 0; + padding-left: 40px; +} + +li +{ + display: list-item; +} + +ul ul, ul ol, ul dir, ul menu, ul dl, ol ul, ol ol, ol dir, ol menu, ol dl, dir ul, dir ol, dir dir, dir menu, dir dl, menu ul, menu ol, menu dir, menu menu, menu dl, dl ul, dl ol, dl dir, dl menu, dl dl +{ + margin-top: 0; + margin-bottom: 0; +} + +ol ul, ul ul, menu ul, dir ul, ol menu, ul menu, menu menu, dir menu, ol dir, ul dir, menu dir, dir dir +{ + list-style-type: circle; +} + +ol ol ul, ol ul ul, ol menu ul, ol dir ul, ol ol menu, ol ul menu, ol menu menu, ol dir menu, ol ol dir, ol ul dir, ol menu dir, ol dir dir, ul ol ul, ul ul ul, ul menu ul, ul dir ul, ul ol menu, ul ul menu, ul menu menu, ul dir menu, ul ol dir, ul ul dir, ul menu dir, ul dir dir, menu ol ul, menu ul ul, menu menu ul, menu dir ul, menu ol menu, menu ul menu, menu menu menu, menu dir menu, menu ol dir, menu ul dir, menu menu dir, menu dir dir, dir ol ul, dir ul ul, dir menu ul, dir dir ul, dir ol menu, dir ul menu, dir menu menu, dir dir menu, dir ol dir, dir ul dir, dir menu dir, dir dir dir +{ + list-style-type: square; +} + +.hidden +{ + display: none; +} + +p +{ + line-height: 1.5em; +} + +h1 +{ + font-size: 1.75em; + line-height: 1.7em; + font-family: helvetica, verdana; +} + +h2 +{ + font-size: 1.5em; + line-height: 1.7em; + font-family: helvetica, verdana; +} + +h3 +{ + font-size: 1.25em; + line-height: 1.7em; + font-family: helvetica, verdana; +} + +h4 +{ + font-size: 1em; + line-height: 1.7em; + font-family: helvetica, verdana; +} + +html, body +{ + width: 100%; + height: 100%; +} + +body +{ + margin: 0; + padding: 0; + background-color: #fff; + position: relative; + font: 16px/24px NobileRegular, "Lucida Grande", Lucida, Verdana, sans-serif; +} + +a +{ + color: #1b61d6; + text-decoration: none; +} + +a:hover +{ + color: #e88f00; + text-decoration: underline; +} + +body h1, body h2, body h3, body h4, body h5, body h6 +{ + font-family: NeutonRegular, "Lucida Grande", Lucida, Verdana, sans-serif; + font-weight: 400; + color: #373839; + font-style: normal; +} + +#wrap +{ + min-height: 100%; +} + +#header, #footer +{ + width: 100%; + color: #fff; + height: 40px; + position: absolute; + text-align: center; + line-height: 40px; + overflow: hidden; + font-size: 12px; + vertical-align: middle; +} + +#header +{ + background: #000; + top: 0; + font-size: 14px; +} + +#footer +{ + bottom: 0; + background: #000 url(footerbg.png) repeat-x 0 top; + position: relative; + margin-top: -40px; + clear: both; +} + +.header, .footer +{ + width: 750px; + margin-right: auto; + margin-left: auto; +} + +.wrapper +{ + width: 100%; +} + +#top, #top-small, #bottom +{ + width: 100%; +} + +#top +{ + color: #000; + height: 230px; + background: #fff url(headerbg.png) repeat-x 0 top; + position: relative; +} + +#top-small +{ + color: #000; + height: 60px; + background: #fff url(headerbg.png) repeat-x 0 top; + position: relative; +} + +#bottom +{ + color: #222; + background-color: #fff; +} + +.top, .top-small, .middle, .bottom +{ + width: 750px; + margin-right: auto; + margin-left: auto; +} + +.top +{ + padding-top: 40px; +} + +.top-small +{ + padding-top: 10px; +} + +#middle +{ + width: 100%; + height: 100px; + background: url(middlebg.png) repeat-x; + border-top: 2px solid #fff; + border-bottom: 2px solid #b2b2b2; +} + +.app-welcome +{ + margin-top: 25px; +} + +.app-name +{ + color: #000; + font-weight: 700; +} + +.bottom +{ + padding-top: 50px; +} + +#left +{ + width: 350px; + float: left; + padding-right: 25px; +} + +#right +{ + width: 350px; + float: right; + padding-left: 25px; +} + +.align-left +{ + text-align: left; +} + +.align-right +{ + text-align: right; +} + +.align-center +{ + text-align: center; +} + +ul.links +{ + margin: 0; + padding: 0; +} + +ul.links li +{ + list-style-type: none; + font-size: 14px; +} + +form +{ + border-style: none; +} + +fieldset +{ + border-style: none; +} + +input +{ + color: #222; + border: 1px solid #ccc; + font-family: sans-serif; + font-size: 12px; + line-height: 16px; +} + +input[type=text], input[type=password] +{ + width: 205px; +} + +input[type=submit] +{ + background-color: #ddd; + font-weight: 700; +} + +/*Opera Fix*/ +body:before +{ + content: ""; + height: 100%; + float: left; + width: 0; + margin-top: -32767px; +} diff --git a/pyramid/scaffolds/alchemy/+package+/static/pyramid-small.png b/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid-small.png Binary files differindex a5bc0ade7..a5bc0ade7 100644 --- a/pyramid/scaffolds/alchemy/+package+/static/pyramid-small.png +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid-small.png diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid.png b/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid.png Binary files differnew file mode 100644 index 000000000..347e05549 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/pyramid.png diff --git a/docs/quick_tutorial/scaffolds/scaffolds/static/transparent.gif b/docs/quick_tutorial/scaffolds/scaffolds/static/transparent.gif Binary files differnew file mode 100644 index 000000000..0341802e5 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/static/transparent.gif diff --git a/docs/quick_tutorial/scaffolds/scaffolds/templates/mytemplate.pt b/docs/quick_tutorial/scaffolds/scaffolds/templates/mytemplate.pt new file mode 100644 index 000000000..b43a174e3 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/templates/mytemplate.pt @@ -0,0 +1,73 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal"> +<head> + <title>The Pyramid Web Framework</title> + <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> + <meta name="keywords" content="python web application" /> + <meta name="description" content="pyramid web application" /> + <link rel="shortcut icon" href="${request.static_url('scaffolds:static/favicon.ico')}" /> + <link rel="stylesheet" href="${request.static_url('scaffolds:static/pylons.css')}" type="text/css" media="screen" charset="utf-8" /> + <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/nobile/stylesheet.css" media="screen" /> + <link rel="stylesheet" href="http://static.pylonsproject.org/fonts/neuton/stylesheet.css" media="screen" /> + <!--[if lte IE 6]> + <link rel="stylesheet" href="${request.static_url('scaffolds:static/ie6.css')}" type="text/css" media="screen" charset="utf-8" /> + <![endif]--> +</head> +<body> + <div id="wrap"> + <div id="top"> + <div class="top align-center"> + <div><img src="${request.static_url('scaffolds:static/pyramid.png')}" width="750" height="169" alt="pyramid"/></div> + </div> + </div> + <div id="middle"> + <div class="middle align-center"> + <p class="app-welcome"> + Welcome to <span class="app-name">${project}</span>, an application generated by<br/> + the Pyramid Web Framework. + </p> + </div> + </div> + <div id="bottom"> + <div class="bottom"> + <div id="left" class="align-right"> + <h2>Search documentation</h2> + <form method="get" action="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/search.html"> + <input type="text" id="q" name="q" value="" /> + <input type="submit" id="x" value="Go" /> + </form> + </div> + <div id="right" class="align-left"> + <h2>Pyramid links</h2> + <ul class="links"> + <li> + <a href="http://pylonsproject.org">Pylons Website</a> + </li> + <li> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#narrative-documentation">Narrative Documentation</a> + </li> + <li> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#reference-material">API Documentation</a> + </li> + <li> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#tutorials">Tutorials</a> + </li> + <li> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#detailed-change-history">Change History</a> + </li> + <li> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#sample-applications">Sample Applications</a> + </li> + <li> + <a href="http://docs.pylonsproject.org/projects/pyramid/en/1.4-branch/#support-and-development">Support and Development</a> + </li> + <li> + <a href="irc://irc.freenode.net#pyramid">IRC Channel</a> + </li> + </ul> + </div> + </div> + </div> + </div> +</body> +</html> diff --git a/docs/quick_tutorial/scaffolds/scaffolds/tests.py b/docs/quick_tutorial/scaffolds/scaffolds/tests.py new file mode 100644 index 000000000..4f906ffa9 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/tests.py @@ -0,0 +1,17 @@ +import unittest + +from pyramid import testing + + +class ViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_my_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['project'], 'scaffolds') diff --git a/docs/quick_tutorial/scaffolds/scaffolds/views.py b/docs/quick_tutorial/scaffolds/scaffolds/views.py new file mode 100644 index 000000000..db90d8364 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/scaffolds/views.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config + + +@view_config(route_name='home', renderer='templates/mytemplate.pt') +def my_view(request): + return {'project': 'scaffolds'} diff --git a/docs/quick_tutorial/scaffolds/setup.cfg b/docs/quick_tutorial/scaffolds/setup.cfg new file mode 100644 index 000000000..c980261e3 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/setup.cfg @@ -0,0 +1,27 @@ +[nosetests] +match = ^test +nocapture = 1 +cover-package = scaffolds +with-coverage = 1 +cover-erase = 1 + +[compile_catalog] +directory = scaffolds/locale +domain = scaffolds +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = scaffolds/locale/scaffolds.pot +width = 80 + +[init_catalog] +domain = scaffolds +input_file = scaffolds/locale/scaffolds.pot +output_dir = scaffolds/locale + +[update_catalog] +domain = scaffolds +input_file = scaffolds/locale/scaffolds.pot +output_dir = scaffolds/locale +previous = true diff --git a/docs/quick_tutorial/scaffolds/setup.py b/docs/quick_tutorial/scaffolds/setup.py new file mode 100644 index 000000000..ec95946a5 --- /dev/null +++ b/docs/quick_tutorial/scaffolds/setup.py @@ -0,0 +1,42 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'waitress', + ] + +setup(name='scaffolds', + version='0.0', + description='scaffolds', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + tests_require=requires, + test_suite="scaffolds", + entry_points="""\ + [paste.app_factory] + main = scaffolds:main + """, + ) diff --git a/docs/quick_tutorial/sessions.rst b/docs/quick_tutorial/sessions.rst new file mode 100644 index 000000000..0f284e9a7 --- /dev/null +++ b/docs/quick_tutorial/sessions.rst @@ -0,0 +1,100 @@ +.. _qtut_sessions: + +================================= +17: Transient Data Using Sessions +================================= + +Store and retrieve non-permanent data in Pyramid sessions. + +Background +========== + +When people use your web application, they frequently perform a task +that requires semi-permanent data to be saved. For example, a shopping +cart. This is called a :term:`session`. + +Pyramid has basic built-in support for sessions, with add-ons +or your own custom sessioning engine) that can provide +richer session support. Let's take a look at the +:ref:`built-in sessioning support <sessions_chapter>`. + +Objectives +========== + +- Make a session factory using a built-in, simple Pyramid sessioning + system + +- Change our code to use a session + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes sessions; cd sessions + $ $VENV/bin/python setup.py develop + +#. Our ``sessions/tutorial/__init__.py`` needs a choice of session + factory to get registered with the :term:`configurator`: + + .. literalinclude:: sessions/tutorial/__init__.py + :linenos: + +#. Our views in ``sessions/tutorial/views.py`` can now use + ``request.session``: + + .. literalinclude:: sessions/tutorial/views.py + :linenos: + +#. The template at ``sessions/tutorial/home.pt`` can display the value: + + .. literalinclude:: sessions/tutorial/home.pt + :language: html + :linenos: + +#. Make sure the tests still pass: + + .. code-block:: bash + + $ $VENV/bin/nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy + in your browser. As you reload and switch between those URLs, note + that the counter increases and is *not* specific to the URL. + +#. Restart the application and revisit the page. Note that counter + still increases from where it left off. + +Analysis +======== + +Pyramid's :term:`request` object now has a ``session`` attribute +that we can use in our view code. It acts like a dictionary. + +Since all the views are using the same counter, we made the counter a +Python property at the view class level. With this, each reload will +increase the counter displayed in our template. + +In web development, "flash messages" are notes for the user that need +to appear on a screen after a future web request. For example, +when you add an item using a form ``POST``, the site usually issues a +second HTTP Redirect web request to view the new item. You might want a +message to appear after that second web request saying "Your item was +added." You can't just return it in the web response for the POST, +as it will be tossed out during the second web requests. + +Flash messages are a technique where messages can be stored between +requests, using sessions, then removed when they finally get displayed. + +.. seealso:: + :ref:`sessions_chapter`, + :ref:`flash_messages`, and + :ref:`session_module`. diff --git a/docs/quick_tutorial/sessions/development.ini b/docs/quick_tutorial/sessions/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/sessions/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/sessions/setup.py b/docs/quick_tutorial/sessions/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/sessions/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/sessions/tutorial/__init__.py b/docs/quick_tutorial/sessions/tutorial/__init__.py new file mode 100644 index 000000000..9ddc2e1b1 --- /dev/null +++ b/docs/quick_tutorial/sessions/tutorial/__init__.py @@ -0,0 +1,14 @@ +from pyramid.config import Configurator +from pyramid.session import SignedCookieSessionFactory + + +def main(global_config, **settings): + my_session_factory = SignedCookieSessionFactory( + 'itsaseekreet') + config = Configurator(settings=settings, + session_factory=my_session_factory) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app() diff --git a/docs/quick_tutorial/sessions/tutorial/home.pt b/docs/quick_tutorial/sessions/tutorial/home.pt new file mode 100644 index 000000000..0b27ba1d8 --- /dev/null +++ b/docs/quick_tutorial/sessions/tutorial/home.pt @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +<p>Count: ${view.counter}</p> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/sessions/tutorial/tests.py b/docs/quick_tutorial/sessions/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/sessions/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/sessions/tutorial/views.py b/docs/quick_tutorial/sessions/tutorial/views.py new file mode 100644 index 000000000..a4659d265 --- /dev/null +++ b/docs/quick_tutorial/sessions/tutorial/views.py @@ -0,0 +1,29 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @property + def counter(self): + session = self.request.session + if 'counter' in session: + session['counter'] += 1 + else: + session['counter'] = 1 + + return session['counter'] + + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/static_assets.rst b/docs/quick_tutorial/static_assets.rst new file mode 100644 index 000000000..19d33f00f --- /dev/null +++ b/docs/quick_tutorial/static_assets.rst @@ -0,0 +1,91 @@ +.. _qtut_static_assets: + +========================================== +13: CSS/JS/Images Files With Static Assets +========================================== + +Of course the Web is more than just markup. You need static assets: +CSS, JS, and images. Let's point our web app at a directory where +Pyramid will serve some static assets. + +Objectives +========== + +- Publish a directory of static assets at a URL + +- Use Pyramid to help generate URLs to files in that directory + +Steps +===== + +#. First we copy the results of the ``view_classes`` step: + + .. code-block:: bash + + $ cd ..; cp -r view_classes static_assets; cd static_assets + $ $VENV/bin/python setup.py develop + +#. We add a call ``config.add_static_view in + ``static_assets/tutorial/__init__.py``: + + .. literalinclude:: static_assets/tutorial/__init__.py + :linenos: + +#. We can add a CSS link in the ``<head>`` of our template at + ``static_assets/tutorial/home.pt``: + + .. literalinclude:: static_assets/tutorial/home.pt + :language: html + +#. Add a CSS file at + ``static_assets/tutorial/static/app.css``: + + .. literalinclude:: static_assets/tutorial/static/app.css + :language: css + +#. Make sure we haven't broken any existing code by running the tests: + + .. code-block:: bash + + $ $VENV/bin/nosetests tutorial + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ in your browser and note the new font. + +Analysis +======== + +We changed our WSGI application to map requests under +http://localhost:6543/static/ to files and directories inside a +``static`` directory inside our ``tutorial`` package. This directory +contained ``app.css``. + +We linked to the CSS in our template. We could have hard-coded this +link to ``/static/app.css``. But what if the site is later moved under +``/somesite/static/``? Or perhaps the web developer changes the +arrangement on disk? Pyramid gives a helper that provides flexibility +on URL generation: + +.. code-block:: html + + ${request.static_url('tutorial:static/app.css')} + +This matches the ``path='tutorial:static'`` in our +``config.add_static_view`` registration. By using ``request.static_url`` +to generate the full URL to the static assets, you both ensure you stay +in sync with the configuration and gain refactoring flexibility later. + +Extra Credit +============ + +#. There is also a ``request.static_path`` API. How does this differ from + ``request.static_url``? + +.. seealso:: :ref:`assets_chapter`, + :ref:`preventing_http_caching`, and + :ref:`influencing_http_caching` diff --git a/docs/quick_tutorial/static_assets/development.ini b/docs/quick_tutorial/static_assets/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/static_assets/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/static_assets/setup.py b/docs/quick_tutorial/static_assets/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/static_assets/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/static_assets/tutorial/__init__.py b/docs/quick_tutorial/static_assets/tutorial/__init__.py new file mode 100644 index 000000000..e244c2997 --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/__init__.py @@ -0,0 +1,11 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.add_static_view(name='static', path='tutorial:static') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/static_assets/tutorial/home.pt b/docs/quick_tutorial/static_assets/tutorial/home.pt new file mode 100644 index 000000000..5d347f057 --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/home.pt @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> + <link rel="stylesheet" + href="${request.static_url('tutorial:static/app.css') }"/> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/static_assets/tutorial/static/app.css b/docs/quick_tutorial/static_assets/tutorial/static/app.css new file mode 100644 index 000000000..f8acf3164 --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/static/app.css @@ -0,0 +1,4 @@ +body { + margin: 2em; + font-family: sans-serif; +}
\ No newline at end of file diff --git a/docs/quick_tutorial/static_assets/tutorial/tests.py b/docs/quick_tutorial/static_assets/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/static_assets/tutorial/views.py b/docs/quick_tutorial/static_assets/tutorial/views.py new file mode 100644 index 000000000..a56c0adbf --- /dev/null +++ b/docs/quick_tutorial/static_assets/tutorial/views.py @@ -0,0 +1,18 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/templating.rst b/docs/quick_tutorial/templating.rst new file mode 100644 index 000000000..d73067f48 --- /dev/null +++ b/docs/quick_tutorial/templating.rst @@ -0,0 +1,123 @@ +.. _qtut_templating: + +=================================== +08: HTML Generation With Templating +=================================== + +Most web frameworks don't embed HTML in programming code. Instead, +they pass data into a templating system. In this step we look at the +basics of using HTML templates in Pyramid. + +Background +========== + +Ouch. We have been making our own ``Response`` and filling the response +body with HTML. You usually won't embed an HTML string directly in +Python, but instead, will use a templating language. + +Pyramid doesn't mandate a particular database system, form library, +etc. It encourages replaceability. This applies equally to templating, +which is fortunate: developers have strong views about template +languages. As of Pyramid 1.5a2, Pyramid doesn't even bundle a template +language! + +It does, however, have strong ties to Jinja2, Mako, and Chameleon. In +this step we see how to add ``pyramid_chameleon`` to your project, +then change your views to use templating. + +Objectives +========== + +- Enable the ``pyramid_chameleon`` Pyramid add-on + +- Generate HTML from template files + +- Connect the templates as "renderers" for view code + +- Change the view code to simply return data + +Steps +===== + +#. Let's begin by using the previous package as a starting point for a + new project: + + .. code-block:: bash + + $ cd ..; cp -r views templating; cd templating + +#. This step depends on ``pyramid_chameleon``, so add it as a dependency + in ``templating/setup.py``: + + .. literalinclude:: templating/setup.py + :linenos: + +#. Now we can activate the development-mode distribution: + + .. code-block:: bash + + $ $VENV/bin/python setup.py develop + +#. We need to connect ``pyramid_chameleon`` as a renderer by making a + call in the setup of ``templating/tutorial/__init__.py``: + + .. literalinclude:: templating/tutorial/__init__.py + :linenos: + +#. Our ``templating/tutorial/views.py`` no longer has HTML in it: + + .. literalinclude:: templating/tutorial/views.py + :linenos: + +#. Instead we have ``templating/tutorial/home.pt`` as a template: + + .. literalinclude:: templating/tutorial/home.pt + :language: html + +#. For convenience, change ``templating/development.ini`` to reload + templates automatically with ``pyramid.reload_templates``: + + .. literalinclude:: templating/development.ini + :language: ini + +#. Our unit tests in ``templating/tutorial/tests.py`` can focus on + data: + + .. literalinclude:: templating/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + + $ $VENV/bin/nosetests tutorial + . + ---------------------------------------------------------------------- + Ran 4 tests in 0.141s + + OK + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy + in your browser. + +Analysis +======== + +Ahh, that looks better. We have a view that is focused on Python code. +Our ``@view_config`` decorator specifies a :term:`renderer` that points +our template file. Our view then simply returns data which is then +supplied to our template. Note that we used the same template for both +views. + +Note the effect on testing. We can focus on having a data-oriented +contract with our view code. + +.. seealso:: :ref:`templates_chapter`, :ref:`debugging_templates`, and + :ref:`available_template_system_bindings`. diff --git a/docs/quick_tutorial/templating/development.ini b/docs/quick_tutorial/templating/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/templating/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/templating/setup.py b/docs/quick_tutorial/templating/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/templating/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/templating/tutorial/__init__.py b/docs/quick_tutorial/templating/tutorial/__init__.py new file mode 100644 index 000000000..c3e1c9eef --- /dev/null +++ b/docs/quick_tutorial/templating/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/templating/tutorial/home.pt b/docs/quick_tutorial/templating/tutorial/home.pt new file mode 100644 index 000000000..a0cc08e7a --- /dev/null +++ b/docs/quick_tutorial/templating/tutorial/home.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/templating/tutorial/tests.py b/docs/quick_tutorial/templating/tutorial/tests.py new file mode 100644 index 000000000..d06a62982 --- /dev/null +++ b/docs/quick_tutorial/templating/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import home + + request = testing.DummyRequest() + response = home(request) + # Our view now returns data + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import hello + + request = testing.DummyRequest() + response = hello(request) + # Our view now returns data + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/templating/tutorial/views.py b/docs/quick_tutorial/templating/tutorial/views.py new file mode 100644 index 000000000..979d69c43 --- /dev/null +++ b/docs/quick_tutorial/templating/tutorial/views.py @@ -0,0 +1,13 @@ +from pyramid.view import view_config + + +# First view, available at http://localhost:6543/ +@view_config(route_name='home', renderer='home.pt') +def home(request): + return {'name': 'Home View'} + + +# /howdy +@view_config(route_name='hello', renderer='home.pt') +def hello(request): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/tutorial_approach.rst b/docs/quick_tutorial/tutorial_approach.rst new file mode 100644 index 000000000..52d768306 --- /dev/null +++ b/docs/quick_tutorial/tutorial_approach.rst @@ -0,0 +1,45 @@ +================= +Tutorial Approach +================= + +This tutorial uses conventions to keep the introduction focused and +concise. Details, references, and deeper discussions are mentioned in +"See Also" notes. + +.. seealso:: This is an example "See Also" note. + +This "Getting Started" tutorial is broken into independent steps, +starting with the smallest possible "single file WSGI app" example. +Each of these steps introduce a topic and a very small set of concepts +via working code. The steps each correspond to a directory in this +repo, where each step/topic/directory is a Python package. + +To successfully run each step:: + + $ cd request_response + $ $VENV/bin/python setup.py develop + +...and repeat for each step you would like to work on. In most cases we +will start with the results of an earlier step. + +Directory Tree +============== + +As we develop our tutorial our directory tree will resemble the +structure below:: + + quicktutorial/ + request_response/ + development.ini + setup.py + tutorial/ + __init__.py + home.pt + tests.py + views.py + +Each of the first-level directories (e.g. ``request_response``) is a +*Python project* (except, as noted, the ``hello_world`` step.) The +``tutorial`` directory is a *Python package*. At the end of each step, +we copy a previous directory into a new directory to use as a starting +point.
\ No newline at end of file diff --git a/docs/quick_tutorial/unit_testing.rst b/docs/quick_tutorial/unit_testing.rst new file mode 100644 index 000000000..ed33f62d7 --- /dev/null +++ b/docs/quick_tutorial/unit_testing.rst @@ -0,0 +1,119 @@ +.. _qtut_unit_testing: + +=========================== +05: Unit Tests and ``nose`` +=========================== + +Provide unit testing for our project's Python code. + +Background +========== + +As the mantra says, "Untested code is broken code." The Python +community has had a long culture of writing test scripts which ensure +that your code works correctly as you write it and maintain it in the +future. Pyramid has always had a deep commitment to testing, +with 100% test coverage from the earliest pre-releases. + +Python includes a +:ref:`unit testing framework <python:unittest-minimal-example>` in its +standard library. Over the years a number of Python projects, such as +`nose <https://pypi.python.org/pypi/nose/>`_, have extended this +framework with alternative test runners that provide more convenience +and functionality. The Pyramid developers use ``nose``, which we'll thus +use in this tutorial. + +Don't worry, this tutorial won't be pedantic about "test-driven +development" (TDD.) We'll do just enough to ensure that, in each step, +we haven't majorly broken the code. As you're writing your code you +might find this more convenient than changing to your browser +constantly and clicking reload. + +We'll also leave discussion of +`coverage <https://pypi.python.org/pypi/coverage>`_ for another section. + +Objectives +========== + +- Write unit tests that ensure the quality of our code + +- Install a Python package (``nose``) which helps in our testing + +Steps +===== + +#. First we copy the results of the previous step, as well as install + the ``nose`` package: + + .. code-block:: bash + + $ cd ..; cp -r debugtoolbar unit_testing; cd unit_testing + $ $VENV/bin/python setup.py develop + $ $VENV/bin/easy_install nose + +#. Now we write a simple unit test in ``unit_testing/tutorial/tests.py``: + + .. literalinclude:: unit_testing/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + + $ $VENV/bin/nosetests tutorial + . + ---------------------------------------------------------------------- + Ran 1 test in 0.141s + + OK + +Analysis +======== + +Our ``tests.py`` imports the Python standard unit testing framework. To +make writing Pyramid-oriented tests more convenient, Pyramid supplies +some ``pyramid.testing`` helpers which we use in the test setup and +teardown. Our one test imports the view, makes a dummy request, and sees +if the view returns what we expected. + +The ``tests.TutorialViewTests.test_hello_world`` test is a small +example of a unit test. First, we import the view inside each test. Why +not import at the top, like in normal Python code? Because imports can +cause effects that break a test. We'd like our tests to be in *units*, +hence the name *unit* testing. Each test should isolate itself to the +correct degree. + +Our test then makes a fake incoming web request, then calls our Pyramid +view. We test the HTTP status code on the response to make sure it +matches our expectations. + +Note that our use of ``pyramid.testing.setUp()`` and +``pyramid.testing.tearDown()`` aren't actually necessary here; they are only +necessary when your test needs to make use of the ``config`` object (it's a +Configurator) to add stuff to the configuration state before calling the view. + +Extra Credit +============ + +#. Change the test to assert that the response status code should be + ``404`` (meaning, not found.) Run ``nosetests`` again. Read the + error report and see if you can decipher what it is telling you. + +#. As a more realistic example, put the ``tests.py`` back as you found + it and put an error in your view, such as a reference to a + non-existing variable. Run the tests and see how this is more + convenient than reloading your browser and going back to your code. + +#. Finally, for the most realistic test, read about Pyramid ``Response`` + objects and see how to change the response code. Run the tests and + see how testing confirms the "contract" that your code claims to + support. + +#. How could we add a unit test assertion to test the HTML value of the + response body? + +#. Why do we import the ``hello_world`` view function *inside* the + ``test_hello_world`` method instead of at the top of the module? + +.. seealso:: See Also: :ref:`testing_chapter` diff --git a/docs/quick_tutorial/unit_testing/development.ini b/docs/quick_tutorial/unit_testing/development.ini new file mode 100644 index 000000000..470d92c57 --- /dev/null +++ b/docs/quick_tutorial/unit_testing/development.ini @@ -0,0 +1,40 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/unit_testing/setup.py b/docs/quick_tutorial/unit_testing/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/unit_testing/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/unit_testing/tutorial/__init__.py b/docs/quick_tutorial/unit_testing/tutorial/__init__.py new file mode 100644 index 000000000..2b4e84f30 --- /dev/null +++ b/docs/quick_tutorial/unit_testing/tutorial/__init__.py @@ -0,0 +1,13 @@ +from pyramid.config import Configurator +from pyramid.response import Response + + +def hello_world(request): + return Response('<body><h1>Hello World!</h1></body>') + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('hello', '/') + config.add_view(hello_world, route_name='hello') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/unit_testing/tutorial/tests.py b/docs/quick_tutorial/unit_testing/tutorial/tests.py new file mode 100644 index 000000000..66029b421 --- /dev/null +++ b/docs/quick_tutorial/unit_testing/tutorial/tests.py @@ -0,0 +1,18 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_hello_world(self): + from tutorial import hello_world + + request = testing.DummyRequest() + response = hello_world(request) + self.assertEqual(response.status_code, 200) diff --git a/docs/quick_tutorial/view_classes.rst b/docs/quick_tutorial/view_classes.rst new file mode 100644 index 000000000..58ab43e40 --- /dev/null +++ b/docs/quick_tutorial/view_classes.rst @@ -0,0 +1,98 @@ +.. _qtut_view_classes: + +====================================== +09: Organizing Views With View Classes +====================================== + +Change our view functions to be methods on a view class, +then move some declarations to the class level. + +Background +========== + +So far our views have been simple, free-standing functions. Many times +your views are related: different ways to look at or work on the same +data or a REST API that handles multiple operations. Grouping these +together as a +:ref:`view class <class_as_view>` makes sense: + +- Group views + +- Centralize some repetitive defaults + +- Share some state and helpers + +In this step we just do the absolute minimum to convert the existing +views to a view class. In a later tutorial step we'll examine view +classes in depth. + +Objectives +========== + +- Group related views into a view class + +- Centralize configuration with class-level ``@view_defaults`` + +Steps +===== + + +#. First we copy the results of the previous step: + + .. code-block:: bash + + $ cd ..; cp -r templating view_classes; cd view_classes + $ $VENV/bin/python setup.py develop + +#. Our ``view_classes/tutorial/views.py`` now has a view class with + our two views: + + .. literalinclude:: view_classes/tutorial/views.py + :linenos: + +#. Our unit tests in ``view_classes/tutorial/tests.py`` don't run, + so let's modify the to import the view class and make an instance + before getting a response: + + .. literalinclude:: view_classes/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + + $ $VENV/bin/nosetests tutorial + . + ---------------------------------------------------------------------- + Ran 4 tests in 0.141s + + OK + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy + in your browser. + +Analysis +======== + +To ease the transition to view classes, we didn't introduce any new +functionality. We simply changed the view functions to methods on a +view class, then updated the tests. + +In our ``TutorialViews`` view class you can see that our two view +classes are logically grouped together as methods on a common class. +Since the two views shared the same template, we could move that to a +``@view_defaults`` decorator on at the class level. + +The tests needed to change. Obviously we needed to import the view +class. But you can also see the pattern in the tests of instantiating +the view class with the dummy request first, then calling the view +method being tested. + +.. seealso:: :ref:`class_as_view` diff --git a/docs/quick_tutorial/view_classes/development.ini b/docs/quick_tutorial/view_classes/development.ini new file mode 100644 index 000000000..62e0c5123 --- /dev/null +++ b/docs/quick_tutorial/view_classes/development.ini @@ -0,0 +1,41 @@ +[app:main] +use = egg:tutorial +pyramid.reload_templates = true +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/view_classes/setup.py b/docs/quick_tutorial/view_classes/setup.py new file mode 100644 index 000000000..2221b72e9 --- /dev/null +++ b/docs/quick_tutorial/view_classes/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup + +requires = [ + 'pyramid', + 'pyramid_chameleon' +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/view_classes/tutorial/__init__.py b/docs/quick_tutorial/view_classes/tutorial/__init__.py new file mode 100644 index 000000000..c3e1c9eef --- /dev/null +++ b/docs/quick_tutorial/view_classes/tutorial/__init__.py @@ -0,0 +1,10 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.include('pyramid_chameleon') + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/view_classes/tutorial/home.pt b/docs/quick_tutorial/view_classes/tutorial/home.pt new file mode 100644 index 000000000..a0cc08e7a --- /dev/null +++ b/docs/quick_tutorial/view_classes/tutorial/home.pt @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Quick Tour: ${name}</title> +</head> +<body> +<h1>Hi ${name}</h1> +</body> +</html>
\ No newline at end of file diff --git a/docs/quick_tutorial/view_classes/tutorial/tests.py b/docs/quick_tutorial/view_classes/tutorial/tests.py new file mode 100644 index 000000000..4381235ec --- /dev/null +++ b/docs/quick_tutorial/view_classes/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.home() + self.assertEqual('Home View', response['name']) + + def test_hello(self): + from .views import TutorialViews + + request = testing.DummyRequest() + inst = TutorialViews(request) + response = inst.hello() + self.assertEqual('Hello View', response['name']) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<h1>Hi Home View', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<h1>Hi Hello View', res.body) diff --git a/docs/quick_tutorial/view_classes/tutorial/views.py b/docs/quick_tutorial/view_classes/tutorial/views.py new file mode 100644 index 000000000..58db53c4a --- /dev/null +++ b/docs/quick_tutorial/view_classes/tutorial/views.py @@ -0,0 +1,17 @@ +from pyramid.view import ( + view_config, + view_defaults + ) + +@view_defaults(renderer='home.pt') +class TutorialViews: + def __init__(self, request): + self.request = request + + @view_config(route_name='home') + def home(self): + return {'name': 'Home View'} + + @view_config(route_name='hello') + def hello(self): + return {'name': 'Hello View'} diff --git a/docs/quick_tutorial/views.rst b/docs/quick_tutorial/views.rst new file mode 100644 index 000000000..529bba0a4 --- /dev/null +++ b/docs/quick_tutorial/views.rst @@ -0,0 +1,122 @@ +.. _qtut_views: + +================================= +07: Basic Web Handling With Views +================================= + +Organize a views module with decorators and multiple views. + +Background +========== + +For the examples so far, the ``hello_world`` function is a "view". In +Pyramid, views are the primary way to accept web requests and return +responses. + +So far our examples place everything in one file: + +- The view function + +- Its registration with the configurator + +- The route to map it to a URL + +- The WSGI application launcher + +Let's move the views out to their own ``views.py`` module and change +our startup code to scan that module, looking for decorators that setup +the views. Let's also add a second view and update our tests. + +Objectives +========== + +- Views in a module that is scanned by the configurator + +- Decorators that do declarative configuration + +Steps +===== + +#. Let's begin by using the previous package as a starting point for a + new distribution, then making it active: + + .. code-block:: bash + + $ cd ..; cp -r functional_testing views; cd views + $ $VENV/bin/python setup.py develop + +#. Our ``views/tutorial/__init__.py`` gets a lot shorter: + + .. literalinclude:: views/tutorial/__init__.py + :linenos: + +#. Let's add a module ``views/tutorial/views.py`` that is focused on + handling requests and responses: + + .. literalinclude:: views/tutorial/views.py + :linenos: + +#. Update the tests to cover the two new views: + + .. literalinclude:: views/tutorial/tests.py + :linenos: + +#. Now run the tests: + + .. code-block:: bash + + + $ $VENV/bin/nosetests tutorial + . + ---------------------------------------------------------------------- + Ran 4 tests in 0.141s + + OK + +#. Run your Pyramid application with: + + .. code-block:: bash + + $ $VENV/bin/pserve development.ini --reload + +#. Open http://localhost:6543/ and http://localhost:6543/howdy + in your browser. + +Analysis +======== + +We added some more URLs, but we also removed the view code from the +application startup code in ``tutorial/__init__.py``. +Our views, and their view registrations (via decorators) are now in a +module ``views.py`` which is scanned via ``config.scan('.views')``. + +We have 2 views, each leading to the other. If you start at +http://localhost:6543/, you get a response with a link to the next +view. The ``hello_view`` (available at the URL ``/howdy``) has a link +back to the first view. + +This step also shows that the name appearing in the URL, +the name of the "route" that maps a URL to a view, +and the name of the view, can all be different. More on routes later. + +Earlier we saw ``config.add_view`` as one way to configure a view. This +section introduces ``@view_config``. Pyramid's configuration supports +:term:`imperative configuration`, such as the +``config.add_view`` in the previous example. You can also use +:term:`declarative configuration`, in which a Python +:term:`python:decorator` +is placed on the line above the view. Both approaches result in the +same final configuration, thus usually, it is simply a matter of taste. + +Extra Credit +============ + +#. What does the dot in ``.views`` signify? + +#. Why might ``assertIn`` be a better choice in testing the text in + responses than ``assertEqual``? + +.. seealso:: :ref:`views_chapter`, + :ref:`view_config_chapter`, and + :ref:`debugging_view_configuration` + diff --git a/docs/quick_tutorial/views/development.ini b/docs/quick_tutorial/views/development.ini new file mode 100644 index 000000000..470d92c57 --- /dev/null +++ b/docs/quick_tutorial/views/development.ini @@ -0,0 +1,40 @@ +[app:main] +use = egg:tutorial +pyramid.includes = + pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +host = 0.0.0.0 +port = 6543 + +# Begin logging configuration + +[loggers] +keys = root, tutorial + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/docs/quick_tutorial/views/setup.py b/docs/quick_tutorial/views/setup.py new file mode 100644 index 000000000..9997984d3 --- /dev/null +++ b/docs/quick_tutorial/views/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +requires = [ + 'pyramid', +] + +setup(name='tutorial', + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = tutorial:main + """, +)
\ No newline at end of file diff --git a/docs/quick_tutorial/views/tutorial/__init__.py b/docs/quick_tutorial/views/tutorial/__init__.py new file mode 100644 index 000000000..013d4538f --- /dev/null +++ b/docs/quick_tutorial/views/tutorial/__init__.py @@ -0,0 +1,9 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + config = Configurator(settings=settings) + config.add_route('home', '/') + config.add_route('hello', '/howdy') + config.scan('.views') + return config.make_wsgi_app()
\ No newline at end of file diff --git a/docs/quick_tutorial/views/tutorial/tests.py b/docs/quick_tutorial/views/tutorial/tests.py new file mode 100644 index 000000000..f1757757c --- /dev/null +++ b/docs/quick_tutorial/views/tutorial/tests.py @@ -0,0 +1,44 @@ +import unittest + +from pyramid import testing + + +class TutorialViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_home(self): + from .views import home + + request = testing.DummyRequest() + response = home(request) + self.assertEqual(response.status_code, 200) + self.assertIn(b'Visit', response.body) + + def test_hello(self): + from .views import hello + + request = testing.DummyRequest() + response = hello(request) + self.assertEqual(response.status_code, 200) + self.assertIn(b'Go back', response.body) + + +class TutorialFunctionalTests(unittest.TestCase): + def setUp(self): + from tutorial import main + app = main({}) + from webtest import TestApp + + self.testapp = TestApp(app) + + def test_home(self): + res = self.testapp.get('/', status=200) + self.assertIn(b'<body>Visit', res.body) + + def test_hello(self): + res = self.testapp.get('/howdy', status=200) + self.assertIn(b'<body>Go back', res.body) diff --git a/docs/quick_tutorial/views/tutorial/views.py b/docs/quick_tutorial/views/tutorial/views.py new file mode 100644 index 000000000..6ff149d7b --- /dev/null +++ b/docs/quick_tutorial/views/tutorial/views.py @@ -0,0 +1,14 @@ +from pyramid.response import Response +from pyramid.view import view_config + + +# First view, available at http://localhost:6543/ +@view_config(route_name='home') +def home(request): + return Response('<body>Visit <a href="/howdy">hello</a></body>') + + +# /howdy +@view_config(route_name='hello') +def hello(request): + return Response('<body>Go back <a href="/">home</a></body>') diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 460a852e0..62b1164e3 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -56,10 +56,10 @@ returns one of these values: return ``None``. For example, ``groupfinder('editor', request )`` returns ``['group:editor']``, -``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin', request)`` -returns ``None``. We will use ``groupfinder()`` as an :term:`authentication policy` -"callback" that will provide the :term:`principal` or principals -for a user. +``groupfinder('viewer', request)`` returns ``[]``, and ``groupfinder('admin', +request)`` returns ``None``. We will use ``groupfinder()`` as an +:term:`authentication policy` "callback" that will provide the +:term:`principal` or principals for a user. In a production system, user and group data will most often come from a database, but here we use "dummy" @@ -251,18 +251,6 @@ in ``views.py``. Return a logged_in flag to the renderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add the following line to the import at the head of -``tutorial/tutorial/views.py``: - -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 11-15 - :linenos: - :emphasize-lines: 4 - :language: python - -(Only the highlighted line and a trailing comma on the preceding -line need to be added.) - Add a ``logged_in`` parameter to the return value of ``view_page()``, ``edit_page()`` and ``add_page()``, like this: @@ -274,14 +262,13 @@ like this: return dict(page = page, content = content, edit_url = edit_url, - logged_in = authenticated_userid(request)) + logged_in = request.authenticated_userid) (Only the highlighted line and a trailing comma on the preceding line need to be added.) -:meth:`~pyramid.security.authenticated_userid()` will return ``None`` -if the user is not authenticated, or a user id if the user is -authenticated. +The :meth:`pyramid.request.Request.authenticated_userid` will be ``None`` if +the user is not authenticated, or a user id if the user is authenticated. Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index 77956b1e3..62e96e0e7 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -11,9 +11,9 @@ from pyramid.view import ( from pyramid.security import ( remember, forget, - authenticated_userid, ) + from .security import USERS from .models import Page @@ -45,7 +45,7 @@ def view_page(context, request): edit_url = request.resource_url(context, 'edit_page') return dict(page = context, content = content, edit_url = edit_url, - logged_in = authenticated_userid(request)) + logged_in = request.authenticated_userid) @view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', @@ -65,7 +65,7 @@ def add_page(context, request): page.__parent__ = context return dict(page=page, save_url=save_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(name='edit_page', context='.models.Page', renderer='templates/edit.pt', @@ -77,7 +77,7 @@ def edit_page(context, request): return dict(page=context, save_url=request.resource_url(context, 'edit_page'), - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py index c435a4519..5add04c20 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/tests.py +++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py @@ -158,11 +158,11 @@ class FunctionalTests(unittest.TestCase): def test_FrontPage(self): res = self.testapp.get('/FrontPage', status=200) - self.assertTrue('FrontPage' in res.body) + self.assertTrue(b'FrontPage' in res.body) def test_unexisting_page(self): res = self.testapp.get('/SomePage', status=404) - self.assertTrue('Not Found' in res.body) + self.assertTrue(b'Not Found' in res.body) def test_successful_log_in(self): res = self.testapp.get( self.viewer_login, status=302) @@ -170,48 +170,48 @@ class FunctionalTests(unittest.TestCase): def test_failed_log_in(self): res = self.testapp.get( self.viewer_wrong_login, status=200) - self.assertTrue('login' in res.body) + self.assertTrue(b'login' in res.body) def test_logout_link_present_when_logged_in(self): res = self.testapp.get( self.viewer_login, status=302) res = self.testapp.get('/FrontPage', status=200) - self.assertTrue('Logout' in res.body) + self.assertTrue(b'Logout' in res.body) def test_logout_link_not_present_after_logged_out(self): res = self.testapp.get( self.viewer_login, status=302) res = self.testapp.get('/FrontPage', status=200) res = self.testapp.get('/logout', status=302) - self.assertTrue('Logout' not in res.body) + self.assertTrue(b'Logout' not in res.body) def test_anonymous_user_cannot_edit(self): res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue('Login' in res.body) + self.assertTrue(b'Login' in res.body) def test_anonymous_user_cannot_add(self): res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue('Login' in res.body) + self.assertTrue(b'Login' in res.body) def test_viewer_user_cannot_edit(self): res = self.testapp.get( self.viewer_login, status=302) res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue('Login' in res.body) + self.assertTrue(b'Login' in res.body) def test_viewer_user_cannot_add(self): res = self.testapp.get( self.viewer_login, status=302) res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue('Login' in res.body) + self.assertTrue(b'Login' in res.body) def test_editors_member_user_can_edit(self): res = self.testapp.get( self.editor_login, status=302) res = self.testapp.get('/FrontPage/edit_page', status=200) - self.assertTrue('Editing' in res.body) + self.assertTrue(b'Editing' in res.body) def test_editors_member_user_can_add(self): res = self.testapp.get( self.editor_login, status=302) res = self.testapp.get('/add_page/NewPage', status=200) - self.assertTrue('Editing' in res.body) + self.assertTrue(b'Editing' in res.body) def test_editors_member_user_can_view(self): res = self.testapp.get( self.editor_login, status=302) res = self.testapp.get('/FrontPage', status=200) - self.assertTrue('FrontPage' in res.body) + self.assertTrue(b'FrontPage' in res.body) diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index 77956b1e3..62e96e0e7 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -11,9 +11,9 @@ from pyramid.view import ( from pyramid.security import ( remember, forget, - authenticated_userid, ) + from .security import USERS from .models import Page @@ -45,7 +45,7 @@ def view_page(context, request): edit_url = request.resource_url(context, 'edit_page') return dict(page = context, content = content, edit_url = edit_url, - logged_in = authenticated_userid(request)) + logged_in = request.authenticated_userid) @view_config(name='add_page', context='.models.Wiki', renderer='templates/edit.pt', @@ -65,7 +65,7 @@ def add_page(context, request): page.__parent__ = context return dict(page=page, save_url=save_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(name='edit_page', context='.models.Page', renderer='templates/edit.pt', @@ -77,7 +77,7 @@ def edit_page(context, request): return dict(page=context, save_url=request.resource_url(context, 'edit_page'), - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(context='.models.Wiki', name='login', renderer='templates/login.pt') diff --git a/docs/tutorials/wiki2/authorization.rst b/docs/tutorials/wiki2/authorization.rst index cf20db6d7..1e5d0dcbf 100644 --- a/docs/tutorials/wiki2/authorization.rst +++ b/docs/tutorials/wiki2/authorization.rst @@ -221,7 +221,7 @@ Add the following import statements to the head of ``tutorial/tutorial/views.py``: .. literalinclude:: src/authorization/tutorial/views.py - :lines: 9-16,18,24-25 + :lines: 9-19 :linenos: :emphasize-lines: 3,6-9,11 :language: python @@ -274,17 +274,6 @@ added to ``views.py``. Return a logged_in flag to the renderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add the following line to the import at the head of -``tutorial/tutorial/views.py``: - -.. literalinclude:: src/authorization/tutorial/views.py - :lines: 14-18 - :linenos: - :emphasize-lines: 4 - :language: python - -(Only the highlighted line needs to be added.) - Add a ``logged_in`` parameter to the return value of ``view_page()``, ``edit_page()`` and ``add_page()``, like this: @@ -296,12 +285,12 @@ like this: return dict(page = page, content = content, edit_url = edit_url, - logged_in = authenticated_userid(request)) + logged_in = request.authenticated_userid) (Only the highlighted line needs to be added.) -The :meth:`~pyramid.security.authenticated_userid` method will return None -if the user is not authenticated. +The :meth:`~pyramid.request.Request.authenticated_userid` property will be +``None`` if the user is not authenticated. Add a "Logout" link when logged in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -349,7 +338,7 @@ when we're done: .. literalinclude:: src/authorization/tutorial/views.py :linenos: - :emphasize-lines: 11,14-18,25,31,37,58,61,73,76,88,91-117,119-123 + :emphasize-lines: 11,14-19,25,31,37,58,61,73,76,88,91-117,119-123 :language: python (Only the highlighted lines need to be added.) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py index 092e359ce..23a5f13f4 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/scripts/initializedb.py @@ -33,5 +33,5 @@ def main(argv=sys.argv): DBSession.configure(bind=engine) Base.metadata.create_all(engine) with transaction.manager: - model = Page('FrontPage', 'This is the front page') + model = Page(name='FrontPage', data='This is the front page') DBSession.add(model) diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py index 5dcee127b..9f01d2da5 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/tests.py @@ -14,7 +14,7 @@ def _initTestingDB(): Base.metadata.create_all(engine) DBSession.configure(bind=engine) with transaction.manager: - model = Page('FrontPage', 'This is the front page') + model = Page(name='FrontPage', data='This is the front page') DBSession.add(model) return DBSession @@ -59,7 +59,7 @@ class ViewPageTests(unittest.TestCase): from tutorial.models import Page request = testing.DummyRequest() request.matchdict['pagename'] = 'IDoExist' - page = Page('IDoExist', 'Hello CruelWorld IDoExist') + page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') self.session.add(page) _registerRoutes(self.config) info = self._callFUT(request) @@ -126,7 +126,7 @@ class EditPageTests(unittest.TestCase): _registerRoutes(self.config) request = testing.DummyRequest() request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') + page = Page(name='abc', data='hello') self.session.add(page) info = self._callFUT(request) self.assertEqual(info['page'], page) @@ -139,7 +139,7 @@ class EditPageTests(unittest.TestCase): request = testing.DummyRequest({'form.submitted':True, 'body':'Hello yo!'}) request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') + page = Page(name='abc', data='hello') self.session.add(page) response = self._callFUT(request) self.assertEqual(response.location, 'http://example.com/abc') diff --git a/docs/tutorials/wiki2/src/authorization/tutorial/views.py b/docs/tutorials/wiki2/src/authorization/tutorial/views.py index 0d085b0e2..e954d5a31 100644 --- a/docs/tutorials/wiki2/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki2/src/authorization/tutorial/views.py @@ -14,15 +14,15 @@ from pyramid.view import ( from pyramid.security import ( remember, forget, - authenticated_userid, ) +from .security import USERS + from .models import ( DBSession, Page, ) -from .security import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -55,7 +55,7 @@ def view_page(request): content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) return dict(page=page, content=content, edit_url=edit_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(route_name='add_page', renderer='templates/edit.pt', permission='edit') @@ -63,14 +63,14 @@ def add_page(request): pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: body = request.params['body'] - page = Page(pagename, body) + page = Page(name=pagename, data=body) DBSession.add(page) return HTTPFound(location = request.route_url('view_page', pagename=pagename)) save_url = request.route_url('add_page', pagename=pagename) - page = Page('', '') + page = Page(name='', data='') return dict(page=page, save_url=save_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(route_name='edit_page', renderer='templates/edit.pt', permission='edit') @@ -84,8 +84,8 @@ def edit_page(request): pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=authenticated_userid(request), + save_url=request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid ) @view_config(route_name='login', renderer='templates/login.pt') diff --git a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py index 092e359ce..23a5f13f4 100644 --- a/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/models/tutorial/scripts/initializedb.py @@ -33,5 +33,5 @@ def main(argv=sys.argv): DBSession.configure(bind=engine) Base.metadata.create_all(engine) with transaction.manager: - model = Page('FrontPage', 'This is the front page') + model = Page(name='FrontPage', data='This is the front page') DBSession.add(model) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py index 092e359ce..23a5f13f4 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py @@ -33,5 +33,5 @@ def main(argv=sys.argv): DBSession.configure(bind=engine) Base.metadata.create_all(engine) with transaction.manager: - model = Page('FrontPage', 'This is the front page') + model = Page(name='FrontPage', data='This is the front page') DBSession.add(model) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests.py b/docs/tutorials/wiki2/src/tests/tutorial/tests.py index 3e96d0a82..c50e05b6d 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests.py @@ -15,7 +15,7 @@ def _initTestingDB(): Base.metadata.create_all(engine) DBSession.configure(bind=engine) with transaction.manager: - model = Page('FrontPage', 'This is the front page') + model = Page(name='FrontPage', data='This is the front page') DBSession.add(model) return DBSession @@ -26,27 +26,6 @@ def _registerRoutes(config): config.add_route('add_page', 'add_page/{pagename}') -class PageModelTests(unittest.TestCase): - - def setUp(self): - self.session = _initTestingDB() - - def tearDown(self): - self.session.remove() - - def _getTargetClass(self): - from tutorial.models import Page - return Page - - def _makeOne(self, name='SomeName', data='some data'): - return self._getTargetClass()(name, data) - - def test_constructor(self): - instance = self._makeOne() - self.assertEqual(instance.name, 'SomeName') - self.assertEqual(instance.data, 'some data') - - class ViewWikiTests(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -82,7 +61,7 @@ class ViewPageTests(unittest.TestCase): from tutorial.models import Page request = testing.DummyRequest() request.matchdict['pagename'] = 'IDoExist' - page = Page('IDoExist', 'Hello CruelWorld IDoExist') + page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') self.session.add(page) _registerRoutes(self.config) info = self._callFUT(request) @@ -150,7 +129,7 @@ class EditPageTests(unittest.TestCase): _registerRoutes(self.config) request = testing.DummyRequest() request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') + page = Page(name='abc', data='hello') self.session.add(page) info = self._callFUT(request) self.assertEqual(info['page'], page) @@ -163,7 +142,7 @@ class EditPageTests(unittest.TestCase): request = testing.DummyRequest({'form.submitted':True, 'body':'Hello yo!'}) request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') + page = Page(name='abc', data='hello') self.session.add(page) response = self._callFUT(request) self.assertEqual(response.location, 'http://example.com/abc') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/views.py b/docs/tutorials/wiki2/src/tests/tutorial/views.py index 0d085b0e2..41bea4785 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/views.py @@ -14,15 +14,15 @@ from pyramid.view import ( from pyramid.security import ( remember, forget, - authenticated_userid, ) +from .security import USERS + from .models import ( DBSession, Page, ) -from .security import USERS # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") @@ -55,7 +55,7 @@ def view_page(request): content = wikiwords.sub(check, content) edit_url = request.route_url('edit_page', pagename=pagename) return dict(page=page, content=content, edit_url=edit_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(route_name='add_page', renderer='templates/edit.pt', permission='edit') @@ -63,14 +63,14 @@ def add_page(request): pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: body = request.params['body'] - page = Page(pagename, body) + page = Page(name=pagename, data=body) DBSession.add(page) return HTTPFound(location = request.route_url('view_page', pagename=pagename)) save_url = request.route_url('add_page', pagename=pagename) - page = Page('', '') + page = Page(name='', data='') return dict(page=page, save_url=save_url, - logged_in=authenticated_userid(request)) + logged_in=request.authenticated_userid) @view_config(route_name='edit_page', renderer='templates/edit.pt', permission='edit') @@ -84,8 +84,8 @@ def edit_page(request): pagename=pagename)) return dict( page=page, - save_url = request.route_url('edit_page', pagename=pagename), - logged_in=authenticated_userid(request), + save_url=request.route_url('edit_page', pagename=pagename), + logged_in=request.authenticated_userid ) @view_config(route_name='login', renderer='templates/login.pt') @@ -121,4 +121,3 @@ def logout(request): headers = forget(request) return HTTPFound(location = request.route_url('view_wiki'), headers = headers) - diff --git a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py index 092e359ce..23a5f13f4 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/views/tutorial/scripts/initializedb.py @@ -33,5 +33,5 @@ def main(argv=sys.argv): DBSession.configure(bind=engine) Base.metadata.create_all(engine) with transaction.manager: - model = Page('FrontPage', 'This is the front page') + model = Page(name='FrontPage', data='This is the front page') DBSession.add(model) diff --git a/docs/tutorials/wiki2/src/views/tutorial/tests.py b/docs/tutorials/wiki2/src/views/tutorial/tests.py index 5dcee127b..9f01d2da5 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/tests.py +++ b/docs/tutorials/wiki2/src/views/tutorial/tests.py @@ -14,7 +14,7 @@ def _initTestingDB(): Base.metadata.create_all(engine) DBSession.configure(bind=engine) with transaction.manager: - model = Page('FrontPage', 'This is the front page') + model = Page(name='FrontPage', data='This is the front page') DBSession.add(model) return DBSession @@ -59,7 +59,7 @@ class ViewPageTests(unittest.TestCase): from tutorial.models import Page request = testing.DummyRequest() request.matchdict['pagename'] = 'IDoExist' - page = Page('IDoExist', 'Hello CruelWorld IDoExist') + page = Page(name='IDoExist', data='Hello CruelWorld IDoExist') self.session.add(page) _registerRoutes(self.config) info = self._callFUT(request) @@ -126,7 +126,7 @@ class EditPageTests(unittest.TestCase): _registerRoutes(self.config) request = testing.DummyRequest() request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') + page = Page(name='abc', data='hello') self.session.add(page) info = self._callFUT(request) self.assertEqual(info['page'], page) @@ -139,7 +139,7 @@ class EditPageTests(unittest.TestCase): request = testing.DummyRequest({'form.submitted':True, 'body':'Hello yo!'}) request.matchdict = {'pagename':'abc'} - page = Page('abc', 'hello') + page = Page(name='abc', data='hello') self.session.add(page) response = self._callFUT(request) self.assertEqual(response.location, 'http://example.com/abc') diff --git a/docs/tutorials/wiki2/src/views/tutorial/views.py b/docs/tutorials/wiki2/src/views/tutorial/views.py index d54b2a7aa..b41d4ab40 100644 --- a/docs/tutorials/wiki2/src/views/tutorial/views.py +++ b/docs/tutorials/wiki2/src/views/tutorial/views.py @@ -48,12 +48,12 @@ def add_page(request): pagename = request.matchdict['pagename'] if 'form.submitted' in request.params: body = request.params['body'] - page = Page(pagename, body) + page = Page(name=pagename, data=body) DBSession.add(page) return HTTPFound(location = request.route_url('view_page', pagename=pagename)) save_url = request.route_url('add_page', pagename=pagename) - page = Page('', '') + page = Page(name='', data='') return dict(page=page, save_url=save_url) @view_config(route_name='edit_page', renderer='templates/edit.pt') diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 454ebd4b2..2c301bd29 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -424,7 +424,9 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): ``secret`` - The secret (a string) used for auth_tkt cookie signing. + The secret (a string) used for auth_tkt cookie signing. This value + should be unique across all values provided to Pyramid for various + subsystem secrets (see :ref:`admonishment_against_secret_sharing`). Required. ``callback`` @@ -1176,10 +1178,19 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): return None if authmeth.lower() != 'basic': return None + try: - auth = b64decode(auth.strip()).decode('ascii') + authbytes = b64decode(auth.strip()) except (TypeError, binascii.Error): # can't decode return None + + # try utf-8 first, then latin-1; see discussion in + # https://github.com/Pylons/pyramid/issues/898 + try: + auth = authbytes.decode('utf-8') + except UnicodeDecodeError: + auth = authbytes.decode('latin-1') + try: username, password = auth.split(':', 1) except ValueError: # not enough values to unpack diff --git a/pyramid/authorization.py b/pyramid/authorization.py index 1fd05e244..5e7baa19d 100644 --- a/pyramid/authorization.py +++ b/pyramid/authorization.py @@ -122,6 +122,9 @@ class ACLAuthorizationPolicy(object): allowed_here = set() denied_here = set() + if acl and callable(acl): + acl = acl() + for ace_action, ace_principal, ace_permissions in acl: if not is_nonstr_iter(ace_permissions): ace_permissions = [ace_permissions] diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index c8f66e83d..967f2eeee 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -13,7 +13,6 @@ from pyramid.traversal import ( from pyramid.urldispatch import _compile_route from pyramid.util import object_description from pyramid.session import check_csrf_token -from pyramid.security import effective_principals from .util import as_sorted_tuple @@ -288,7 +287,7 @@ class EffectivePrincipalsPredicate(object): phash = text def __call__(self, context, request): - req_principals = effective_principals(request) + req_principals = request.effective_principals if is_nonstr_iter(req_principals): rpset = set(req_principals) if self.val.issubset(rpset): diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 9dca9e51e..4fd207600 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -36,7 +36,6 @@ class RoutesConfiguratorMixin(object): request_param=None, traverse=None, custom_predicates=(), - renderer=None, use_global_views=False, path=None, pregenerator=None, @@ -238,10 +237,10 @@ class RoutesConfiguratorMixin(object): If specified, this value should be a :term:`principal` identifier or a sequence of principal identifiers. If the - :func:`pyramid.security.effective_principals` method indicates that - every principal named in the argument list is present in the current - request, this predicate will return True; otherwise it will return - False. For example: + :attr:`pyramid.request.Request.effective_principals` property + indicates that every principal named in the argument list is present + in the current request, this predicate will return True; otherwise it + will return False. For example: ``effective_principals=pyramid.security.Authenticated`` or ``effective_principals=('fred', 'group:admins')``. diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py index 2ab85b1f5..5df726a31 100644 --- a/pyramid/config/testing.py +++ b/pyramid/config/testing.py @@ -47,14 +47,14 @@ class TestingConfiguratorMixin(object): ``groupids`` argument. The authentication policy will return the userid identifier implied by the ``userid`` argument and the group ids implied by the ``groupids`` argument when the - :func:`pyramid.security.authenticated_userid` or - :func:`pyramid.security.effective_principals` APIs are + :attr:`pyramid.request.Request.authenticated_userid` or + :attr:`pyramid.request.Request.effective_principals` APIs are used. This function is most useful when testing code that uses - the APIs named :func:`pyramid.security.has_permission`, - :func:`pyramid.security.authenticated_userid`, - :func:`pyramid.security.effective_principals`, and + the APIs named :meth:`pyramid.request.Request.has_permission`, + :attr:`pyramid.request.Request.authenticated_userid`, + :attr:`pyramid.request.Request.effective_principals`, and :func:`pyramid.security.principals_allowed_by_permission`. .. versionadded:: 1.4 diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 233bbac12..a3f885504 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1017,10 +1017,10 @@ class ViewsConfiguratorMixin(object): If specified, this value should be a :term:`principal` identifier or a sequence of principal identifiers. If the - :func:`pyramid.security.effective_principals` method indicates that - every principal named in the argument list is present in the current - request, this predicate will return True; otherwise it will return - False. For example: + :attr:`pyramid.request.Request.effective_principals` property + indicates that every principal named in the argument list is present + in the current request, this predicate will return True; otherwise it + will return False. For example: ``effective_principals=pyramid.security.Authenticated`` or ``effective_principals=('fred', 'group:admins')``. @@ -1550,6 +1550,7 @@ class ViewsConfiguratorMixin(object): return deriver(view) + @viewdefaults @action_method def add_forbidden_view( self, @@ -1629,6 +1630,7 @@ class ViewsConfiguratorMixin(object): set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias + @viewdefaults @action_method def add_notfound_view( self, diff --git a/pyramid/encode.py b/pyramid/encode.py index 65bc95032..9e190bc21 100644 --- a/pyramid/encode.py +++ b/pyramid/encode.py @@ -32,6 +32,10 @@ def urlencode(query, doseq=True): See the Python stdlib documentation for ``urllib.urlencode`` for more information. + + .. versionchanged:: 1.5 + In a key/value pair, if the value is ``None`` then it will be + dropped from the resulting output. """ try: # presumed to be a dictionary @@ -50,6 +54,8 @@ def urlencode(query, doseq=True): x = _enc(x) result += '%s%s=%s' % (prefix, k, x) prefix = '&' + elif v is None: + result += '%s%s=' % (prefix, k) else: v = _enc(v) result += '%s%s=%s' % (prefix, k, v) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index a8fca1d84..c59d109df 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,4 +1,5 @@ from pyramid.httpexceptions import ( + HTTPBadRequest, HTTPNotFound, HTTPForbidden, ) @@ -8,6 +9,20 @@ Forbidden = HTTPForbidden # bw compat CR = '\n' +class BadCSRFToken(HTTPBadRequest): + """ + This exception indicates the request has failed cross-site request + forgery token validation. + """ + title = 'Bad CSRF Token' + explanation = ( + 'Access is denied. This server can not verify that your cross-site ' + 'request forgery token belongs to your login session. Either you ' + 'supplied the wrong cross-site request forgery token or your session ' + 'no longer exists. This may be due to session timeout or because ' + 'browser is not supplying the credentials required, as can happen ' + 'when the browser has cookies turned off.') + class PredicateMismatch(HTTPNotFound): """ This exception is raised by multiviews when no view matches diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index fff17b2df..ebee39ada 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -565,6 +565,14 @@ class HTTPClientError(HTTPError): 'it is either malformed or otherwise incorrect.') class HTTPBadRequest(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the body or headers failed validity checks, + preventing the server from being able to continue processing. + + code: 400, title: Bad Request + """ pass class HTTPUnauthorized(HTTPClientError): diff --git a/pyramid/i18n.py b/pyramid/i18n.py index cdedbc877..6ffd93e8f 100644 --- a/pyramid/i18n.py +++ b/pyramid/i18n.py @@ -107,7 +107,8 @@ def default_locale_negotiator(request): - First, the negotiator looks for the ``_LOCALE_`` attribute of the request object (possibly set by a view or a listener for an - :term:`event`). + :term:`event`). If the attribute exists and it is not ``None``, + its value will be used. - Then it looks for the ``request.params['_LOCALE_']`` value. diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 85b2227b4..cf651cf1e 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -367,9 +367,29 @@ class IBeforeRender(IDict): '``render`` method for this rendering. ' 'This feature is new in Pyramid 1.2.') +class IRendererInfo(Interface): + """ An object implementing this interface is passed to every + :term:`renderer factory` constructor as its only argument (conventionally + named ``info``)""" + name = Attribute('The value passed by the user as the renderer name') + package = Attribute('The "current package" when the renderer ' + 'configuration statement was found') + type = Attribute('The renderer type name') + registry = Attribute('The "current" application registry when the ' + 'renderer was created') + settings = Attribute('The deployment settings dictionary related ' + 'to the current application') + +class IRendererFactory(Interface): + def __call__(info): + """ Return an object that implements + :class:`pyramid.interfaces.IRenderer`. ``info`` is an + object that implements :class:`pyramid.interfaces.IRendererInfo`. + """ + class IRenderer(Interface): def __call__(value, system): - """ Call a the renderer implementation with the result of the + """ Call the renderer with the result of the view (``value``) passed in and return a result (a string or unicode object useful as a response body). Values computed by the system are passed by the system in the ``system`` @@ -387,6 +407,13 @@ class ITemplateRenderer(IRenderer): accepts arbitrary keyword arguments and returns a string or unicode object """ +deprecated( + 'ITemplateRenderer', + 'As of Pyramid 1.5 the, "pyramid.interfaces.ITemplateRenderer" interface ' + 'is scheduled to be removed. It was used by the Mako and Chameleon ' + 'renderers which have been split into their own packages.' + ) + class IViewMapper(Interface): def __call__(self, object): """ Provided with an arbitrary object (a function, class, or @@ -611,17 +638,13 @@ class ITraverser(Interface): ITraverserFactory = ITraverser # b / c for 1.0 code -class IRendererFactory(Interface): - def __call__(info): - """ Return an object that implements ``IRenderer``. ``info`` is an - object that implement ``IRendererInfo``. """ - class IViewPermission(Interface): def __call__(context, request): - """ Return True if the permission allows, return False if it denies. """ + """ Return True if the permission allows, return False if it denies. + """ class IRouter(Interface): - """WSGI application which routes requests to 'view' code based on + """ WSGI application which routes requests to 'view' code based on a view registry.""" registry = Attribute( """Component architecture registry local to this application.""") @@ -932,20 +955,6 @@ class ISession(IDict): returned. """ -class IRendererInfo(Interface): - """ An object implementing this interface is passed to every - :term:`renderer factory` constructor as its only argument (conventionally - named ``info``)""" - name = Attribute('The value passed by the user as the renderer name') - package = Attribute('The "current package" when the renderer ' - 'configuration statement was found') - type = Attribute('The renderer type name') - registry = Attribute('The "current" application registry when the ' - 'renderer was created') - settings = Attribute('The deployment settings dictionary related ' - 'to the current application') - - class IIntrospector(Interface): def get(category_name, discriminator, default=None): """ Get the IIntrospectable related to the category_name and the diff --git a/pyramid/request.py b/pyramid/request.py index 2cf0613f7..188e968ac 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -21,6 +21,10 @@ from pyramid.compat import ( from pyramid.decorator import reify from pyramid.i18n import LocalizerRequestMixin from pyramid.response import Response +from pyramid.security import ( + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ) from pyramid.url import URLMethodsMixin from pyramid.util import InstancePropertyMixin @@ -136,8 +140,15 @@ class CallbackMethodsMixin(object): callback(self) @implementer(IRequest) -class Request(BaseRequest, URLMethodsMixin, CallbackMethodsMixin, - InstancePropertyMixin, LocalizerRequestMixin): +class Request( + BaseRequest, + URLMethodsMixin, + CallbackMethodsMixin, + InstancePropertyMixin, + LocalizerRequestMixin, + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ): """ A subclass of the :term:`WebOb` Request class. An instance of this class is created by the :term:`router` and is provided to a diff --git a/pyramid/scaffolds/starter/+package+/static/pyramid-small.png b/pyramid/scaffolds/starter/+package+/static/pyramid-small.png Binary files differdeleted file mode 100644 index a5bc0ade7..000000000 --- a/pyramid/scaffolds/starter/+package+/static/pyramid-small.png +++ /dev/null diff --git a/pyramid/scaffolds/zodb/+package+/static/pyramid-small.png b/pyramid/scaffolds/zodb/+package+/static/pyramid-small.png Binary files differdeleted file mode 100644 index a5bc0ade7..000000000 --- a/pyramid/scaffolds/zodb/+package+/static/pyramid-small.png +++ /dev/null diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index ba4eb0856..9a3b53b33 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -77,8 +77,8 @@ class PCreateCommand(object): def render_scaffolds(self): options = self.options args = self.args - project_name = os.path.basename(args[0]) output_dir = os.path.abspath(os.path.normpath(args[0])) + project_name = os.path.basename(os.path.split(output_dir)[1]) pkg_name = _bad_chars_re.sub('', project_name.lower()) safe_name = pkg_resources.safe_name(project_name) egg_name = pkg_resources.to_filename(safe_name) @@ -123,4 +123,5 @@ class PCreateCommand(object): if not self.quiet: print(msg) - +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/pyramid/scripts/pdistreport.py b/pyramid/scripts/pdistreport.py index 10edb5715..61098dc27 100644 --- a/pyramid/scripts/pdistreport.py +++ b/pyramid/scripts/pdistreport.py @@ -35,3 +35,6 @@ def main(argv=sys.argv, pkg_resources=pkg_resources, platform=platform.platform, for package in packages: out(' ', package['name'], package['version']) out(' ', package['location']) + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py index 8628d5a5a..2ab3b8bb9 100644 --- a/pyramid/scripts/prequest.py +++ b/pyramid/scripts/prequest.py @@ -175,3 +175,6 @@ class PRequestCommand(object): else: self.out(response.body) return 0 + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/pyramid/scripts/proutes.py b/pyramid/scripts/proutes.py index 49e19deca..5784026bb 100644 --- a/pyramid/scripts/proutes.py +++ b/pyramid/scripts/proutes.py @@ -82,3 +82,5 @@ class PRoutesCommand(object): self.out(fmt % (route.name, pattern, view_callable)) return 0 +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 8cceecbb3..03cbc23ab 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -984,3 +984,6 @@ def cherrypy_server_runner( server.stop() return server + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index f74402928..dd09bf457 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -217,3 +217,5 @@ class PShellCommand(object): IPShell() return shell +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/pyramid/scripts/ptweens.py b/pyramid/scripts/ptweens.py index 5fe2fa120..ad52d5d8f 100644 --- a/pyramid/scripts/ptweens.py +++ b/pyramid/scripts/ptweens.py @@ -87,3 +87,6 @@ class PTweensCommand(object): self.out('') self.show_chain(tweens.implicit()) return 0 + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index 081c13e9d..5e92ec4e6 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -4,6 +4,7 @@ import textwrap from pyramid.interfaces import IMultiView from pyramid.paster import bootstrap +from pyramid.request import Request from pyramid.scripts.common import parse_vars def main(argv=sys.argv, quiet=False): @@ -52,7 +53,7 @@ class PViewsCommand(object): infos.append(info) return infos - def _find_view(self, url, registry): + def _find_view(self, request): """ Accept ``url`` and ``registry``; create a :term:`request` and find a :app:`Pyramid` view based on introspection of :term:`view @@ -63,22 +64,19 @@ class PViewsCommand(object): from pyramid.interfaces import IRequest from pyramid.interfaces import IRootFactory from pyramid.interfaces import IRouteRequest - from pyramid.interfaces import IRequestFactory from pyramid.interfaces import IRoutesMapper from pyramid.interfaces import IView from pyramid.interfaces import IViewClassifier from pyramid.interfaces import ITraverser - from pyramid.request import Request from pyramid.traversal import DefaultRootFactory from pyramid.traversal import ResourceTreeTraverser + registry = request.registry q = registry.queryUtility root_factory = q(IRootFactory, default=DefaultRootFactory) routes_mapper = q(IRoutesMapper) - request_factory = q(IRequestFactory, default=Request) adapters = registry.adapters - request = None @implementer(IMultiView) class RoutesMultiView(object): @@ -111,20 +109,9 @@ class PViewsCommand(object): view.__view_attr__ = '' self.views.append((None, view, None)) - - # create the request - environ = { - 'wsgi.url_scheme':'http', - 'SERVER_NAME':'localhost', - 'SERVER_PORT':'8080', - 'REQUEST_METHOD':'GET', - 'PATH_INFO':url, - } - request = request_factory(environ) context = None routes_multiview = None attrs = request.__dict__ - attrs['registry'] = registry request_iface = IRequest # find the root object @@ -236,9 +223,10 @@ class PViewsCommand(object): if not url.startswith('/'): url = '/%s' % url - env = self.bootstrap[0](config_uri, options=parse_vars(self.args[2:])) - registry = env['registry'] - view = self._find_view(url, registry) + request = Request.blank(url) + env = self.bootstrap[0](config_uri, options=parse_vars(self.args[2:]), + request=request) + view = self._find_view(request) self.out('') self.out("URL = %s" % url) self.out('') @@ -257,5 +245,8 @@ class PViewsCommand(object): else: self.out(" Not found.") self.out('') + env['closer']() return 0 +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/pyramid/security.py b/pyramid/security.py index 3e25f9b2f..58fa9332a 100644 --- a/pyramid/security.py +++ b/pyramid/security.py @@ -1,3 +1,4 @@ +from zope.deprecation import deprecated from zope.interface import providedBy from pyramid.interfaces import ( @@ -30,79 +31,143 @@ DENY_ALL = (Deny, Everyone, ALL_PERMISSIONS) NO_PERMISSION_REQUIRED = '__no_permission_required__' -def has_permission(permission, context, request): - """ Provided a permission (a string or unicode object), a context - (a :term:`resource` instance) and a request object, return an - instance of :data:`pyramid.security.Allowed` if the permission - is granted in this context to the user implied by the - request. Return an instance of :mod:`pyramid.security.Denied` - if this permission is not granted in this context to this user. - This function delegates to the current authentication and - authorization policies. Return - :data:`pyramid.security.Allowed` unconditionally if no - authentication policy has been configured in this application.""" +def _get_registry(request): try: reg = request.registry except AttributeError: reg = get_current_registry() # b/c - authn_policy = reg.queryUtility(IAuthenticationPolicy) - if authn_policy is None: - return Allowed('No authentication policy in use.') + return reg - authz_policy = reg.queryUtility(IAuthorizationPolicy) - if authz_policy is None: - raise ValueError('Authentication policy registered without ' - 'authorization policy') # should never happen - principals = authn_policy.effective_principals(request) - return authz_policy.permits(context, principals, permission) +def _get_authentication_policy(request): + registry = _get_registry(request) + return registry.queryUtility(IAuthenticationPolicy) -def authenticated_userid(request): - """ Return the userid of the currently authenticated user or - ``None`` if there is no :term:`authentication policy` in effect or - there is no currently authenticated user.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c +def has_permission(permission, context, request): + """ + A function that calls + :meth:`pyramid.request.Request.has_permission` and returns its result. + + .. deprecated:: 1.5 + Use :meth:`pyramid.request.Request.has_permission` instead. + + .. versionchanged:: 1.5a3 + If context is None, then attempt to use the context attribute + of self, if not set then the AttributeError is propergated. + """ + return request.has_permission(permission, context) + +deprecated( + 'has_permission', + 'As of Pyramid 1.5 the "pyramid.security.has_permission" API is now ' + 'deprecated. It will be removed in Pyramd 1.8. Use the ' + '"has_permission" method of the Pyramid request instead.' + ) - policy = reg.queryUtility(IAuthenticationPolicy) - if policy is None: - return None - return policy.authenticated_userid(request) + +def authenticated_userid(request): + """ + A function that returns the value of the property + :attr:`pyramid.request.Request.authenticated_userid`. + + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.authenticated_userid` instead. + """ + return request.authenticated_userid + +deprecated( + 'authenticated_userid', + 'As of Pyramid 1.5 the "pyramid.security.authenticated_userid" API is now ' + 'deprecated. It will be removed in Pyramd 1.8. Use the ' + '"authenticated_userid" attribute of the Pyramid request instead.' + ) def unauthenticated_userid(request): - """ Return an object which represents the *claimed* (not verified) user - id of the credentials present in the request. ``None`` if there is no - :term:`authentication policy` in effect or there is no user data - associated with the current request. This differs from - :func:`~pyramid.security.authenticated_userid`, because the effective - authentication policy will not ensure that a record associated with the - userid exists in persistent storage.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c + """ + A function that returns the value of the property + :attr:`pyramid.request.Request.unauthenticated_userid`. + + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.unauthenticated_userid` instead. + """ + return request.unauthenticated_userid + +deprecated( + 'unauthenticated_userid', + 'As of Pyramid 1.5 the "pyramid.security.unauthenticated_userid" API is ' + 'now deprecated. It will be removed in Pyramd 1.8. Use the ' + '"unauthenticated_userid" attribute of the Pyramid request instead.' + ) + +def effective_principals(request): + """ + A function that returns the value of the property + :attr:`pyramid.request.Request.effective_principals`. + + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.effective_principals` instead. + """ + return request.effective_principals + +deprecated( + 'effective_principals', + 'As of Pyramid 1.5 the "pyramid.security.effective_principals" API is ' + 'now deprecated. It will be removed in Pyramd 1.8. Use the ' + '"effective_principals" attribute of the Pyramid request instead.' + ) + +def remember(request, principal, **kw): + """ + Returns a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``) + on this request's response. + These headers are suitable for 'remembering' a set of credentials + implied by the data passed as ``principal`` and ``*kw`` using the + current :term:`authentication policy`. Common usage might look + like so within the body of a view function (``response`` is + assumed to be a :term:`WebOb` -style :term:`response` object + computed previously by the view code):: - policy = reg.queryUtility(IAuthenticationPolicy) + .. code-block:: python + + from pyramid.security import remember + headers = remember(request, 'chrism', password='123', max_age='86400') + response = request.response + response.headerlist.extend(headers) + return response + + If no :term:`authentication policy` is in use, this function will + always return an empty sequence. If used, the composition and + meaning of ``**kw`` must be agreed upon by the calling code and + the effective authentication policy. + """ + policy = _get_authentication_policy(request) if policy is None: - return None - return policy.unauthenticated_userid(request) + return [] + return policy.remember(request, principal, **kw) -def effective_principals(request): - """ Return the list of 'effective' :term:`principal` identifiers - for the ``request``. This will include the userid of the - currently authenticated user if a user is currently - authenticated. If no :term:`authentication policy` is in effect, - this will return an empty sequence.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c +def forget(request): + """ + Return a sequence of header tuples (e.g. ``[('Set-Cookie', + 'foo=abc')]``) suitable for 'forgetting' the set of credentials + possessed by the currently authenticated user. A common usage + might look like so within the body of a view function + (``response`` is assumed to be an :term:`WebOb` -style + :term:`response` object computed previously by the view code):: + + from pyramid.security import forget + headers = forget(request) + response.headerlist.extend(headers) + return response - policy = reg.queryUtility(IAuthenticationPolicy) + If no :term:`authentication policy` is in use, this function will + always return an empty sequence. + + .. deprecated:: 1.5 + Use :meth:`pyramid.request.Request.get_logout_headers` instead. + """ + policy = _get_authentication_policy(request) if policy is None: - return [Everyone] - return policy.effective_principals(request) + return [] + return policy.forget(request) def principals_allowed_by_permission(context, permission): """ Provided a ``context`` (a resource object), and a ``permission`` @@ -140,10 +205,7 @@ def view_execution_permitted(context, request, name=''): An exception is raised if no view is found. """ - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c + reg = _get_registry(request) provides = [IViewClassifier] + map_(providedBy, (request, context)) view = reg.adapters.lookup(provides, ISecuredView, name=name) if view is None: @@ -157,58 +219,6 @@ def view_execution_permitted(context, request, name=''): (name, context)) return view.__permitted__(context, request) -def remember(request, principal, **kw): - """ Return a sequence of header tuples (e.g. ``[('Set-Cookie', - 'foo=abc')]``) suitable for 'remembering' a set of credentials - implied by the data passed as ``principal`` and ``*kw`` using the - current :term:`authentication policy`. Common usage might look - like so within the body of a view function (``response`` is - assumed to be a :term:`WebOb` -style :term:`response` object - computed previously by the view code):: - - from pyramid.security import remember - headers = remember(request, 'chrism', password='123', max_age='86400') - response.headerlist.extend(headers) - return response - - If no :term:`authentication policy` is in use, this function will - always return an empty sequence. If used, the composition and - meaning of ``**kw`` must be agreed upon by the calling code and - the effective authentication policy.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c - policy = reg.queryUtility(IAuthenticationPolicy) - if policy is None: - return [] - else: - return policy.remember(request, principal, **kw) - -def forget(request): - """ Return a sequence of header tuples (e.g. ``[('Set-Cookie', - 'foo=abc')]``) suitable for 'forgetting' the set of credentials - possessed by the currently authenticated user. A common usage - might look like so within the body of a view function - (``response`` is assumed to be an :term:`WebOb` -style - :term:`response` object computed previously by the view code):: - - from pyramid.security import forget - headers = forget(request) - response.headerlist.extend(headers) - return response - - If no :term:`authentication policy` is in use, this function will - always return an empty sequence.""" - try: - reg = request.registry - except AttributeError: - reg = get_current_registry() # b/c - policy = reg.queryUtility(IAuthenticationPolicy) - if policy is None: - return [] - else: - return policy.forget(request) class PermitsResult(int): def __new__(cls, s, *args): @@ -294,3 +304,89 @@ class ACLAllowed(ACLPermitsResult): summary is available as the ``msg`` attribute.""" boolval = 1 +class AuthenticationAPIMixin(object): + + def _get_authentication_policy(self): + reg = _get_registry(self) + return reg.queryUtility(IAuthenticationPolicy) + + @property + def authenticated_userid(self): + """ Return the userid of the currently authenticated user or + ``None`` if there is no :term:`authentication policy` in effect or + there is no currently authenticated user. + + .. versionadded:: 1.5 + """ + policy = self._get_authentication_policy() + if policy is None: + return None + return policy.authenticated_userid(self) + + @property + def unauthenticated_userid(self): + """ Return an object which represents the *claimed* (not verified) user + id of the credentials present in the request. ``None`` if there is no + :term:`authentication policy` in effect or there is no user data + associated with the current request. This differs from + :attr:`~pyramid.request.Request.authenticated_userid`, because the + effective authentication policy will not ensure that a record + associated with the userid exists in persistent storage. + + .. versionadded:: 1.5 + """ + policy = self._get_authentication_policy() + if policy is None: + return None + return policy.unauthenticated_userid(self) + + @property + def effective_principals(self): + """ Return the list of 'effective' :term:`principal` identifiers + for the ``request``. This will include the userid of the + currently authenticated user if a user is currently + authenticated. If no :term:`authentication policy` is in effect, + this will return an empty sequence. + + .. versionadded:: 1.5 + """ + policy = self._get_authentication_policy() + if policy is None: + return [Everyone] + return policy.effective_principals(self) + +class AuthorizationAPIMixin(object): + + def has_permission(self, permission, context=None): + """ Given a permission and an optional context, returns an instance of + :data:`pyramid.security.Allowed` if the permission is granted to this + request with the provided context, or the context already associated + with the request. Otherwise, returns an instance of + :data:`pyramid.security.Denied`. This method delegates to the current + authentication and authorization policies. Returns + :data:`pyramid.security.Allowed` unconditionally if no authentication + policy has been registered for this request. If ``context`` is not + supplied or is supplied as ``None``, the context used is the + ``request.context`` attribute. + + :param permission: Does this request have the given permission? + :type permission: unicode, str + :param context: A resource object or ``None`` + :type context: object + :returns: `pyramid.security.PermitsResult` + + .. versionadded:: 1.5 + + """ + if context is None: + context = self.context + reg = _get_registry(self) + authn_policy = reg.queryUtility(IAuthenticationPolicy) + if authn_policy is None: + return Allowed('No authentication policy in use.') + authz_policy = reg.queryUtility(IAuthorizationPolicy) + if authz_policy is None: + raise ValueError('Authentication policy registered without ' + 'authorization policy') # should never happen + principals = authn_policy.effective_principals(self) + return authz_policy.permits(context, principals, permission) diff --git a/pyramid/session.py b/pyramid/session.py index 3708ef879..d3a4113b9 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -1,10 +1,11 @@ -from hashlib import sha1 import base64 import binascii +import hashlib import hmac -import time import os +import time +from zope.deprecation import deprecated from zope.interface import implementer from pyramid.compat import ( @@ -15,25 +16,31 @@ from pyramid.compat import ( native_, ) -from pyramid.httpexceptions import HTTPBadRequest +from pyramid.exceptions import BadCSRFToken from pyramid.interfaces import ISession from pyramid.util import strings_differ def manage_accessed(wrapped): - """ Decorator which causes a cookie to be set when a wrapped - method is called""" + """ Decorator which causes a cookie to be renewed when an accessor + method is called.""" def accessed(session, *arg, **kw): - session.accessed = int(time.time()) - if not session._dirty: - session._dirty = True - def set_cookie_callback(request, response): - session._set_cookie(response) - session.request = None # explicitly break cycle for gc - session.request.add_response_callback(set_cookie_callback) + session.accessed = now = int(time.time()) + if now - session.renewed > session._reissue_time: + session.changed() return wrapped(session, *arg, **kw) accessed.__doc__ = wrapped.__doc__ return accessed +def manage_changed(wrapped): + """ Decorator which causes a cookie to be set when a setter method + is called.""" + def changed(session, *arg, **kw): + session.accessed = int(time.time()) + session.changed() + return wrapped(session, *arg, **kw) + changed.__doc__ = wrapped.__doc__ + return changed + def signed_serialize(data, secret): """ Serialize any pickleable structure (``data``) and sign it using the ``secret`` (must be a string). Return the @@ -48,7 +55,7 @@ def signed_serialize(data, secret): response.set_cookie('signed_cookie', cookieval) """ pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + sig = hmac.new(bytes_(secret), pickled, hashlib.sha1).hexdigest() return sig + native_(base64.b64encode(pickled)) def signed_deserialize(serialized, secret, hmac=hmac): @@ -66,13 +73,13 @@ def signed_deserialize(serialized, secret, hmac=hmac): """ # hmac parameterized only for unit tests try: - input_sig, pickled = (serialized[:40], + input_sig, pickled = (bytes_(serialized[:40]), base64.b64decode(bytes_(serialized[40:]))) except (binascii.Error, TypeError) as e: # Badly formed data can make base64 die raise ValueError('Badly formed base64 data: %s' % e) - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + sig = bytes_(hmac.new(bytes_(secret), pickled, hashlib.sha1).hexdigest()) # Avoid timing attacks (see # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) @@ -95,7 +102,7 @@ def check_csrf_token(request, If the value supplied by param or by header doesn't match the value supplied by ``request.session.get_csrf_token()``, and ``raises`` is ``True``, this function will raise an - :exc:`pyramid.httpexceptions.HTTPBadRequest` exception. + :exc:`pyramid.exceptions.BadCSRFToken` exception. If the check does succeed and ``raises`` is ``False``, this function will return ``False``. If the CSRF check is successful, this function will return ``True`` unconditionally. @@ -108,93 +115,115 @@ def check_csrf_token(request, supplied_token = request.params.get(token, request.headers.get(header)) if supplied_token != request.session.get_csrf_token(): if raises: - raise HTTPBadRequest('incorrect CSRF token') + raise BadCSRFToken('check_csrf_token(): Invalid token') return False return True -def UnencryptedCookieSessionFactoryConfig( - secret, - timeout=1200, +def BaseCookieSessionFactory( + serialize, + deserialize, cookie_name='session', - cookie_max_age=None, - cookie_path='/', - cookie_domain=None, - cookie_secure=False, - cookie_httponly=False, - cookie_on_exception=True, - signed_serialize=signed_serialize, - signed_deserialize=signed_deserialize, + max_age=None, + path='/', + domain=None, + secure=False, + httponly=False, + timeout=1200, + reissue_time=0, + set_on_exception=True, ): """ - Configure a :term:`session factory` which will provide unencrypted - (but signed) cookie-based sessions. The return value of this - function is a :term:`session factory`, which may be provided as - the ``session_factory`` argument of a - :class:`pyramid.config.Configurator` constructor, or used - as the ``session_factory`` argument of the - :meth:`pyramid.config.Configurator.set_session_factory` - method. + .. versionadded:: 1.5 + + Configure a :term:`session factory` which will provide cookie-based + sessions. The return value of this function is a :term:`session factory`, + which may be provided as the ``session_factory`` argument of a + :class:`pyramid.config.Configurator` constructor, or used as the + ``session_factory`` argument of the + :meth:`pyramid.config.Configurator.set_session_factory` method. The session factory returned by this function will create sessions which are limited to storing fewer than 4000 bytes of data (as the payload must fit into a single cookie). + .. warning: + + This class provides no protection from tampering and is only intended + to be used by framework authors to create their own cookie-based + session factories. + Parameters: - ``secret`` - A string which is used to sign the cookie. + ``serialize`` + A callable accepting a Python object and returning a bytestring. A + ``ValueError`` should be raised for malformed inputs. - ``timeout`` - A number of seconds of inactivity before a session times out. + ``deserialize`` + A callable accepting a bytestring and returning a Python object. A + ``ValueError`` should be raised for malformed inputs. ``cookie_name`` - The name of the cookie used for sessioning. + The name of the cookie used for sessioning. Default: ``'session'``. - ``cookie_max_age`` + ``max_age`` The maximum age of the cookie used for sessioning (in seconds). Default: ``None`` (browser scope). - ``cookie_path`` - The path used for the session cookie. + ``path`` + The path used for the session cookie. Default: ``'/'``. - ``cookie_domain`` + ``domain`` The domain used for the session cookie. Default: ``None`` (no domain). - ``cookie_secure`` - The 'secure' flag of the session cookie. + ``secure`` + The 'secure' flag of the session cookie. Default: ``False``. - ``cookie_httponly`` - The 'httpOnly' flag of the session cookie. + ``httponly`` + Hide the cookie from Javascript by setting the 'HttpOnly' flag of the + session cookie. Default: ``False``. - ``cookie_on_exception`` + ``timeout`` + A number of seconds of inactivity before a session times out. If + ``None`` then the cookie never expires. Default: 1200. + + ``reissue_time`` + The number of seconds that must pass before the cookie is automatically + reissued as the result of a request which accesses the session. The + duration is measured as the number of seconds since the last session + cookie was issued and 'now'. If this value is ``0``, a new cookie + will be reissued on every request accesses the session. If ``None`` + then the cookie's lifetime will never be extended. + + A good rule of thumb: if you want auto-expired cookies based on + inactivity: set the ``timeout`` value to 1200 (20 mins) and set the + ``reissue_time`` value to perhaps a tenth of the ``timeout`` value + (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower + than the ``reissue_time`` value, as the ticket will never be reissued. + However, such a configuration is not explicitly prevented. + + Default: ``0``. + + ``set_on_exception`` If ``True``, set a session cookie even if an exception occurs - while rendering a view. + while rendering a view. Default: ``True``. - ``signed_serialize`` - A callable which takes more or less arbitrary Python data structure and - a secret and returns a signed serialization in bytes. - Default: ``signed_serialize`` (using pickle). - - ``signed_deserialize`` - A callable which takes a signed and serialized data structure in bytes - and a secret and returns the original data structure if the signature - is valid. Default: ``signed_deserialize`` (using pickle). + .. versionadded: 1.5a3 """ @implementer(ISession) - class UnencryptedCookieSessionFactory(dict): + class CookieSession(dict): """ Dictionary-like session object """ # configuration parameters _cookie_name = cookie_name - _cookie_max_age = cookie_max_age - _cookie_path = cookie_path - _cookie_domain = cookie_domain - _cookie_secure = cookie_secure - _cookie_httponly = cookie_httponly - _cookie_on_exception = cookie_on_exception - _secret = secret + _cookie_max_age = max_age + _cookie_path = path + _cookie_domain = domain + _cookie_secure = secure + _cookie_httponly = httponly + _cookie_on_exception = set_on_exception _timeout = timeout + _reissue_time = reissue_time # dirty flag _dirty = False @@ -202,33 +231,45 @@ def UnencryptedCookieSessionFactoryConfig( def __init__(self, request): self.request = request now = time.time() - created = accessed = now + created = renewed = now new = True value = None state = {} cookieval = request.cookies.get(self._cookie_name) if cookieval is not None: try: - value = signed_deserialize(cookieval, self._secret) + value = deserialize(bytes_(cookieval)) except ValueError: + # the cookie failed to deserialize, dropped value = None if value is not None: - accessed, created, state = value - new = False - if now - accessed > self._timeout: + try: + renewed, created, state = value + new = False + if now - renewed > self._timeout: + # expire the session because it was not renewed + # before the timeout threshold + state = {} + except TypeError: + # value failed to unpack properly or renewed was not + # a numeric type so we'll fail deserialization here state = {} self.created = created - self.accessed = accessed + self.accessed = renewed + self.renewed = renewed self.new = new dict.__init__(self, state) # ISession methods def changed(self): - """ This is intentionally a noop; the session is - serialized on every access, so unnecessary""" - pass + if not self._dirty: + self._dirty = True + def set_cookie_callback(request, response): + self._set_cookie(response) + self.request = None # explicitly break cycle for gc + self.request.add_response_callback(set_cookie_callback) def invalidate(self): self.clear() # XXX probably needs to unset cookie @@ -250,22 +291,22 @@ def UnencryptedCookieSessionFactoryConfig( has_key = manage_accessed(dict.has_key) # modifying dictionary methods - clear = manage_accessed(dict.clear) - update = manage_accessed(dict.update) - setdefault = manage_accessed(dict.setdefault) - pop = manage_accessed(dict.pop) - popitem = manage_accessed(dict.popitem) - __setitem__ = manage_accessed(dict.__setitem__) - __delitem__ = manage_accessed(dict.__delitem__) + clear = manage_changed(dict.clear) + update = manage_changed(dict.update) + setdefault = manage_changed(dict.setdefault) + pop = manage_changed(dict.pop) + popitem = manage_changed(dict.popitem) + __setitem__ = manage_changed(dict.__setitem__) + __delitem__ = manage_changed(dict.__delitem__) # flash API methods - @manage_accessed + @manage_changed def flash(self, msg, queue='', allow_duplicate=True): storage = self.setdefault('_f_' + queue, []) if allow_duplicate or (msg not in storage): storage.append(msg) - @manage_accessed + @manage_changed def pop_flash(self, queue=''): storage = self.pop('_f_' + queue, []) return storage @@ -276,7 +317,7 @@ def UnencryptedCookieSessionFactoryConfig( return storage # CSRF API methods - @manage_accessed + @manage_changed def new_csrf_token(self): token = text_(binascii.hexlify(os.urandom(20))) self['_csrft_'] = token @@ -295,9 +336,9 @@ def UnencryptedCookieSessionFactoryConfig( exception = getattr(self.request, 'exception', None) if exception is not None: # dont set a cookie during exceptions return False - cookieval = signed_serialize( - (self.accessed, self.created, dict(self)), self._secret - ) + cookieval = native_(serialize( + (self.accessed, self.created, dict(self)) + )) if len(cookieval) > 4064: raise ValueError( 'Cookie value is too long to store (%s bytes)' % @@ -306,12 +347,259 @@ def UnencryptedCookieSessionFactoryConfig( response.set_cookie( self._cookie_name, value=cookieval, - max_age = self._cookie_max_age, - path = self._cookie_path, - domain = self._cookie_domain, - secure = self._cookie_secure, - httponly = self._cookie_httponly, + max_age=self._cookie_max_age, + path=self._cookie_path, + domain=self._cookie_domain, + secure=self._cookie_secure, + httponly=self._cookie_httponly, ) return True - return UnencryptedCookieSessionFactory + return CookieSession + + +def UnencryptedCookieSessionFactoryConfig( + secret, + timeout=1200, + cookie_name='session', + cookie_max_age=None, + cookie_path='/', + cookie_domain=None, + cookie_secure=False, + cookie_httponly=False, + cookie_on_exception=True, + signed_serialize=signed_serialize, + signed_deserialize=signed_deserialize, + ): + """ + .. deprecated:: 1.5 + Use :func:`pyramid.session.SignedCookieSessionFactory` instead. + + Configure a :term:`session factory` which will provide unencrypted + (but signed) cookie-based sessions. The return value of this + function is a :term:`session factory`, which may be provided as + the ``session_factory`` argument of a + :class:`pyramid.config.Configurator` constructor, or used + as the ``session_factory`` argument of the + :meth:`pyramid.config.Configurator.set_session_factory` + method. + + The session factory returned by this function will create sessions + which are limited to storing fewer than 4000 bytes of data (as the + payload must fit into a single cookie). + + Parameters: + + ``secret`` + A string which is used to sign the cookie. + + ``timeout`` + A number of seconds of inactivity before a session times out. + + ``cookie_name`` + The name of the cookie used for sessioning. + + ``cookie_max_age`` + The maximum age of the cookie used for sessioning (in seconds). + Default: ``None`` (browser scope). + + ``cookie_path`` + The path used for the session cookie. + + ``cookie_domain`` + The domain used for the session cookie. Default: ``None`` (no domain). + + ``cookie_secure`` + The 'secure' flag of the session cookie. + + ``cookie_httponly`` + The 'httpOnly' flag of the session cookie. + + ``cookie_on_exception`` + If ``True``, set a session cookie even if an exception occurs + while rendering a view. + + ``signed_serialize`` + A callable which takes more or less arbitrary Python data structure and + a secret and returns a signed serialization in bytes. + Default: ``signed_serialize`` (using pickle). + + ``signed_deserialize`` + A callable which takes a signed and serialized data structure in bytes + and a secret and returns the original data structure if the signature + is valid. Default: ``signed_deserialize`` (using pickle). + """ + + return BaseCookieSessionFactory( + lambda v: signed_serialize(v, secret), + lambda v: signed_deserialize(v, secret), + cookie_name=cookie_name, + max_age=cookie_max_age, + path=cookie_path, + domain=cookie_domain, + secure=cookie_secure, + httponly=cookie_httponly, + timeout=timeout, + reissue_time=0, # to keep session.accessed == session.renewed + set_on_exception=cookie_on_exception, + ) + +deprecated( + 'UnencryptedCookieSessionFactoryConfig', + 'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of ' + 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead.' + ) + +def SignedCookieSessionFactory( + secret, + cookie_name='session', + max_age=None, + path='/', + domain=None, + secure=False, + httponly=False, + set_on_exception=True, + timeout=1200, + reissue_time=0, + hashalg='sha512', + salt='pyramid.session.', + serialize=None, + deserialize=None, + ): + """ + .. versionadded:: 1.5 + + Configure a :term:`session factory` which will provide signed + cookie-based sessions. The return value of this + function is a :term:`session factory`, which may be provided as + the ``session_factory`` argument of a + :class:`pyramid.config.Configurator` constructor, or used + as the ``session_factory`` argument of the + :meth:`pyramid.config.Configurator.set_session_factory` + method. + + The session factory returned by this function will create sessions + which are limited to storing fewer than 4000 bytes of data (as the + payload must fit into a single cookie). + + Parameters: + + ``secret`` + A string which is used to sign the cookie. The secret should be at + least as long as the block size of the selected hash algorithm. For + ``sha512`` this would mean a 128 bit (64 character) secret. It should + be unique within the set of secret values provided to Pyramid for + its various subsystems (see :ref:`admonishment_against_secret_sharing`). + + ``hashalg`` + The HMAC digest algorithm to use for signing. The algorithm must be + supported by the :mod:`hashlib` library. Default: ``'sha512'``. + + ``salt`` + A namespace to avoid collisions between different uses of a shared + secret. Reusing a secret for different parts of an application is + strongly discouraged (see :ref:`admonishment_against_secret_sharing`). + Default: ``'pyramid.session.'``. + + ``cookie_name`` + The name of the cookie used for sessioning. Default: ``'session'``. + + ``max_age`` + The maximum age of the cookie used for sessioning (in seconds). + Default: ``None`` (browser scope). + + ``path`` + The path used for the session cookie. Default: ``'/'``. + + ``domain`` + The domain used for the session cookie. Default: ``None`` (no domain). + + ``secure`` + The 'secure' flag of the session cookie. Default: ``False``. + + ``httponly`` + Hide the cookie from Javascript by setting the 'HttpOnly' flag of the + session cookie. Default: ``False``. + + ``timeout`` + A number of seconds of inactivity before a session times out. If + ``None`` then the cookie never expires. Default: 1200. + + ``reissue_time`` + The number of seconds that must pass before the cookie is automatically + reissued as the result of a request which accesses the session. The + duration is measured as the number of seconds since the last session + cookie was issued and 'now'. If this value is ``0``, a new cookie + will be reissued on every request accesses the session. If ``None`` + then the cookie's lifetime will never be extended. + + A good rule of thumb: if you want auto-expired cookies based on + inactivity: set the ``timeout`` value to 1200 (20 mins) and set the + ``reissue_time`` value to perhaps a tenth of the ``timeout`` value + (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower + than the ``reissue_time`` value, as the ticket will never be reissued. + However, such a configuration is not explicitly prevented. + + Default: ``0``. + + ``set_on_exception`` + If ``True``, set a session cookie even if an exception occurs + while rendering a view. Default: ``True``. + + ``serialize`` + A callable accepting a Python object and returning a bytestring. A + ``ValueError`` should be raised for malformed inputs. + Default: :func:`pickle.dumps`. + + ``deserialize`` + A callable accepting a bytestring and returning a Python object. A + ``ValueError`` should be raised for malformed inputs. + Default: :func:`pickle.loads`. + + .. versionadded: 1.5a3 + """ + + if serialize is None: + serialize = lambda v: pickle.dumps(v, pickle.HIGHEST_PROTOCOL) + + if deserialize is None: + deserialize = pickle.loads + + digestmod = lambda string=b'': hashlib.new(hashalg, string) + digest_size = digestmod().digest_size + + salted_secret = bytes_(salt or '') + bytes_(secret) + + def signed_serialize(appstruct): + cstruct = serialize(appstruct) + sig = hmac.new(salted_secret, cstruct, digestmod).digest() + return base64.b64encode(cstruct + sig) + + def signed_deserialize(bstruct): + try: + fstruct = base64.b64decode(bstruct) + except (binascii.Error, TypeError) as e: + raise ValueError('Badly formed base64 data: %s' % e) + + cstruct = fstruct[:-digest_size] + expected_sig = fstruct[-digest_size:] + + sig = hmac.new(salted_secret, cstruct, digestmod).digest() + if strings_differ(sig, expected_sig): + raise ValueError('Invalid signature') + + return deserialize(cstruct) + + return BaseCookieSessionFactory( + signed_serialize, + signed_deserialize, + cookie_name=cookie_name, + max_age=max_age, + path=path, + domain=domain, + secure=secure, + httponly=httponly, + timeout=timeout, + reissue_time=reissue_time, + set_on_exception=set_on_exception, + ) diff --git a/pyramid/testing.py b/pyramid/testing.py index 4590c55f8..b3460d8aa 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -27,6 +27,8 @@ from pyramid.registry import Registry from pyramid.security import ( Authenticated, Everyone, + AuthenticationAPIMixin, + AuthorizationAPIMixin, ) from pyramid.threadlocal import ( @@ -280,10 +282,15 @@ class DummySession(dict): token = self.new_csrf_token() return token - @implementer(IRequest) -class DummyRequest(URLMethodsMixin, CallbackMethodsMixin, InstancePropertyMixin, - LocalizerRequestMixin): +class DummyRequest( + URLMethodsMixin, + CallbackMethodsMixin, + InstancePropertyMixin, + LocalizerRequestMixin, + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ): """ A DummyRequest object (incompletely) imitates a :term:`request` object. The ``params``, ``environ``, ``headers``, ``path``, and diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index 6e9e3920d..3ac8f2d61 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -1374,6 +1374,32 @@ class TestBasicAuthAuthenticationPolicy(unittest.TestCase): policy = self._makeOne(check) self.assertEqual(policy.authenticated_userid(request), 'chrisr') + def test_authenticated_userid_utf8(self): + import base64 + request = testing.DummyRequest() + inputs = (b'm\xc3\xb6rk\xc3\xb6:' + b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8') + request.headers['Authorization'] = 'Basic %s' % ( + base64.b64encode(inputs.encode('utf-8')).decode('latin-1')) + def check(username, password, request): + return [] + policy = self._makeOne(check) + self.assertEqual(policy.authenticated_userid(request), + b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8')) + + def test_authenticated_userid_latin1(self): + import base64 + request = testing.DummyRequest() + inputs = (b'm\xc3\xb6rk\xc3\xb6:' + b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8') + request.headers['Authorization'] = 'Basic %s' % ( + base64.b64encode(inputs.encode('latin-1')).decode('latin-1')) + def check(username, password, request): + return [] + policy = self._makeOne(check) + self.assertEqual(policy.authenticated_userid(request), + b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8')) + def test_unauthenticated_userid_invalid_payload(self): import base64 request = testing.DummyRequest() diff --git a/pyramid/tests/test_authorization.py b/pyramid/tests/test_authorization.py index 60b1b0c8d..05cd3b4f8 100644 --- a/pyramid/tests/test_authorization.py +++ b/pyramid/tests/test_authorization.py @@ -146,6 +146,19 @@ class TestACLAuthorizationPolicy(unittest.TestCase): policy.principals_allowed_by_permission(context, 'read')) self.assertEqual(result, ['chrism']) + def test_principals_allowed_by_permission_callable_acl(self): + from pyramid.security import Allow + from pyramid.security import DENY_ALL + context = DummyContext() + acl = lambda: [ (Allow, 'chrism', ('read', 'write')), + DENY_ALL, + (Allow, 'other', 'read') ] + context.__acl__ = acl + policy = self._makeOne() + result = sorted( + policy.principals_allowed_by_permission(context, 'read')) + self.assertEqual(result, ['chrism']) + def test_principals_allowed_by_permission_string_permission(self): from pyramid.security import Allow context = DummyContext() diff --git a/pyramid/tests/test_config/test_testing.py b/pyramid/tests/test_config/test_testing.py index 1089f09fc..05561bfe9 100644 --- a/pyramid/tests/test_config/test_testing.py +++ b/pyramid/tests/test_config/test_testing.py @@ -1,6 +1,7 @@ import unittest from pyramid.compat import text_ +from pyramid.security import AuthenticationAPIMixin, AuthorizationAPIMixin from pyramid.tests.test_config import IDummy class TestingConfiguratorMixinTests(unittest.TestCase): @@ -196,13 +197,9 @@ from zope.interface import implementer class DummyEvent: pass -class DummyRequest: - subpath = () - matchdict = None +class DummyRequest(AuthenticationAPIMixin, AuthorizationAPIMixin): def __init__(self, environ=None): if environ is None: environ = {} self.environ = environ - self.params = {} - self.cookies = {} - + diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index be2865d30..051961d25 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -40,9 +40,6 @@ class TestViewsConfigurationMixin(unittest.TestCase): def _registerRenderer(self, config, name='.txt'): from pyramid.interfaces import IRendererFactory - from pyramid.interfaces import ITemplateRenderer - from zope.interface import implementer - @implementer(ITemplateRenderer) class Renderer: def __init__(self, info): self.__class__.info = info @@ -1818,6 +1815,36 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertRaises(ConfigurationError, config.add_forbidden_view, http_cache='foo') + def test_add_forbidden_view_with_view_defaults(self): + from pyramid.interfaces import IRequest + from pyramid.renderers import null_renderer + from pyramid.exceptions import PredicateMismatch + from pyramid.httpexceptions import HTTPForbidden + from zope.interface import directlyProvides + from zope.interface import implementedBy + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + def __init__(self, request): + pass + def __call__(self): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_forbidden_view( + view=view, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(HTTPForbidden), + request_iface=IRequest) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + context = DummyContext() + request = self._makeRequest(config) + self.assertRaises(PredicateMismatch, wrapper, context, request) + def test_add_notfound_view(self): from pyramid.renderers import null_renderer from zope.interface import implementedBy @@ -1885,6 +1912,36 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = view(None, request) self.assertEqual(result.location, '/scriptname/foo/?a=1&b=2') + def test_add_notfound_view_with_view_defaults(self): + from pyramid.interfaces import IRequest + from pyramid.renderers import null_renderer + from pyramid.exceptions import PredicateMismatch + from pyramid.httpexceptions import HTTPNotFound + from zope.interface import directlyProvides + from zope.interface import implementedBy + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + def __init__(self, request): + pass + def __call__(self): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_notfound_view( + view=view, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(HTTPNotFound), + request_iface=IRequest) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + context = DummyContext() + request = self._makeRequest(config) + self.assertRaises(PredicateMismatch, wrapper, context, request) + # Since Python 3 has to be all cool and fancy and different... def _assertBody(self, response, value): from pyramid.compat import text_type diff --git a/pyramid/tests/test_encode.py b/pyramid/tests/test_encode.py index 736ecb5b3..908249877 100644 --- a/pyramid/tests/test_encode.py +++ b/pyramid/tests/test_encode.py @@ -41,6 +41,18 @@ class UrlEncodeTests(unittest.TestCase): result = self._callFUT({'a':1}) self.assertEqual(result, 'a=1') + def test_None_value(self): + result = self._callFUT([('a', None)]) + self.assertEqual(result, 'a=') + + def test_None_value_with_prefix(self): + result = self._callFUT([('a', '1'), ('b', None)]) + self.assertEqual(result, 'a=1&b=') + + def test_None_value_with_prefix_values(self): + result = self._callFUT([('a', '1'), ('b', None), ('c', None)]) + self.assertEqual(result, 'a=1&b=&c=') + class URLQuoteTests(unittest.TestCase): def _callFUT(self, val, safe=''): from pyramid.encode import url_quote diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index aa5ebb376..993209046 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -11,6 +11,12 @@ class TestBWCompat(unittest.TestCase): from pyramid.httpexceptions import HTTPForbidden as two self.assertTrue(one is two) +class TestBadCSRFToken(unittest.TestCase): + def test_response_equivalence(self): + from pyramid.exceptions import BadCSRFToken + from pyramid.httpexceptions import HTTPBadRequest + self.assertTrue(isinstance(BadCSRFToken(), HTTPBadRequest)) + class TestNotFound(unittest.TestCase): def _makeOne(self, message): from pyramid.exceptions import NotFound diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 6cd72fc59..ed41b62ff 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -6,9 +6,10 @@ from pyramid.compat import ( text_, bytes_, native_, - iteritems_, - iterkeys_, - itervalues_, + ) +from pyramid.security import ( + AuthenticationAPIMixin, + AuthorizationAPIMixin, ) class TestRequest(unittest.TestCase): @@ -53,6 +54,11 @@ class TestRequest(unittest.TestCase): cls = self._getTargetClass() self.assertEqual(cls.ResponseClass, Response) + def test_implements_security_apis(self): + apis = (AuthenticationAPIMixin, AuthorizationAPIMixin) + r = self._makeOne() + self.assertTrue(isinstance(r, apis)) + def test_charset_defaults_to_utf8(self): r = self._makeOne({'PATH_INFO':'/'}) self.assertEqual(r.charset, 'UTF-8') diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py index d580203af..366aa00b5 100644 --- a/pyramid/tests/test_scripts/dummy.py +++ b/pyramid/tests/test_scripts/dummy.py @@ -146,10 +146,13 @@ class DummyBootstrap(object): def __call__(self, *a, **kw): self.a = a self.kw = kw + registry = kw.get('registry', self.registry) + request = kw.get('request', self.request) + request.registry = registry return { 'app': self.app, - 'registry': self.registry, - 'request': self.request, + 'registry': registry, + 'request': request, 'root': self.root, 'root_factory': self.root_factory, 'closer': self.closer, diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 1406d3911..6516ac229 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -110,6 +110,21 @@ class TestPCreateCommand(unittest.TestCase): scaffold2.vars, {'project': 'Distro', 'egg': 'Distro', 'package': 'distro'}) + def test_known_scaffold_with_path_as_project_target_rendered(self): + import os + cmd = self._makeOne('-s', 'dummy', '/tmp/foo/Distro/') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), '/tmp/foo/Distro')) + ) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro'}) + class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.pcreate import main diff --git a/pyramid/tests/test_scripts/test_pviews.py b/pyramid/tests/test_scripts/test_pviews.py index 266d1ec90..b162144a7 100644 --- a/pyramid/tests/test_scripts/test_pviews.py +++ b/pyramid/tests/test_scripts/test_pviews.py @@ -12,6 +12,12 @@ class TestPViewsCommand(unittest.TestCase): cmd.args = ('/foo/bar/myapp.ini#myapp',) return cmd + def _makeRequest(self, url, registry): + from pyramid.request import Request + request = Request.blank('/a') + request.registry = registry + return request + def _register_mapper(self, registry, routes): from pyramid.interfaces import IRoutesMapper mapper = dummy.DummyMapper(*routes) @@ -22,7 +28,8 @@ class TestPViewsCommand(unittest.TestCase): registry = Registry() self._register_mapper(registry, []) command = self._makeOne(registry) - result = command._find_view('/a', registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) self.assertEqual(result, None) def test__find_view_no_match_multiview_registered(self): @@ -45,7 +52,8 @@ class TestPViewsCommand(unittest.TestCase): IMultiView) self._register_mapper(registry, []) command = self._makeOne(registry=registry) - result = command._find_view('/x', registry) + request = self._makeRequest('/x', registry) + result = command._find_view(request) self.assertEqual(result, None) def test__find_view_traversal(self): @@ -65,7 +73,8 @@ class TestPViewsCommand(unittest.TestCase): IView, name='a') self._register_mapper(registry, []) command = self._makeOne(registry=registry) - result = command._find_view('/a', registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) self.assertEqual(result, view1) def test__find_view_traversal_multiview(self): @@ -89,7 +98,8 @@ class TestPViewsCommand(unittest.TestCase): IMultiView, name='a') self._register_mapper(registry, []) command = self._makeOne(registry=registry) - result = command._find_view('/a', registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) self.assertEqual(result, view) def test__find_view_route_no_multiview(self): @@ -117,7 +127,8 @@ class TestPViewsCommand(unittest.TestCase): dummy.DummyRoute('b', '/b', factory=Factory)] self._register_mapper(registry, routes) command = self._makeOne(registry=registry) - result = command._find_view('/a', registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) self.assertEqual(result, view) def test__find_view_route_multiview_no_view_registered(self): @@ -147,7 +158,8 @@ class TestPViewsCommand(unittest.TestCase): dummy.DummyRoute('b', '/a', matchdict={})] self._register_mapper(registry, routes) command = self._makeOne(registry=registry) - result = command._find_view('/a', registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) self.assertTrue(IMultiView.providedBy(result)) def test__find_view_route_multiview(self): @@ -185,7 +197,8 @@ class TestPViewsCommand(unittest.TestCase): dummy.DummyRoute('b', '/a', matchdict={})] self._register_mapper(registry, routes) command = self._makeOne(registry=registry) - result = command._find_view('/a', registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) self.assertTrue(IMultiView.providedBy(result)) self.assertEqual(len(result.views), 2) self.assertTrue((None, view1, None) in result.views) @@ -228,7 +241,7 @@ class TestPViewsCommand(unittest.TestCase): command = self._makeOne(registry=registry) L = [] command.out = L.append - command._find_view = lambda arg1, arg2: None + command._find_view = lambda arg1: None command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -241,7 +254,7 @@ class TestPViewsCommand(unittest.TestCase): command = self._makeOne(registry=registry) L = [] command.out = L.append - command._find_view = lambda arg1, arg2: None + command._find_view = lambda arg1: None command.args = ('/foo/bar/myapp.ini#myapp', 'a') result = command.run() self.assertEqual(result, 0) @@ -255,7 +268,7 @@ class TestPViewsCommand(unittest.TestCase): L = [] command.out = L.append view = dummy.DummyView(context='context', view_name='a') - command._find_view = lambda arg1, arg2: view + command._find_view = lambda arg1: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -273,7 +286,7 @@ class TestPViewsCommand(unittest.TestCase): command.out = L.append def view(): pass view.__request_attrs__ = {'context': 'context', 'view_name': 'a'} - command._find_view = lambda arg1, arg2: view + command._find_view = lambda arg1: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -291,7 +304,7 @@ class TestPViewsCommand(unittest.TestCase): command.out = L.append view = dummy.DummyView(context='context', view_name='a') view.__permission__ = 'test' - command._find_view = lambda arg1, arg2: view + command._find_view = lambda arg1: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -312,7 +325,7 @@ class TestPViewsCommand(unittest.TestCase): predicate.text = lambda *arg: "predicate = x" view = dummy.DummyView(context='context', view_name='a') view.__predicates__ = [predicate] - command._find_view = lambda arg1, arg2: view + command._find_view = lambda arg1: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -332,7 +345,7 @@ class TestPViewsCommand(unittest.TestCase): route = dummy.DummyRoute('a', '/a', matchdict={}) view = dummy.DummyView(context='context', view_name='a', matched_route=route, subpath='') - command._find_view = lambda arg1, arg2: view + command._find_view = lambda arg1: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -360,7 +373,7 @@ class TestPViewsCommand(unittest.TestCase): view_name='a1') multiview2 = dummy.DummyMultiView(multiview1, context='context', view_name='a') - command._find_view = lambda arg1, arg2: multiview2 + command._find_view = lambda arg1: multiview2 command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -383,7 +396,7 @@ class TestPViewsCommand(unittest.TestCase): route = dummy.DummyRoute('a', '/a', matchdict={}, predicate=predicate) view = dummy.DummyView(context='context', view_name='a', matched_route=route, subpath='') - command._find_view = lambda arg1, arg2: view + command._find_view = lambda arg1: view command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -409,7 +422,7 @@ class TestPViewsCommand(unittest.TestCase): view.__name__ = 'view' view.__view_attr__ = 'call' multiview = dummy.DummyMultiView(view, context='context', view_name='a') - command._find_view = lambda arg1, arg2: multiview + command._find_view = lambda arg1: multiview command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -430,7 +443,7 @@ class TestPViewsCommand(unittest.TestCase): view.__view_attr__ = 'call' view.__permission__ = 'test' multiview = dummy.DummyMultiView(view, context='context', view_name='a') - command._find_view = lambda arg1, arg2: multiview + command._find_view = lambda arg1: multiview command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) @@ -454,7 +467,7 @@ class TestPViewsCommand(unittest.TestCase): view.__view_attr__ = 'call' view.__predicates__ = [predicate] multiview = dummy.DummyMultiView(view, context='context', view_name='a') - command._find_view = lambda arg1, arg2: multiview + command._find_view = lambda arg1: multiview command.args = ('/foo/bar/myapp.ini#myapp', '/a') result = command.run() self.assertEqual(result, 0) diff --git a/pyramid/tests/test_security.py b/pyramid/tests/test_security.py index e530e33ca..6f08a100c 100644 --- a/pyramid/tests/test_security.py +++ b/pyramid/tests/test_security.py @@ -1,14 +1,13 @@ import unittest -from pyramid.testing import cleanUp - +from pyramid import testing class TestAllPermissionsList(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() def tearDown(self): - cleanUp() + testing.tearDown() def _getTargetClass(self): from pyramid.security import AllPermissionsList @@ -103,13 +102,101 @@ class TestACLDenied(unittest.TestCase): self.assertTrue('<ACLDenied instance at ' in repr(denied)) self.assertTrue("with msg %r>" % msg in repr(denied)) -class TestViewExecutionPermitted(unittest.TestCase): +class TestPrincipalsAllowedByPermission(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, *arg): + from pyramid.security import principals_allowed_by_permission + return principals_allowed_by_permission(*arg) + + def test_no_authorization_policy(self): + from pyramid.security import Everyone + context = DummyContext() + result = self._callFUT(context, 'view') + self.assertEqual(result, [Everyone]) + + def test_with_authorization_policy(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + _registerAuthorizationPolicy(registry, 'yo') + context = DummyContext() + result = self._callFUT(context, 'view') + self.assertEqual(result, 'yo') + +class TestRemember(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, *arg): + from pyramid.security import remember + return remember(*arg) + + def test_no_authentication_policy(self): + request = _makeRequest() + result = self._callFUT(request, 'me') + self.assertEqual(result, []) + def test_with_authentication_policy(self): + request = _makeRequest() + registry = request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request, 'me') + self.assertEqual(result, [('X-Pyramid-Test', 'me')]) + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = _makeRequest() + del request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request, 'me') + self.assertEqual(result, [('X-Pyramid-Test', 'me')]) + +class TestForget(unittest.TestCase): + def setUp(self): + testing.setUp() + def tearDown(self): - cleanUp() + testing.tearDown() + def _callFUT(self, *arg): + from pyramid.security import forget + return forget(*arg) + + def test_no_authentication_policy(self): + request = _makeRequest() + result = self._callFUT(request) + self.assertEqual(result, []) + + def test_with_authentication_policy(self): + request = _makeRequest() + _registerAuthenticationPolicy(request.registry, 'yo') + result = self._callFUT(request) + self.assertEqual(result, [('X-Pyramid-Test', 'logout')]) + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = _makeRequest() + del request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request) + self.assertEqual(result, [('X-Pyramid-Test', 'logout')]) + +class TestViewExecutionPermitted(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + def _callFUT(self, *arg, **kw): from pyramid.security import view_execution_permitted return view_execution_permitted(*arg, **kw) @@ -140,7 +227,7 @@ class TestViewExecutionPermitted(unittest.TestCase): reg = get_current_registry() reg.registerUtility(settings, ISettings) context = DummyContext() - request = DummyRequest({}) + request = testing.DummyRequest({}) class DummyView(object): pass view = DummyView() @@ -159,7 +246,7 @@ class TestViewExecutionPermitted(unittest.TestCase): reg = get_current_registry() reg.registerUtility(settings, ISettings) context = DummyContext() - request = DummyRequest({}) + request = testing.DummyRequest({}) self.assertRaises(TypeError, self._callFUT, context, request, '') def test_with_permission(self): @@ -171,232 +258,197 @@ class TestViewExecutionPermitted(unittest.TestCase): context = DummyContext() directlyProvides(context, IContext) self._registerSecuredView('', True) - request = DummyRequest({}) + request = testing.DummyRequest({}) directlyProvides(request, IRequest) result = self._callFUT(context, request, '') - self.assertTrue(result is True) - -class TestHasPermission(unittest.TestCase): - def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() - - def _callFUT(self, *arg): - from pyramid.security import has_permission - return has_permission(*arg) - - def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT('view', None, request) - self.assertEqual(result, True) - self.assertEqual(result.msg, 'No authentication policy in use.') - - def test_authentication_policy_no_authorization_policy(self): - request = _makeRequest() - _registerAuthenticationPolicy(request.registry, None) - self.assertRaises(ValueError, self._callFUT, 'view', None, request) - - def test_authn_and_authz_policies_registered(self): - request = _makeRequest() - _registerAuthenticationPolicy(request.registry, None) - _registerAuthorizationPolicy(request.registry, 'yo') - self.assertEqual(self._callFUT('view', None, request), 'yo') - - def test_no_registry_on_request(self): - from pyramid.threadlocal import get_current_registry - request = DummyRequest({}) - registry = get_current_registry() - _registerAuthenticationPolicy(registry, None) - _registerAuthorizationPolicy(registry, 'yo') - self.assertEqual(self._callFUT('view', None, request), 'yo') + self.assertTrue(result) class TestAuthenticatedUserId(unittest.TestCase): def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() + testing.setUp() - def _callFUT(self, request): - from pyramid.security import authenticated_userid - return authenticated_userid(request) + def tearDown(self): + testing.tearDown() + + def test_backward_compat_delegates_to_mixin(self): + from zope.deprecation import __show__ + try: + __show__.off() + request = _makeFakeRequest() + from pyramid.security import authenticated_userid + self.assertEqual( + authenticated_userid(request), + 'authenticated_userid' + ) + finally: + __show__.on() def test_no_authentication_policy(self): request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, None) + self.assertEqual(request.authenticated_userid, None) def test_with_authentication_policy(self): request = _makeRequest() _registerAuthenticationPolicy(request.registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.authenticated_userid, 'yo') def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry - request = DummyRequest({}) registry = get_current_registry() + request = _makeRequest() + del request.registry _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.authenticated_userid, 'yo') -class TestUnauthenticatedUserId(unittest.TestCase): +class TestUnAuthenticatedUserId(unittest.TestCase): def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() + testing.setUp() - def _callFUT(self, request): - from pyramid.security import unauthenticated_userid - return unauthenticated_userid(request) + def tearDown(self): + testing.tearDown() + + def test_backward_compat_delegates_to_mixin(self): + from zope.deprecation import __show__ + try: + __show__.off() + request = _makeFakeRequest() + from pyramid.security import unauthenticated_userid + self.assertEqual( + unauthenticated_userid(request), + 'unauthenticated_userid', + ) + finally: + __show__.on() def test_no_authentication_policy(self): request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, None) + self.assertEqual(request.unauthenticated_userid, None) def test_with_authentication_policy(self): request = _makeRequest() _registerAuthenticationPolicy(request.registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.unauthenticated_userid, 'yo') def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry - request = DummyRequest({}) registry = get_current_registry() + request = _makeRequest() + del request.registry _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.unauthenticated_userid, 'yo') class TestEffectivePrincipals(unittest.TestCase): def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() + testing.setUp() - def _callFUT(self, request): - from pyramid.security import effective_principals - return effective_principals(request) + def tearDown(self): + testing.tearDown() + + def test_backward_compat_delegates_to_mixin(self): + request = _makeFakeRequest() + from zope.deprecation import __show__ + try: + __show__.off() + from pyramid.security import effective_principals + self.assertEqual( + effective_principals(request), + 'effective_principals' + ) + finally: + __show__.on() def test_no_authentication_policy(self): from pyramid.security import Everyone request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, [Everyone]) + self.assertEqual(request.effective_principals, [Everyone]) def test_with_authentication_policy(self): request = _makeRequest() _registerAuthenticationPolicy(request.registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.effective_principals, 'yo') def test_with_authentication_policy_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry registry = get_current_registry() - request = DummyRequest({}) + request = _makeRequest() + del request.registry _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + self.assertEqual(request.effective_principals, 'yo') -class TestPrincipalsAllowedByPermission(unittest.TestCase): - def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() - - def _callFUT(self, *arg): - from pyramid.security import principals_allowed_by_permission - return principals_allowed_by_permission(*arg) - - def test_no_authorization_policy(self): - from pyramid.security import Everyone - context = DummyContext() - result = self._callFUT(context, 'view') - self.assertEqual(result, [Everyone]) - - def test_with_authorization_policy(self): - from pyramid.threadlocal import get_current_registry - registry = get_current_registry() - _registerAuthorizationPolicy(registry, 'yo') - context = DummyContext() - result = self._callFUT(context, 'view') - self.assertEqual(result, 'yo') - -class TestRemember(unittest.TestCase): +class TestHasPermission(unittest.TestCase): def setUp(self): - cleanUp() + testing.setUp() def tearDown(self): - cleanUp() + testing.tearDown() - def _callFUT(self, *arg): - from pyramid.security import remember - return remember(*arg) + def _makeOne(self): + from pyramid.security import AuthorizationAPIMixin + from pyramid.registry import Registry + mixin = AuthorizationAPIMixin() + mixin.registry = Registry() + mixin.context = object() + return mixin + + def test_delegates_to_mixin(self): + from zope.deprecation import __show__ + try: + __show__.off() + mixin = self._makeOne() + from pyramid.security import has_permission + self.called_has_permission = False + + def mocked_has_permission(*args, **kw): + self.called_has_permission = True + + mixin.has_permission = mocked_has_permission + has_permission('view', object(), mixin) + self.assertTrue(self.called_has_permission) + finally: + __show__.on() def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT(request, 'me') - self.assertEqual(result, []) + request = self._makeOne() + result = request.has_permission('view') + self.assertTrue(result) + self.assertEqual(result.msg, 'No authentication policy in use.') - def test_with_authentication_policy(self): - request = _makeRequest() - registry = request.registry - _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request, 'me') - self.assertEqual(result, 'yo') + def test_with_no_authorization_policy(self): + request = self._makeOne() + _registerAuthenticationPolicy(request.registry, None) + self.assertRaises(ValueError, + request.has_permission, 'view', context=None) - def test_with_authentication_policy_no_reg_on_request(self): + def test_with_authn_and_authz_policies_registered(self): + request = self._makeOne() + _registerAuthenticationPolicy(request.registry, None) + _registerAuthorizationPolicy(request.registry, 'yo') + self.assertEqual(request.has_permission('view', context=None), 'yo') + + def test_with_no_reg_on_request(self): from pyramid.threadlocal import get_current_registry registry = get_current_registry() - request = DummyRequest({}) - _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request, 'me') - self.assertEqual(result, 'yo') - -class TestForget(unittest.TestCase): - def setUp(self): - cleanUp() - - def tearDown(self): - cleanUp() - - def _callFUT(self, *arg): - from pyramid.security import forget - return forget(*arg) + request = self._makeOne() + del request.registry + _registerAuthenticationPolicy(registry, None) + _registerAuthorizationPolicy(registry, 'yo') + self.assertEqual(request.has_permission('view'), 'yo') - def test_no_authentication_policy(self): - request = _makeRequest() - result = self._callFUT(request) - self.assertEqual(result, []) + def test_with_no_context_passed(self): + request = self._makeOne() + self.assertTrue(request.has_permission('view')) - def test_with_authentication_policy(self): - request = _makeRequest() - _registerAuthenticationPolicy(request.registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') + def test_with_no_context_passed_or_on_request(self): + request = self._makeOne() + del request.context + self.assertRaises(AttributeError, request.has_permission, 'view') - def test_with_authentication_policy_no_reg_on_request(self): - from pyramid.threadlocal import get_current_registry - registry = get_current_registry() - request = DummyRequest({}) - _registerAuthenticationPolicy(registry, 'yo') - result = self._callFUT(request) - self.assertEqual(result, 'yo') +_TEST_HEADER = 'X-Pyramid-Test' class DummyContext: def __init__(self, *arg, **kw): self.__dict__.update(kw) -class DummyRequest: - def __init__(self, environ): - self.environ = environ - class DummyAuthenticationPolicy: def __init__(self, result): self.result = result @@ -411,10 +463,14 @@ class DummyAuthenticationPolicy: return self.result def remember(self, request, principal, **kw): - return self.result + headers = [(_TEST_HEADER, principal)] + self._header_remembered = headers[0] + return headers def forget(self, request): - return self.result + headers = [(_TEST_HEADER, 'logout')] + self._header_forgotten = headers[0] + return headers class DummyAuthorizationPolicy: def __init__(self, result): @@ -440,8 +496,24 @@ def _registerAuthorizationPolicy(reg, result): def _makeRequest(): from pyramid.registry import Registry - request = DummyRequest({}) + request = testing.DummyRequest(environ={}) request.registry = Registry() + request.context = object() return request +def _makeFakeRequest(): + class FakeRequest(testing.DummyRequest): + @property + def authenticated_userid(req): + return 'authenticated_userid' + + @property + def unauthenticated_userid(req): + return 'unauthenticated_userid' + + @property + def effective_principals(req): + return 'effective_principals' + + return FakeRequest({}) diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 35e2b5c27..a9f70d6a0 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -1,10 +1,8 @@ +import json import unittest from pyramid import testing -class TestUnencryptedCookieSession(unittest.TestCase): - def _makeOne(self, request, **kw): - from pyramid.session import UnencryptedCookieSessionFactoryConfig - return UnencryptedCookieSessionFactoryConfig('secret', **kw)(request) +class SharedCookieSessionTests(object): def test_ctor_no_cookie(self): request = testing.DummyRequest() @@ -18,36 +16,47 @@ class TestUnencryptedCookieSession(unittest.TestCase): session = self._makeOne(request) verifyObject(ISession, session) - def _serialize(self, accessed, state, secret='secret'): - from pyramid.session import signed_serialize - return signed_serialize((accessed, accessed, state), secret) - def test_ctor_with_cookie_still_valid(self): import time request = testing.DummyRequest() - cookieval = self._serialize(time.time(), {'state':1}) + cookieval = self._serialize((time.time(), 0, {'state': 1})) request.cookies['session'] = cookieval session = self._makeOne(request) self.assertEqual(dict(session), {'state':1}) - + def test_ctor_with_cookie_expired(self): request = testing.DummyRequest() - cookieval = self._serialize(0, {'state':1}) + cookieval = self._serialize((0, 0, {'state': 1})) request.cookies['session'] = cookieval session = self._makeOne(request) self.assertEqual(dict(session), {}) - def test_ctor_with_bad_cookie(self): + def test_ctor_with_bad_cookie_cannot_deserialize(self): + request = testing.DummyRequest() + request.cookies['session'] = 'abc' + session = self._makeOne(request) + self.assertEqual(dict(session), {}) + + def test_ctor_with_bad_cookie_not_tuple(self): request = testing.DummyRequest() - cookieval = 'abc' + cookieval = self._serialize('abc') request.cookies['session'] = cookieval session = self._makeOne(request) self.assertEqual(dict(session), {}) + def test_timeout(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time() - 5, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, timeout=1) + self.assertEqual(dict(session), {}) + def test_changed(self): request = testing.DummyRequest() session = self._makeOne(request) self.assertEqual(session.changed(), None) + self.assertTrue(session._dirty) def test_invalidate(self): request = testing.DummyRequest() @@ -56,6 +65,15 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertEqual(session.invalidate(), None) self.assertFalse('a' in session) + def test_reissue_triggered(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time() - 2, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request) + self.assertEqual(session['state'], 1) + self.assertTrue(session._dirty) + def test__set_cookie_on_exception(self): request = testing.DummyRequest() request.exception = True @@ -95,16 +113,16 @@ class TestUnencryptedCookieSession(unittest.TestCase): request = testing.DummyRequest() request.exception = None session = self._makeOne(request, - cookie_name = 'abc', - cookie_path = '/foo', - cookie_domain = 'localhost', - cookie_secure = True, - cookie_httponly = True, + cookie_name='abc', + path='/foo', + domain='localhost', + secure=True, + httponly=True, ) session['abc'] = 'x' response = Response() self.assertEqual(session._set_cookie(response), True) - cookieval= response.headerlist[-1][1] + cookieval = response.headerlist[-1][1] val, domain, path, secure, httponly = [x.strip() for x in cookieval.split(';')] self.assertTrue(val.startswith('abc=')) @@ -205,6 +223,199 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertTrue(token) self.assertTrue('_csrft_' in session) + def test_no_set_cookie_with_exception(self): + import webob + request = testing.DummyRequest() + request.exception = True + session = self._makeOne(request, set_on_exception=False) + session['a'] = 1 + callbacks = request.response_callbacks + self.assertEqual(len(callbacks), 1) + response = webob.Response() + result = callbacks[0](request, response) + self.assertEqual(result, None) + self.assertFalse('Set-Cookie' in dict(response.headerlist)) + + def test_set_cookie_with_exception(self): + import webob + request = testing.DummyRequest() + request.exception = True + session = self._makeOne(request) + session['a'] = 1 + callbacks = request.response_callbacks + self.assertEqual(len(callbacks), 1) + response = webob.Response() + result = callbacks[0](request, response) + self.assertEqual(result, None) + self.assertTrue('Set-Cookie' in dict(response.headerlist)) + + def test_cookie_is_set(self): + import webob + request = testing.DummyRequest() + session = self._makeOne(request) + session['a'] = 1 + callbacks = request.response_callbacks + self.assertEqual(len(callbacks), 1) + response = webob.Response() + result = callbacks[0](request, response) + self.assertEqual(result, None) + self.assertTrue('Set-Cookie' in dict(response.headerlist)) + +class TestBaseCookieSession(SharedCookieSessionTests, unittest.TestCase): + def _makeOne(self, request, **kw): + from pyramid.session import BaseCookieSessionFactory + return BaseCookieSessionFactory( + dummy_serialize, dummy_deserialize, **kw)(request) + + def _serialize(self, value): + return json.dumps(value) + + def test_reissue_not_triggered(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, reissue_time=1) + self.assertEqual(session['state'], 1) + self.assertFalse(session._dirty) + +class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase): + def _makeOne(self, request, **kw): + from pyramid.session import SignedCookieSessionFactory + kw.setdefault('secret', 'secret') + return SignedCookieSessionFactory(**kw)(request) + + def _serialize(self, value, salt=b'pyramid.session.', hashalg='sha512'): + import base64 + import hashlib + import hmac + import pickle + + digestmod = lambda: hashlib.new(hashalg) + cstruct = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) + sig = hmac.new(salt + b'secret', cstruct, digestmod).digest() + return base64.b64encode(cstruct + sig) + + def test_reissue_not_triggered(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, reissue_time=1) + self.assertEqual(session['state'], 1) + self.assertFalse(session._dirty) + + def test_custom_salt(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1}), salt=b'f.') + request.cookies['session'] = cookieval + session = self._makeOne(request, salt=b'f.') + self.assertEqual(session['state'], 1) + + def test_salt_mismatch(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1}), salt=b'f.') + request.cookies['session'] = cookieval + session = self._makeOne(request, salt=b'g.') + self.assertEqual(session, {}) + + def test_custom_hashalg(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1}), + hashalg='sha1') + request.cookies['session'] = cookieval + session = self._makeOne(request, hashalg='sha1') + self.assertEqual(session['state'], 1) + + def test_hashalg_mismatch(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1}), + hashalg='sha1') + request.cookies['session'] = cookieval + session = self._makeOne(request, hashalg='sha256') + self.assertEqual(session, {}) + + def test_secret_mismatch(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, secret='evilsecret') + self.assertEqual(session, {}) + + def test_custom_serializer(self): + import base64 + from hashlib import sha512 + import hmac + import time + request = testing.DummyRequest() + cstruct = dummy_serialize((time.time(), 0, {'state': 1})) + sig = hmac.new(b'pyramid.session.secret', cstruct, sha512).digest() + cookieval = base64.b64encode(cstruct + sig) + request.cookies['session'] = cookieval + session = self._makeOne(request, deserialize=dummy_deserialize) + self.assertEqual(session['state'], 1) + + def test_invalid_data_size(self): + from hashlib import sha512 + import base64 + request = testing.DummyRequest() + num_bytes = sha512().digest_size - 1 + cookieval = base64.b64encode(b' ' * num_bytes) + request.cookies['session'] = cookieval + session = self._makeOne(request) + self.assertEqual(session, {}) + + def test_very_long_key(self): + verylongkey = b'a' * 1024 + import webob + request = testing.DummyRequest() + session = self._makeOne(request, secret=verylongkey) + session['a'] = 1 + callbacks = request.response_callbacks + self.assertEqual(len(callbacks), 1) + response = webob.Response() + + try: + result = callbacks[0](request, response) + except TypeError as e: # pragma: no cover + self.fail('HMAC failed to initialize due to key length.') + + self.assertEqual(result, None) + self.assertTrue('Set-Cookie' in dict(response.headerlist)) + +class TestUnencryptedCookieSession(SharedCookieSessionTests, unittest.TestCase): + def setUp(self): + super(TestUnencryptedCookieSession, self).setUp() + from zope.deprecation import __show__ + __show__.off() + + def tearDown(self): + super(TestUnencryptedCookieSession, self).tearDown() + from zope.deprecation import __show__ + __show__.on() + + def _makeOne(self, request, **kw): + from pyramid.session import UnencryptedCookieSessionFactoryConfig + self._rename_cookie_var(kw, 'path', 'cookie_path') + self._rename_cookie_var(kw, 'domain', 'cookie_domain') + self._rename_cookie_var(kw, 'secure', 'cookie_secure') + self._rename_cookie_var(kw, 'httponly', 'cookie_httponly') + self._rename_cookie_var(kw, 'set_on_exception', 'cookie_on_exception') + return UnencryptedCookieSessionFactoryConfig('secret', **kw)(request) + + def _rename_cookie_var(self, kw, src, dest): + if src in kw: + kw.setdefault(dest, kw.pop(src)) + + def _serialize(self, value): + from pyramid.session import signed_serialize + return signed_serialize(value, 'secret') + def test_serialize_option(self): from pyramid.response import Response secret = 'secret' @@ -255,54 +466,48 @@ class Test_manage_accessed(unittest.TestCase): def test_accessed_set(self): request = testing.DummyRequest() session = DummySessionFactory(request) - session.accessed = None + session.renewed = 0 wrapper = self._makeOne(session.__class__.get) wrapper(session, 'a') self.assertNotEqual(session.accessed, None) - - def test_already_dirty(self): + self.assertTrue(session._dirty) + + def test_accessed_without_renew(self): + import time request = testing.DummyRequest() session = DummySessionFactory(request) - session._dirty = True - session['a'] = 1 + session._reissue_time = 5 + session.renewed = time.time() wrapper = self._makeOne(session.__class__.get) - self.assertEqual(wrapper.__doc__, session.get.__doc__) - result = wrapper(session, 'a') - self.assertEqual(result, 1) - callbacks = request.response_callbacks - self.assertEqual(len(callbacks), 0) + wrapper(session, 'a') + self.assertNotEqual(session.accessed, None) + self.assertFalse(session._dirty) - def test_with_exception(self): - import webob + def test_already_dirty(self): request = testing.DummyRequest() - request.exception = True session = DummySessionFactory(request) + session.renewed = 0 + session._dirty = True session['a'] = 1 wrapper = self._makeOne(session.__class__.get) self.assertEqual(wrapper.__doc__, session.get.__doc__) result = wrapper(session, 'a') self.assertEqual(result, 1) callbacks = request.response_callbacks - self.assertEqual(len(callbacks), 1) - response = webob.Response() - result = callbacks[0](request, response) - self.assertEqual(result, None) - self.assertFalse('Set-Cookie' in dict(response.headerlist)) + self.assertEqual(len(callbacks), 0) - def test_cookie_is_set(self): +class Test_manage_changed(unittest.TestCase): + def _makeOne(self, wrapped): + from pyramid.session import manage_changed + return manage_changed(wrapped) + + def test_it(self): request = testing.DummyRequest() session = DummySessionFactory(request) - session['a'] = 1 - wrapper = self._makeOne(session.__class__.get) - self.assertEqual(wrapper.__doc__, session.get.__doc__) - result = wrapper(session, 'a') - self.assertEqual(result, 1) - callbacks = request.response_callbacks - self.assertEqual(len(callbacks), 1) - response = DummyResponse() - result = callbacks[0](request, response) - self.assertEqual(result, None) - self.assertEqual(session.response, response) + wrapper = self._makeOne(session.__class__.__setitem__) + wrapper(session, 'a', 1) + self.assertNotEqual(session.accessed, None) + self.assertTrue(session._dirty) def serialize(data, secret): import hmac @@ -354,7 +559,7 @@ class Test_signed_deserialize(unittest.TestCase): def test_it_bad_encoding(self): serialized = 'bad' + serialize('123', 'secret') self.assertRaises(ValueError, self._callFUT, serialized, 'secret') - + class Test_check_csrf_token(unittest.TestCase): def _callFUT(self, *args, **kwargs): from ..session import check_csrf_token @@ -381,15 +586,22 @@ class Test_check_csrf_token(unittest.TestCase): self.assertEqual(self._callFUT(request), True) def test_failure_raises(self): - from pyramid.httpexceptions import HTTPBadRequest + from pyramid.exceptions import BadCSRFToken request = testing.DummyRequest() - self.assertRaises(HTTPBadRequest, self._callFUT, request, 'csrf_token') + self.assertRaises(BadCSRFToken, self._callFUT, request, + 'csrf_token') def test_failure_no_raises(self): request = testing.DummyRequest() result = self._callFUT(request, 'csrf_token', raises=False) self.assertEqual(result, False) +def dummy_serialize(value): + return json.dumps(value).encode('utf-8') + +def dummy_deserialize(value): + return json.loads(value.decode('utf-8')) + class DummySessionFactory(dict): _dirty = False _cookie_name = 'session' @@ -399,13 +611,14 @@ class DummySessionFactory(dict): _cookie_secure = False _cookie_httponly = False _timeout = 1200 - _secret = 'secret' + _reissue_time = 0 + def __init__(self, request): self.request = request dict.__init__(self, {}) - def _set_cookie(self, response): - self.response = response + def changed(self): + self._dirty = True class DummyResponse(object): def __init__(self): |
