diff options
| author | Steve Piercy <web@stevepiercy.com> | 2016-12-13 13:13:47 -0800 |
|---|---|---|
| committer | Steve Piercy <web@stevepiercy.com> | 2016-12-13 13:13:47 -0800 |
| commit | 9bbed79e04378d6534c07d73ab801191f0489e88 (patch) | |
| tree | 6ec01ee3cb5389cd0cbe8234280bf4408123659d | |
| parent | 4393f8b8e54e636b5655e0a8d2477e0b15820f68 (diff) | |
| parent | 884bcdc628e7144abf8e1cd1cde1ed3019e7e699 (diff) | |
| download | pyramid-9bbed79e04378d6534c07d73ab801191f0489e88.tar.gz pyramid-9bbed79e04378d6534c07d73ab801191f0489e88.tar.bz2 pyramid-9bbed79e04378d6534c07d73ab801191f0489e88.zip | |
Merge remote-tracking branch 'upstream/pcreate-to-cookiecutter' into pcreate-to-cookiecutter
85 files changed, 3004 insertions, 1468 deletions
diff --git a/.travis.yml b/.travis.yml index b46f677a6..02e844196 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,13 @@ matrix: env: TOXENV=docs - python: 3.5 env: TOXENV=pep8 - - python: nightly + - python: 3.6-dev env: TOXENV=py36 + - python: nightly + env: TOXENV=py37 allow_failures: - env: TOXENV=py36 + - env: TOXENV=py37 install: - travis_retry pip install tox diff --git a/CHANGES.txt b/CHANGES.txt index d4afe5f7a..8a6713783 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -16,15 +16,39 @@ Backward Incompatibilities See https://github.com/Pylons/pyramid/pull/2615 -- ``pcreate`` is now interactive by default. You will be prompted if it +- ``pcreate`` is now interactive by default. You will be prompted if a file already exists with different content. Previously if there were similar files it would silently skip them unless you specified ``--interactive`` or ``--overwrite``. See https://github.com/Pylons/pyramid/pull/2775 +- Removed undocumented argument ``cachebust_match`` from + ``pyramid.static.static_view``. This argument was shipped accidentally + in Pyramid 1.6. See https://github.com/Pylons/pyramid/pull/2681 + +- Change static view to avoid setting the ``Content-Encoding`` response header + to an encoding guessed using Python's ``mimetypes`` module. This was causing + clients to decode the content of gzipped files when downloading them. The + client would end up with a ``foo.txt.gz`` file on disk that was already + decoded, thus should really be ``foo.txt``. Also, the ``Content-Encoding`` + should only have been used if the client itself broadcast support for the + encoding via ``Accept-Encoding`` request headers. + See https://github.com/Pylons/pyramid/pull/2810 + +- Settings are no longer accessible as attributes on the settings object + (e.g. ``request.registry.settings.foo``). This was deprecated in Pyramid 1.2. + See https://github.com/Pylons/pyramid/pull/2823 + Features -------- +- Python 3.6 compatibility. + https://github.com/Pylons/pyramid/issues/2835 + +- pcreate learned about --package-name to allow you to create a new project in + an existing folder with a different package name than the project name. See + https://github.com/Pylons/pyramid/pull/2783 + - The `_get_credentials` private method of `BasicAuthAuthenticationPolicy` has been extracted into standalone function ``extract_http_basic_credentials` in `pyramid.authentication` module, this function extracts HTTP Basic @@ -58,6 +82,53 @@ Features - the pipeline can be optimized at config-time. See https://github.com/Pylons/pyramid/pull/2660 +- ``pserve`` should now work with ``gevent`` and other workers that need + to monkeypatch the process, assuming the server and / or the app do so + as soon as possible before importing the rest of pyramid. + See https://github.com/Pylons/pyramid/pull/2797 + +- Pyramid no longer copies the settings object passed to the + ``pyramid.config.Configurator(settings=)``. The original ``dict`` is kept. + See https://github.com/Pylons/pyramid/pull/2823 + +- The csrf trusted origins setting may now be a whitespace-separated list of + domains. Previously only a python list was allowed. Also, it can now be set + using the ``PYRAMID_CSRF_TRUSTED_ORIGINS`` environment variable similar to + other settings. See https://github.com/Pylons/pyramid/pull/2823 + +- ``pserve --reload`` now uses the + `hupper <http://docs.pylonsproject.org/projects/hupper/en/latest/>` + library to monitor file changes. This comes with many improvements: + + - If the `watchdog <http://pythonhosted.org/watchdog/>`_ package is + installed then monitoring will be done using inotify instead of + cpu and disk-intensive polling. + + - The monitor is now a separate process that will not crash and starts up + before any of your code. + + - The monitor will not restart the process after a crash until a file is + saved. + + - The monitor works on windows. + + - You can now trigger a reload manually from a pyramid view or any other + code via ``hupper.get_reloader().trigger_reload()``. Kind of neat. + + - You can trigger a reload by issuing a ``SIGHUP`` to the monitor process. + + See https://github.com/Pylons/pyramid/pull/2805 + +- A new ``[pserve]`` section is supported in your config files with a + ``watch_files`` key that can configure ``pserve --reload`` to monitor custom + file paths. See https://github.com/Pylons/pyramid/pull/2827 + +- Allow streaming responses to be made from subclasses of + ``pyramid.httpexceptions.HTTPException``. Previously the response would + be unrolled while testing for a body, making it impossible to stream + a response. + See https://github.com/Pylons/pyramid/pull/2863 + Bug Fixes --------- @@ -88,11 +159,24 @@ Bug Fixes from previous orders have executed. See https://github.com/Pylons/pyramid/pull/2757 +- Fix bug in i18n where the default domain would always use the Germanic plural + style, even if a different plural function is defined in the relevant + messages file. See https://github.com/Pylons/pyramid/pull/2859 + Deprecations ------------ Documentation Changes --------------------- +- Replace Typographical Conventions with an enhanced Style Guide. + https://github.com/Pylons/pyramid/pull/2838 + +- Add pyramid_nacl_session to session factories. + See https://github.com/Pylons/pyramid/issues/2791 + +- Update HACKING.txt from stale branch that was never merged to master. + See https://github.com/Pylons/pyramid/pull/2782 + - Updated Windows installation instructions and related bits. See https://github.com/Pylons/pyramid/issues/2661 diff --git a/HACKING.txt b/HACKING.txt index 4b237b56c..bbebb5165 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -3,14 +3,16 @@ Hacking on Pyramid Here are some guidelines for hacking on Pyramid. + Using a Development 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 use tox to set up a working development +installed (it's on PyPI), you can use ``tox`` to set up a working development environment. Each installation method is described below. + By Hand +++++++ @@ -26,180 +28,196 @@ By Hand substituting your account username and specifying the destination as "hack-on-pyramid". - $ cd ~ - $ git clone git@github.com:USERNAME/pyramid.git hack-on-pyramid - $ cd hack-on-pyramid - # Configure remotes such that you can pull changes from the Pyramid - # repository into your local repository. - $ git remote add upstream https://github.com/Pylons/pyramid.git - # fetch and merge changes from upstream into master - $ git fetch upstream - $ git merge upstream/master + $ cd ~ + $ git clone git@github.com:USERNAME/pyramid.git hack-on-pyramid + $ cd hack-on-pyramid + # Configure remotes such that you can pull changes from the Pyramid + # repository into your local repository. + $ git remote add upstream https://github.com/Pylons/pyramid.git + # fetch and merge changes from upstream into master + $ git fetch upstream + $ git merge upstream/master Now your local repo is set up such that you will push changes to your GitHub repo, from which you can submit a pull request. -- Create a virtualenv in which to install Pyramid: - - $ cd ~/hack-on-pyramid - $ virtualenv -ppython2.7 env +- Create a virtual environment in which to install Pyramid: - 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. - - 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. + $ cd ~/hack-on-pyramid + $ python3 -m venv 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): +- Install ``setuptools-git`` into the virtual environment (for good measure, as + we're using git to do version control): - $ $VENV/bin/easy_install setuptools-git + $ $VENV/bin/pip 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 - installs testing requirements such as nose and coverage. Running - ``setup.py dev`` *must* be done while the current working directory is the - ``pyramid`` checkout directory: +- Install Pyramid from the checkout into the virtual environment, where the + current working directory is the ``pyramid`` checkout directory. We will + install Pyramid in editable (development) mode as well as its testing + requirements. - $ cd ~/hack-on-pyramid - $ $VENV/bin/python setup.py dev + $ cd ~/hack-on-pyramid + $ $VENV/bin/pip install -e ".[testing,docs]" - Optionally create a new Pyramid project using ``pcreate``: - $ cd $VENV - $ bin/pcreate -s starter starter + $ cd $VENV + $ bin/pcreate -s starter starter + +- ...and install the new project into the virtual environment: -- ...and install the new project (also using ``setup.py develop``) into the - virtualenv: + $ cd $VENV/starter + $ $VENV/bin/pip install -e . - $ cd $VENV/starter - $ $VENV/bin/python setup.py develop -Using Tox -+++++++++ +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 + $ 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 . + $ git clone git://github.com/Pylons/pyramid.git . + + Alternatively, create a writeable fork on GitHub and clone it. - (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 virtual environment. A +quick way to achieve this is to use `tox +<http://tox.readthedocs.org/en/latest/>`_ with a custom configuration file +that is part of the checkout: -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 - tox -c hacking-tox.ini +This will create a python-2.7 based virtual environment named ``env27`` +(Pyramid's ``.gitconfig` ignores all top-level folders that start with ``env`` +specifically in our use case), and inside that a simple pyramid application +named ``hacking`` that you can then fire up like so: -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/pip install -e ".[testing,docs]" + $ ../bin/pserve development.ini - cd env27/hacking - ../bin/python setup.py develop - ../bin/pserve development.ini Adding Features --------------- In order to add a feature to Pyramid: -- The feature must be documented in both the API and narrative - documentation (in ``docs/``). +- The feature must be documented in both the API and narrative documentation + (in ``docs/``). -- The feature must work fully on the following CPython versions: 2.7, 3.4, - and 3.5 on both UNIX and Windows. +- The feature must work fully on the following CPython versions: 2.7, 3.4, 3.5, + and 3.6 on both UNIX and Windows. - The feature must work on the latest version of PyPy. -- The feature must not cause installation or runtime failure on App Engine. - If it doesn't cause installation or runtime failure, but doesn't actually - *work* on these platforms, that caveat should be spelled out in the - documentation. +- The feature must not depend on any particular persistence layer (filesystem, + SQL, etc). -- The feature must not depend on any particular persistence layer - (filesystem, SQL, etc). +- The feature must not add unnecessary dependencies (where "unnecessary" is of + course subjective, but new dependencies should be discussed). -- The feature must not add unnecessary dependencies (where - "unnecessary" is of course subjective, but new dependencies should - be discussed). +The above requirements are relaxed for scaffolding dependencies. If a scaffold +has an install-time dependency on something that doesn't work on a particular +platform, that caveat should be spelled out clearly in *its* documentation +(within its ``docs/`` directory). -The above requirements are relaxed for scaffolding dependencies. If a -scaffold has an install-time dependency on something that doesn't work on a -particular platform, that caveat should be spelled out clearly in *its* -documentation (within its ``docs/`` directory). Coding Style ------------ -- PEP8 compliance. Whitespace rules are relaxed: not necessary to put - 2 newlines between classes. But 79-column lines, in particular, are - mandatory. See - http://docs.pylonsproject.org/en/latest/community/codestyle.html for more +- PEP8 compliance. Whitespace rules are relaxed: not necessary to put two + newlines between classes. But 79-column lines, in particular, are mandatory. + See http://docs.pylonsproject.org/en/latest/community/codestyle.html for more information. - Please do not remove trailing whitespace. Configure your editor to reduce diff noise. See https://github.com/Pylons/pyramid/issues/788 for more. + Running Tests --------------- +------------- + +- To run all tests for Pyramid on a single Python version from your development + virtual environment (See *Using a Development Checkout* above), run + ``nosetests``: + + $ $VENV/bin/nosetests + +- To run individual tests (i.e., during development), you can use ``nosetests`` + syntax as follows: -- To run all tests for Pyramid on a single Python version, run ``nosetests`` - from your development virtualenv (See *Using a Development Checkout* above). + # run a single test + $ $VENV/bin/nosetests pyramid.tests.test_module:ClassName.test_mytestname -- To run individual tests (i.e. during development) you can use a regular - expression with the ``-t`` parameter courtesy of the `nose-selecttests - <https://pypi.python.org/pypi/nose-selecttests/>`_ plugin that's been - installed (along with nose itself) via ``python setup.py dev``. The - easiest usage is to simply provide the verbatim name of the test you're - working on. + # run all tests in a class + $ $VENV/bin/nosetests pyramid.tests.test_module:ClassName -- To run the full set of Pyramid tests on all platforms, install ``tox`` - (http://codespeak.net/~hpk/tox/) into a system Python. The ``tox`` console - script will be installed into the scripts location for that Python. While + Optionally you can install a nose plugin, `nose-selecttests + <https://pypi.python.org/pypi/nose-selecttests/>`_, and use a regular + expression with the ``-t`` parameter to run tests. + + # run a single test + $ $VENV/bin/nosetests -t test_mytestname + +- The ``tox.ini`` uses ``nose`` and ``coverage``. As such ``tox`` may be used + to run groups of tests or only a specific version of Python. For example, the + following command will run tests on Python 2.7 only without coverage: + + $ tox -e py27 + + This command will run tests on the latest versions of Python 2 and 3 with + coverage totaled for both versions. + + $ tox -e py2-cover,py3-cover,coverage + +- To run the full set of Pyramid tests on all platforms, install `tox + <http://codespeak.net/~hpk/tox/>`_ into a system Python. The ``tox`` console + script will be installed into the scripts location for that Python. While ``cd``'ed to the Pyramid checkout root directory (it contains ``tox.ini``), - invoke the ``tox`` console script. This will read the ``tox.ini`` file and - execute the tests on multiple Python versions and platforms; while it runs, - it creates a virtualenv for each version/platform combination. For - example:: + invoke the ``tox`` console script. This will read the ``tox.ini`` file and + execute the tests on multiple Python versions and platforms. While it runs, + it creates a virtual environment for each version/platform combination. For + example: - $ sudo /usr/bin/easy_install tox - $ cd ~/hack-on-pyramid/ - $ /usr/bin/tox + $ sudo /usr/bin/pip install tox + $ cd ~/hack-on-pyramid/ + $ /usr/bin/tox -- 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:: +- The tests can also be run using `pytest <http://pytest.org/>`_. This is + intended as a convenience for people who are more used to or fond of + ``pytest``. Run the tests like so: - $ $VENV/bin/easy_install pytest - $ $VENV/bin/py.test --strict pyramid/ + $ $VENV/bin/pip install pytest + $ $VENV/bin/py.test --strict pyramid/ + + To run individual tests (i.e., during development), see "py.test usage - + Specifying tests / selecting tests": + http://pytest.org/latest/usage.html#specifying-tests-selecting-tests - Functional tests related to the "scaffolds" (starter, zodb, alchemy) which - create a virtualenv, install the scaffold package and its dependencies, start - a server, and hit a URL on the server can be run like so:: + create a virtual environment, install the scaffold package and its + dependencies, start a server, and hit a URL on the server, can be run like + so: + + $ ./scaffoldtests.sh - $ ./scaffoldtests.sh + Alternatively: - Alternately:: + $ tox -e{py27,py34,py35,pypy}-scaffolds - $ tox -e{py27,py34,py35,pypy}-scaffolds, Test Coverage ------------- @@ -208,6 +226,7 @@ Test Coverage can test coverage via ``./coverage.sh`` (which itself just executes ``tox -epy2-cover,py3-cover,coverage``). + Documentation Coverage and Building HTML Documentation ------------------------------------------------------ @@ -217,13 +236,14 @@ changed to reflect the bug fix, ideally in the same commit that fixes the bug or adds the feature. To build and review docs, use the following steps. 1. In the main Pyramid checkout directory, run ``./builddocs.sh`` (which just - turns around and runs ``tox -e docs``):: + turns around and runs ``tox -e docs``): - $ ./builddocs.sh + $ ./builddocs.sh 2. Open the ``docs/_build/html/index.html`` file to see the resulting HTML rendering. + Change Log ---------- @@ -231,4 +251,3 @@ Change Log file in the prevailing style. Changelog entries should be long and descriptive, not cryptic. Other developers should be able to know what your changelog entry means. - diff --git a/RELEASING.txt b/RELEASING.txt index 4690fbd37..73cf38aa7 100644 --- a/RELEASING.txt +++ b/RELEASING.txt @@ -33,8 +33,8 @@ Prepare new release branch - Run tests on Windows if feasible. -- Make sure all scaffold tests pass (CPython 2.7, 3.4, and 3.5, and PyPy on - UNIX; this doesn't work on Windows): +- Make sure all scaffold tests pass (CPython 2.7, 3.4, 3.5, and 3.6, and PyPy + on UNIX; this doesn't work on Windows): $ ./scaffoldtests.sh @@ -114,15 +114,9 @@ Nice-to-Have Future ------ -- 1.6: turn ``pyramid.settings.Settings`` into a function that returns the - original dict (after ``__getattr__`` deprecation period, it was deprecated - in 1.2). - - 1.6: Remove IContextURL and TraversalContextURL. -- 1.8: Remove set_request_property. -- 1.8: Drop Python 3.3 support. - +- 1.9: Remove set_request_property. - 1.9: Remove extra code enabling ``pyramid.security.remember(principal=...)`` and force use of ``userid``. diff --git a/appveyor.yml b/appveyor.yml index 1350507b2..4c684bfc6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,6 +2,13 @@ environment: matrix: - PYTHON: "C:\\Python35" TOXENV: "py35" + - PYTHON: "C:\\Python27" + TOXENV: "py27" + +cache: + - '%LOCALAPPDATA%\pip\Cache' + +version: '{branch}.{build}' install: - "%PYTHON%\\python.exe -m pip install tox" diff --git a/docs/api/i18n.rst b/docs/api/i18n.rst index 3b9abbc1d..7a61246df 100644 --- a/docs/api/i18n.rst +++ b/docs/api/i18n.rst @@ -6,6 +6,7 @@ .. automodule:: pyramid.i18n .. autoclass:: TranslationString + :noindex: .. autofunction:: TranslationStringFactory diff --git a/docs/conf.py b/docs/conf.py index c3a7170fc..12dd27722 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,9 +51,10 @@ book = os.environ.get('BOOK') extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', - 'repoze.sphinx.autointerface', - 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.viewcode', + 'repoze.sphinx.autointerface', 'sphinxcontrib.programoutput', # enable pylons_sphinx_latesturl when this branch is no longer "latest" # 'pylons_sphinx_latesturl', @@ -68,6 +69,7 @@ intersphinx_mapping = { 'pylonswebframework': ('http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/', None), 'python': ('https://docs.python.org/3', None), 'pytest': ('http://pytest.org/latest/', None), + 'sphinx': ('http://www.sphinx-doc.org/en/latest', None), 'sqla': ('http://docs.sqlalchemy.org/en/latest', None), 'tm': ('http://docs.pylonsproject.org/projects/pyramid-tm/en/latest/', None), 'toolbar': ('http://docs.pylonsproject.org/projects/pyramid-debugtoolbar/en/latest', None), @@ -119,6 +121,9 @@ exclude_patterns = ['_themes/README.rst', ] # unit titles (such as .. function::). add_module_names = False +# Add support for todo items +todo_include_todos = True + # The name of the Pygments (syntax highlighting) style to use. #pygments_style = book and 'bw' or 'tango' if book: @@ -191,10 +196,10 @@ latex_documents = [ # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -latex_use_parts = True +latex_toplevel_sectioning = "section" # If false, no module index is generated. -latex_use_modindex = False +latex_domain_indices = False ## Say, for a moment that you have a twoside document that needs a 3cm ## inner margin to allow for binding and at least two centimetres the diff --git a/docs/conventions.rst b/docs/conventions.rst deleted file mode 100644 index de041da04..000000000 --- a/docs/conventions.rst +++ /dev/null @@ -1,107 +0,0 @@ -Typographical Conventions -========================= - -Literals, filenames, and function arguments are presented using the -following style: - - ``argument1`` - -Warnings which represent limitations and need-to-know information -related to a topic or concept are presented in the following style: - - .. warning:: - - This is a warning. - -Notes which represent additional information related to a topic or -concept are presented in the following style: - - .. note:: - - This is a note. - -We present Python method names using the following style: - - :meth:`pyramid.config.Configurator.add_view` - -We present Python class names, module names, attributes, and global -variables using the following style: - - :class:`pyramid.config.Configurator.registry` - -References to glossary terms are presented using the following style: - - :term:`Pylons` - -URLs are presented using the following style: - - `Pylons <http://www.pylonsproject.org>`_ - -References to sections and chapters are presented using the following -style: - - :ref:`traversal_chapter` - -Code and configuration file blocks are presented in the following style: - - .. code-block:: python - :linenos: - - def foo(abc): - pass - -Example blocks representing UNIX shell commands are prefixed with a ``$`` -character, e.g.: - - .. code-block:: bash - - $ $VENV/bin/py.test -q - -See :term:`venv` for the meaning of ``$VENV``. - -Example blocks representing Windows commands are prefixed with a drive letter -with an optional directory name, e.g.: - - .. code-block:: doscon - - c:\examples> %VENV%\Scripts\py.test -q - -See :term:`venv` for the meaning of ``%VENV%``. - -When a command that should be typed on one line is too long to fit on a page, -the backslash ``\`` is used to indicate that the following printed line should -be part of the command: - - .. code-block:: bash - - $VENV/bin/py.test tutorial/tests.py --cov-report term-missing \ - --cov=tutorial -q - -A sidebar, which presents a concept tangentially related to content discussed -on a page, is rendered like so: - -.. sidebar:: This is a sidebar - - Sidebar information. - -When multiple objects are imported from the same package, the following -convention is used: - - .. code-block:: python - - from foo import ( - bar, - baz, - ) - -It may look unusual, but it has advantages: - -* It allows one to swap out the higher-level package ``foo`` for something else - that provides the similar API. An example would be swapping out one database - for another (e.g., graduating from SQLite to PostgreSQL). - -* Looks more neat in cases where a large number of objects get imported from - that package. - -* Adding or removing imported objects from the package is quicker and results - in simpler diffs. diff --git a/docs/index.rst b/docs/index.rst index 02c35866a..a783e8a70 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -213,13 +213,14 @@ Copyright, Trademarks, and Attributions copyright -Typographical Conventions -========================= +Typographical Conventions and Style Guide +========================================= .. toctree:: :maxdepth: 1 - conventions + typographical-conventions + style-guide Index and Glossary diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 05199d313..83a139917 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -15,7 +15,7 @@ Front Matter :maxdepth: 1 copyright - conventions + style-guide authorintro designdefense diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index b22b31bf9..d21edc7b4 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -116,14 +116,6 @@ callable: .. note:: - Both :meth:`pyramid.config.Configurator.add_notfound_view` and - :class:`pyramid.view.notfound_view_config` are new as of Pyramid 1.3. - Older Pyramid documentation instructed users to use ``add_view`` instead, - with a ``context`` of ``HTTPNotFound``. This still works; the convenience - method and decorator are just wrappers around this functionality. - -.. warning:: - When a Not Found View callable accepts an argument list as described in :ref:`request_and_context_view_definitions`, the ``context`` passed as the first argument to the view callable will be the @@ -131,6 +123,13 @@ callable: available, the resource context will still be available as ``request.context``. +.. warning:: + + The :term:`Not Found View` callables are only invoked when a + :exc:`~pyramid.httpexceptions.HTTPNotFound` exception is raised. If the + exception is returned from a view then it will be treated as a regular + response object and it will not trigger the custom view. + .. index:: single: forbidden view @@ -210,6 +209,13 @@ Here's some sample code that implements a minimal forbidden view: whether the ``pyramid.debug_authorization`` environment setting is true or false. +.. warning:: + + The :term:`forbidden view` callables are only invoked when a + :exc:`~pyramid.httpexceptions.HTTPForbidden` exception is raised. If the + exception is returned from a view then it will be treated as a regular + response object and it will not trigger the custom view. + .. index:: single: request factory @@ -744,7 +750,9 @@ The API that must be implemented by a class that provides """ Accept the resource and request and set self.physical_path and self.virtual_path """ self.virtual_path = some_function_of(resource, request) + self.virtual_path_tuple = some_function_of(resource, request) self.physical_path = some_other_function_of(resource, request) + self.physical_path_tuple = some_function_of(resource, request) The default context URL generator is available for perusal as the class :class:`pyramid.traversal.ResourceURL` in the `traversal module diff --git a/docs/narr/install.rst b/docs/narr/install.rst index 570cb2285..c3c2ba64c 100644 --- a/docs/narr/install.rst +++ b/docs/narr/install.rst @@ -22,7 +22,7 @@ the following sections. .. sidebar:: Python Versions As of this writing, :app:`Pyramid` is tested against Python 2.7, - Python 3.4, Python 3.5, PyPy. + Python 3.4, Python 3.5, Python 3.6, and PyPy. :app:`Pyramid` is known to run on all popular UNIX-like systems such as Linux, Mac OS X, and FreeBSD, as well as on Windows platforms. It is also known to diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 47638579b..adad196e4 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -860,7 +860,7 @@ Every release of Pyramid has 100% statement coverage via unit and integration tests, as measured by the ``coverage`` tool available on PyPI. It also has greater than 95% decision/condition coverage as measured by the ``instrumental`` tool available on PyPI. It is automatically tested by Travis, -and Jenkins on Python 2.7, Python 3.4, Python 3.5, and PyPy +and Jenkins on Python 2.7, Python 3.4, Python 3.5, Python 3.6, and PyPy after each commit to its GitHub repository. Official Pyramid add-ons are held to a similar testing standard. We still find bugs in Pyramid and its official add-ons, but we've noticed we find a lot more of them while working on other diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 71bd176f6..77c637571 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -1045,3 +1045,45 @@ Another good production alternative is :term:`Green Unicorn` (aka mod_wsgi, although it depends, in its default configuration, on having a buffering HTTP proxy in front of it. It does not, as of this writing, work on Windows. + +Automatically Reloading Your Code +--------------------------------- + +During development, it can be really useful to automatically have the +webserver restart when you make changes. ``pserve`` has a ``--reload`` switch +to enable this. It uses the +`hupper <http://docs.pylonsproject.org/projects/hupper/en/latest/>` package +to enable this behavior. When your code crashes, ``hupper`` will wait for +another change or the ``SIGHUP`` signal before restarting again. + +inotify support +~~~~~~~~~~~~~~~ + +By default, ``hupper`` will poll the filesystem for changes to all python +code. This can be pretty inefficient in larger projects. To be nicer to your +hard drive, you should install the +`watchdog <http://pythonhosted.org/watchdog/>` package in development. +``hupper`` will automatically use ``watchdog`` to more efficiently poll the +filesystem. + +Monitoring Custom Files +~~~~~~~~~~~~~~~~~~~~~~~ + +By default, ``pserve --reload`` will monitor all imported Python code +(everything in ``sys.modules``) as well as the config file passed to +``pserve`` (e.g. ``development.ini``). You can instruct ``pserve`` to watch +other files for changes as well by defining a ``[pserve]`` section in your +configuration file. For example, let's say your application loads the +``favicon.ico`` file at startup and stores it in memory to efficiently +serve it many times. When you change it you want ``pserve`` to restart: + +.. code-block:: ini + + [pserve] + watch_files = + myapp/static/favicon.ico + +Paths may be absolute or relative to the configuration file. They may also +be an :term:`asset specification`. These paths are passed to ``hupper`` which +has some basic support for globbing. Acceptable glob patterns depend on the +version of Python being used. diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst index a1319e45f..5b24201a9 100644 --- a/docs/narr/sessions.rst +++ b/docs/narr/sessions.rst @@ -157,6 +157,12 @@ The following session factories exist at the time of this writing. ======================= ======= ============================= Session Factory Backend Description ======================= ======= ============================= +pyramid_nacl_session_ PyNaCl_ Defines an encrypting, + pickle-based cookie + serializer, using PyNaCl to + generate the symmetric + encryption for the cookie + state. pyramid_redis_sessions_ Redis_ Server-side session library for Pyramid, using Redis for storage. @@ -165,6 +171,9 @@ pyramid_beaker_ Beaker_ Session factory for Pyramid sessioning system. ======================= ======= ============================= +.. _pyramid_nacl_session: https://pypi.python.org/pypi/pyramid_nacl_session +.. _PyNaCl: https://pynacl.readthedocs.io/en/latest/secret/ + .. _pyramid_redis_sessions: https://pypi.python.org/pypi/pyramid_redis_sessions .. _Redis: http://redis.io/ diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 7cb8e0306..3b683ff79 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -252,7 +252,7 @@ Non-Predicate Arguments def myview(request): ... - Is similar to doing:: + Is similar to decorating the view callable directly:: @view_config(...) @decorator2 @@ -260,8 +260,10 @@ Non-Predicate Arguments def myview(request): ... - All view callables in the decorator chain must return a response object - implementing :class:`pyramid.interfaces.IResponse` or raise an exception: + An important distinction is that each decorator will receive a response + object implementing :class:`pyramid.interfaces.IResponse` instead of the + raw value returned from the view callable. All decorators in the chain must + return a response object or raise an exception: .. code-block:: python diff --git a/docs/quick_tour.rst b/docs/quick_tour.rst index 39b4cafb3..5dc7d8816 100644 --- a/docs/quick_tour.rst +++ b/docs/quick_tour.rst @@ -26,7 +26,7 @@ To save a little bit of typing and to be certain that we use the modules, scripts, and packages installed in our virtual environment, we'll set an environment variable, too. -As an example, for Python 3.5+ on Linux: +As an example, for Python 3.6+ on Linux: .. parsed-literal:: @@ -504,7 +504,7 @@ Pyramid's ``pcreate`` command can list the available scaffolds: .. code-block:: bash - $ pcreate --list + $ $VENV/bin/pcreate --list Available scaffolds: alchemy: Pyramid project using SQLAlchemy, SQLite, URL dispatch, and Jinja2 pyramid_jinja2_starter: Pyramid Jinja2 starter project @@ -517,7 +517,7 @@ that scaffold to make our project: .. code-block:: bash - $ pcreate --scaffold pyramid_jinja2_starter hello_world + $ $VENV/bin/pcreate --scaffold pyramid_jinja2_starter hello_world We next use the normal Python command to set up our package for development: @@ -678,10 +678,10 @@ egregious, as Pyramid has had a deep commitment to full test coverage since before its release. Our ``pyramid_jinja2_starter`` scaffold generated a ``tests.py`` module with -one unit test in it. To run it, let's install the handy ``pytest`` test runner -by editing ``setup.py``. While we're at it, we'll throw in the ``pytest-cov`` -tool which yells at us for code that isn't tested. Insert and edit the -following lines as shown: +one unit test in it. It also configured ``setup.py`` with test requirements: +``py.test`` as the test runner, ``WebTest`` for running view tests, and the +``pytest-cov`` tool which yells at us for code that isn't tested. The +highlighted lines show this: .. code-block:: python :linenos: @@ -711,7 +711,7 @@ following lines as shown: 'testing': tests_require, }, -We changed ``setup.py`` which means we need to rerun ``$VENV/bin/pip install -e +To install the test requirements, run ``$VENV/bin/pip install -e ".[testing]"``. We can now run all our tests: .. code-block:: bash @@ -729,7 +729,7 @@ This yields the following output. collected 1 items hello_world/tests.py . - ------------- coverage: platform darwin, python 3.5.0-final-0 ------------- + ------------- coverage: platform darwin, python 3.6.0-final-0 ------------- Name Stmts Miss Cover Missing -------------------------------------------------------- hello_world/__init__.py 11 8 27% 11-23 diff --git a/docs/quick_tutorial/requirements.rst b/docs/quick_tutorial/requirements.rst index afa8ed104..913e08a62 100644 --- a/docs/quick_tutorial/requirements.rst +++ b/docs/quick_tutorial/requirements.rst @@ -19,11 +19,11 @@ virtual environment.) This *Quick Tutorial* is based on: -* **Python 3.5**. Pyramid fully supports Python 3.4+ and Python 2.7+. This - tutorial uses **Python 3.5** but runs fine under Python 2.7. +* **Python 3.6**. Pyramid fully supports Python 3.4+ and Python 2.7+. This + tutorial uses **Python 3.6** but runs fine under Python 2.7. * **venv**. We believe in virtual environments. For this tutorial, we use - Python 3.5's built-in solution :term:`venv`. For Python 2.7, you can install + Python 3.6's built-in solution :term:`venv`. For Python 2.7, you can install :term:`virtualenv`. * **pip**. We use :term:`pip` for package management. diff --git a/docs/style-guide.rst b/docs/style-guide.rst new file mode 100644 index 000000000..bdca45a06 --- /dev/null +++ b/docs/style-guide.rst @@ -0,0 +1,1258 @@ +.. _style-guide: + +Style Guide +=========== + +.. meta:: + :description: This chapter describes how to edit, update, and build the Pyramid documentation. + :keywords: Pyramid, Style Guide + + +.. _style-guide-introduction: + +Introduction +------------ + +This chapter provides details of how to contribute updates to the documentation following style guidelines and conventions. We provide examples, including reStructuredText code and its rendered output for both visual and technical reference. + +For coding style guidelines, see `Coding Style <http://docs.pylonsproject.org/en/latest/community/codestyle.html#coding-style>`_. + + +.. _style-guide-contribute: + +How to update and contribute to documentation +--------------------------------------------- + +All projects under the Pylons Projects, including this one, follow the guidelines established at `How to Contribute <http://www.pylonsproject.org/community/how-to-contribute>`_ and `Coding Style and Standards <http://docs.pylonsproject.org/en/latest/community/codestyle.html>`_. + +By building the documentation locally, you can preview the output before committing and pushing your changes to the repository. Follow the instructions for `Building documentation for a Pylons Project project <https://github.com/Pylons/pyramid/blob/master/contributing.md#building-documentation-for-a-pylons-project-project>`_. These instructions also include how to install packages required to build the documentation, and how to follow our recommended git workflow. + +When submitting a pull request for the first time in a project, sign `CONTRIBUTORS.txt <https://github.com/Pylons/pyramid/blob/master/CONTRIBUTORS.txt>`_ and commit it along with your pull request. + + +.. _style-guide-file-conventions: + +Location, referencing, and naming of files +------------------------------------------ + +* reStructuredText (reST) files must be located in ``docs/`` and its subdirectories. +* Image files must be located in ``docs/_static/``. +* reST directives must refer to files either relative to the source file or absolute from the top source directory. For example, in ``docs/narr/source.rst``, you could refer to a file in a different directory as either ``.. include:: ../diff-dir/diff-source.rst`` or ``.. include:: /diff-dir/diff-source.rst``. +* File names should be lower-cased and have words separated with either a hyphen "-" or an underscore "_". +* reST files must have an extension of ``.rst``. +* Image files may be any format but must have lower-cased file names and have standard file extensions that consist three letters (``.gif``, ``.jpg``, ``.png``, ``.svg``). ``.gif`` and ``.svg`` are not currently supported by PDF builders in Sphinx, but you can allow the Sphinx builder to automatically select the correct image format for the desired output by replacing the three-letter file extension with ``*``. For example: + + .. code-block:: rst + + .. image:: ../_static/pyramid_request_processing. + + will select the image ``pyramid_request_processing.svg`` for the HTML documentation builder, and ``pyramid_request_processing.png`` for the PDF builder. See the related `Stack Overflow post <http://stackoverflow.com/questions/6473660/using-sphinx-docs-how-can-i-specify-png-image-formats-for-html-builds-and-pdf-im/6486713#6486713>`_. + + +.. _style-guide-table-of-contents-tree: + +Table of contents tree +---------------------- + +To insert a table of contents (TOC), use the ``toctree`` directive. Entries listed under the ``toctree`` directive follow :ref:`location conventions <style-guide-file-conventions>`. A numeric ``maxdepth`` option may be given to indicate the depth of the tree; by default, all levels are included. + +.. code-block:: rst + + .. toctree:: + :maxdepth: 2 + + narr/introduction + narr/install + +The above code renders as follows. + +.. toctree:: + :maxdepth: 2 + + narr/introduction + narr/install + +Globbing can be used. + +.. code-block:: rst + + .. toctree:: + :maxdepth: 1 + :glob: + + pscripts/index + pscripts/* + +The above code renders as follows. + +.. toctree:: + :maxdepth: 1 + :glob: + + pscripts/index + pscripts/* + +To notify Sphinx of the document hierarchy, but not insert links into the document at the location of the directive, use the option ``hidden``. This makes sense when you want to insert these links yourself, in a different style, or in the HTML sidebar. + +.. code-block:: rst + + .. toctree:: + :hidden: + + quick_tour + + * :doc:`quick_tour` gives an overview of the major features in Pyramid, covering a little about a lot. + +The above code renders as follows. + +.. toctree:: + :hidden: + + quick_tour + +* :doc:`quick_tour` gives an overview of the major features in Pyramid, covering a little about a lot. + +.. seealso:: Sphinx documentation of :ref:`toctree-directive`. + + +.. _style-guide-glossary: + +Glossary +-------- + +A glossary defines terms used throughout the documentation. + +The glossary file must be named ``glossary.rst``. Its content must begin with the directive ``glossary``. An optional ``sorted`` argument should be used to sort the terms alphabetically when rendered, making it easier for the user to find a given term. Without the argument ``sorted``, terms will appear in the order of the ``glossary`` source file. + +.. code-block:: rst + + .. glossary:: + :sorted: + + voom + Theoretically, the sound a parrot makes when four-thousand volts of electricity pass through it. + + pining + What the Norwegien Blue does when it misses its homeland, e.g., pining for the fjords. + +The above code renders as follows. + +.. glossary:: + :sorted: + + voom + Theoretically, the sound a parrot makes when four-thousand volts of electricity pass through it. + + pining + What the Norwegien Blue does when it misses its homeland, e.g., pining for the fjords. + +References to glossary terms use the ``term`` directive. + +.. code-block:: rst + + :term:`voom` + +The above code renders as follows. Note it is hyperlinked, and when clicked it will take the user to the term in the Glossary and highlight the term. + +:term:`voom` + + +.. _style-guide-section-structure: + +Section structure +----------------- + +Each section, or a subdirectory of reST files, such as a tutorial, must contain an ``index.rst`` file. ``index.rst`` must contain the following. + +* A section heading. This will be visible in the table of contents. +* A single paragraph describing this section. +* A Sphinx ``toctree`` directive, with a ``maxdepth`` of 2. Each ``.rst`` file in the folder should be linked to this ``toctree``. + + .. code-block:: rst + + .. toctree:: + :maxdepth: 2 + + chapter1 + chapter2 + chapter3 + + +.. _style-guide-page-structure: + +Page structure +-------------- + +Each page should contain in order the following. + +#. The main heading. This will be visible in the table of contents. + + .. code-block:: rst + + ================ + The main heading + ================ + +#. Meta tag information. The "meta" directive is used to specify HTML metadata stored in HTML META tags. "Metadata" is data about data, in this case data about web pages. Metadata is used to describe and classify web pages in the World Wide Web, in a form that is easy for search engines to extract and collate. + + .. code-block:: rst + + .. meta:: + :description: This chapter describes how to edit, update, and build the Pyramid documentation. + :keywords: Pyramid, Style Guide + + The above code renders as follows. + + .. code-block:: xml + + <meta content="This chapter describes how to edit, update, and build the Pyramid documentation." name="description" /> + <meta content="Pyramid, Style Guide" name="keywords" /> + +#. Introduction paragraph. + + .. code-block:: rst + + Introduction + ------------ + + This chapter is an introduction. + +#. Finally the content of the document page, consisting of reST elements such as headings, paragraphs, tables, and so on. + + +.. _style-guide-page-content: + +Page content +------------ + +Within a page, content should adhere to specific guidelines. + + +.. _style-guide-line-lengths: + +Line lengths +^^^^^^^^^^^^ + +Narrative documentation is not code, and should therefore not adhere to PEP8 or other line length conventions. When a translator sees only part of a sentence or paragraph, it makes it more difficult to translate the concept. Line lengths make ``diff`` more difficult. Text editors can soft wrap lines for display to avoid horizontal scrolling. We admit, we boofed it by using arbitrary 79-character line lengths in our own documentation, but we have seen the error of our ways and wish to correct this going forward. + + +.. _style-guide-trailing-white-space: + +Trailing white spaces +^^^^^^^^^^^^^^^^^^^^^ + +* No trailing white spaces. +* Always use a line feed or carriage return at the end of a file. + + +.. _style-guide-indentation: + +Indentation +^^^^^^^^^^^ + +* Indent using four spaces, except for :ref:`nested lists <style-guide-lists>`. +* Do not use tabs to indent. + + +.. _style-guide-grammar-spelling-preferences: + +Grammar, spelling, and capitalization preferences +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use any commercial or free professional style guide in general. Use a spell- and grammar-checker. The following table lists the preferred grammar, spelling, and capitalization of words and phrases for frequently used items in the documentation. + +========== ===== +Preferred Avoid +========== ===== +add-on addon +and so on etc. +GitHub Github, github +JavaScript Javascript, javascript +plug-in plugin +select check, tick (checkbox) +such as like +verify be sure +========== ===== + + +.. _style-guide-headings: + +Headings +^^^^^^^^ + +Capitalize only the first letter in a heading (sentence-case), unless other words are proper nouns or acronyms, e.g., "Pyramid" or "HTML". + +For consistent heading characters throughout the documentation, follow the guidelines stated in the `Python Developer's Guide <https://docs.python.org/devguide/documenting.html#sections>`_. Specifically: + +* =, for sections +* -, for subsections +* ^, for subsubsections +* ", for paragraphs + +As individual files do not have so-called "parts" or "chapters", the headings would be underlined with characters as shown. + + .. code-block:: rst + + ================================== + The main heading or web page title + ================================== + + Heading Level 1 + --------------- + + Heading Level 2 + ^^^^^^^^^^^^^^^ + + Heading Level 3 + """"""""""""""" + +Note, we do not render heading levels here because doing so causes a loss in page structure. + + +.. _style-guide-paragraphs: + +Paragraphs +^^^^^^^^^^ + +A paragraph should be on one line. Paragraphs must be separated by two line feeds. + + +.. _style-guide-links: + +Links +^^^^^ + +Use inline links to keep the context or link label together with the URL. Do not use targets and links at the end of the page, because the separation makes it difficult to update and translate. Here is an example of inline links, our required method. + +.. code-block:: rst + + `TryPyramid <https://trypyramid.com>`_ + +The above code renders as follows. + +`TryPyramid <https://TryPyramid.com>`_ + +.. seealso:: See also :ref:`style-guide-cross-references` for generating links throughout the entire documentation. + + +.. _style-guide-topic: + +Topic +^^^^^ + +A topic is similar to a block quote with a title, or a self-contained section with no subsections. Use the ``topic`` directive to indicate a self-contained idea that is separate from the flow of the document. Topics may occur anywhere a section or transition may occur. Body elements and topics may not contain nested topics. + +The directive's sole argument is interpreted as the topic title, and next line must be blank. All subsequent lines make up the topic body, interpreted as body elements. + + .. code-block:: rst + + .. topic:: Topic Title + + Subsequent indented lines comprise + the body of the topic, and are + interpreted as body elements. + +The above code renders as follows. + +.. topic:: Topic Title + + Subsequent indented lines comprise + the body of the topic, and are + interpreted as body elements. + +.. _style-guide-displaying-code: + +Displaying code +^^^^^^^^^^^^^^^ + +Code may be displayed in blocks or inline. You can include blocks of code from other source files. Blocks of code should use syntax highlighting, and may use line numbering or emphasis. + +.. seealso:: See also the Sphinx documentation for :ref:`code-examples`. + + +.. _style-guide-syntax-highlighting: + +Syntax highlighting +""""""""""""""""""" + +Sphinx does syntax highlighting of code blocks using the `Pygments <http://pygments.org/>`_ library. + +Do not use two colons "::" at the end of a line, followed by a blank line, then code. Always specify the language to be used for syntax highlighting by using a language argument in the ``code-block`` directive. Always indent the subsequent code. + +.. code-block:: rst + + .. code-block:: python + + if "foo" == "bar": + # This is Python code + pass + +XML: + +.. code-block:: rst + + .. code-block:: xml + + <somesnippet>Some XML</somesnippet> + +Unix shell commands are prefixed with a ``$`` character. (See :term:`venv` for the meaning of ``$VENV``.) + +.. code-block:: rst + + .. code-block:: bash + + $ $VENV/bin/pip install -e . + +Windows commands are prefixed with a drive letter with an optional directory name. (See :term:`venv` for the meaning of ``%VENV%``.) + +.. code-block:: rst + + .. code-block:: doscon + + c:\> %VENV%\Scripts\pcreate -s starter MyProject + +cfg: + +.. code-block:: rst + + .. code-block:: cfg + + [some-part] + # A random part in the buildout + recipe = collective.recipe.foo + option = value + +ini: + +.. code-block:: rst + + .. code-block:: ini + + [nosetests] + match=^test + where=pyramid + nocapture=1 + +Interactive Python: + +.. code-block:: rst + + .. code-block:: pycon + + >>> class Foo: + ... bar = 100 + ... + >>> f = Foo() + >>> f.bar + 100 + >>> f.bar / 0 + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + ZeroDivisionError: integer division or modulo by zero + +If syntax highlighting is not enabled for your code block, you probably have a syntax error and Pygments will fail silently. + +View the `full list of lexers and associated short names <http://pygments.org/docs/lexers/>`_. + + +.. _style-guide-parsed-literals: + +Parsed literals +""""""""""""""" + +Parsed literals are used to render, for example, a specific version number of the application in code blocks. Use the directive ``parsed-literal``. Note that syntax highlighting is not supported and code is rendered as plain text. + +.. code-block:: rst + + .. parsed-literal:: + + $ $VENV/bin/pip install "pyramid==\ |release|\ " + +The above code renders as follows. + +.. parsed-literal:: + + $ $VENV/bin/pip install "pyramid==\ |release|\ " + + +.. _style-guide-long-commands: + +Displaying long commands +"""""""""""""""""""""""" + +When a command that should be typed on one line is too long to fit on the displayed width of a page, the backslash character ``\`` is used to indicate that the subsequent printed line should be part of the command: + +.. code-block:: rst + + .. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py --cov-report term-missing \ + --cov=tutorial -q + + +.. _style-guide-code-block-options: + +Code block options +"""""""""""""""""" + +To emphasize lines (give the appearance that a highlighting pen has been used on the code), use the ``emphasize-lines`` option. The argument passed to ``emphasize-lines`` must be a comma-separated list of either single or ranges of line numbers. + +.. code-block:: rst + + .. code-block:: python + :emphasize-lines: 1,3 + + if "foo" == "bar": + # This is Python code + pass + +The above code renders as follows. + +.. code-block:: python + :emphasize-lines: 1,3 + + if "foo" == "bar": + # This is Python code + pass + +To display a code block with line numbers, use the ``linenos`` option. + +.. code-block:: rst + + .. code-block:: python + :linenos: + + if "foo" == "bar": + # This is Python code + pass + +The above code renders as follows. + +.. code-block:: python + :linenos: + + if "foo" == "bar": + # This is Python code + pass + +Code blocks may be given a caption, which may serve as a filename or other description, using the ``caption`` option. They may also be given a ``name`` option, providing an implicit target name that can be referenced by using ``ref`` (see :ref:`style-guide-cross-referencing-arbitrary-locations`). + +.. code-block:: rst + + .. code-block:: python + :caption: sample.py + :name: sample-py + + if "foo" == "bar": + # This is Python code + pass + +The above code renders as follows. + +.. code-block:: python + :caption: sample.py + :name: sample-py + + if "foo" == "bar": + # This is Python code + pass + +To specify the starting number to use for line numbering, use the ``lineno-start`` directive. + +.. code-block:: rst + + .. code-block:: python + :lineno-start: 2 + + if "foo" == "bar": + # This is Python code + pass + +The above code renders as follows. As you can see, ``lineno-start`` is not altogether accurate. + +.. code-block:: python + :lineno-start: 2 + + if "foo" == "bar": + # This is Python code + pass + + +.. _style-guide-includes: + +Includes +"""""""" + +Longer displays of verbatim text may be included by storing the example text in an external file containing only plain text or code. The file may be included using the ``literalinclude`` directive. The file name follows the conventions of :ref:`style-guide-file-conventions`. + +.. code-block:: rst + + .. literalinclude:: narr/helloworld.py + :language: python + +The above code renders as follows. + +.. literalinclude:: narr/helloworld.py + :language: python + +Like code blocks, ``literalinclude`` supports the following options. + +* ``language`` to select a language for syntax highlighting +* ``linenos`` to switch on line numbers +* ``lineno-start`` to specify the starting number to use for line numbering +* ``emphasize-lines`` to emphasize particular lines + +.. code-block:: rst + + .. literalinclude:: narr/helloworld.py + :language: python + :linenos: + :lineno-start: 11 + :emphasize-lines: 1,6-7,9- + +The above code renders as follows. Note that ``lineno-start`` and ``emphasize-lines`` do not align. The former displays numbering starting from the *arbitrarily provided value*, whereas the latter emphasizes the line numbers of the *source file*. + +.. literalinclude:: narr/helloworld.py + :language: python + :linenos: + :lineno-start: 11 + :emphasize-lines: 1,6-7,9- + +``literalinclude`` also supports including only parts of a file. + +If the source code is a Python module, you can select a class, function, or method to include using the ``pyobject`` option. + +.. code-block:: rst + + .. literalinclude:: narr/helloworld.py + :language: python + :pyobject: hello_world + +The above code renders as follows. It returns the function ``hello_world`` in the source file. + +.. literalinclude:: narr/helloworld.py + :language: python + :pyobject: hello_world + +Another way to control which part of the file is included is to use the ``start-after`` and ``end-before`` options (or only one of them). If ``start-after`` is given as a string option, only lines that follow the first line containing that string are included. If ``end-before`` is given as a string option, only lines that precede the first lines containing that string are included. + +.. code-block:: rst + + .. literalinclude:: narr/helloworld.py + :language: python + :start-after: from pyramid.response import Response + :end-before: if __name__ == '__main__': + +The above code renders as follows. + +.. literalinclude:: narr/helloworld.py + :language: python + :start-after: from pyramid.response import Response + :end-before: if __name__ == '__main__': + +You can specify exactly which lines to include by giving a ``lines`` option. + +.. code-block:: rst + + .. literalinclude:: narr/helloworld.py + :language: python + :lines: 6-7 + +The above code renders as follows. + +.. literalinclude:: narr/helloworld.py + :language: python + :lines: 6-7 + +When specifying particular parts of a file to display, it can be useful to display exactly which lines are being presented. This can be done using the ``lineno-match`` option. + +.. code-block:: rst + + .. literalinclude:: narr/helloworld.py + :language: python + :lines: 6-7 + :lineno-match: + +The above code renders as follows. + +.. literalinclude:: narr/helloworld.py + :language: python + :lines: 6-7 + :lineno-match: + +Out of all the ways to include parts of a file, ``pyobject`` is the most preferred option because if you change your code and add or remove lines, you don't need to adjust line numbering, whereas with ``lines`` you would have to adjust. ``start-after`` and ``end-before`` are less desirable because they depend on source code not changing. Alternatively you can insert comments into your source code to act as the delimiters, but that just adds comments that have nothing to do with the functionality of your code. + +Above all with includes, if you use line numbering, it's much preferred to use ``lineno-match`` over ``linenos`` with ``lineno-start`` because it "just works" without thinking and with less markup. + + +.. _style-guide-inline-code: + +Inline code +""""""""""" + +Inline code is surrounded by double backtick marks. Literals, filenames, and function arguments are presented using this style. + +.. code-block:: rst + + Install requirements for building documentation: ``pip install -e ".[docs]"`` + +The above code renders as follows. + +Install requirements for building documentation: ``pip install -e ".[docs]"`` + + +.. _style-guide-rest-block-markup: + +reST block markup +----------------- + +This section contains miscellaneous reST block markup for items not already covered. + + +.. _style-guide-lists: + +Lists +^^^^^ + +Bulleted lists use an asterisk "``*``". + +.. code-block:: rst + + * This is an item in a bulleted list. + * This is another item in a bulleted list. + +The above code renders as follows. + +* This is an item in a bulleted list. +* This is another item in a bulleted list. + +Numbered lists should use a number sign followed by a period "``#.``" and will be numbered automatically. + +.. code-block:: rst + + #. This is an item in a numbered list. + #. This is another item in a numbered list. + +The above code renders as follows. + +#. This is an item in a numbered list. +#. This is another item in a numbered list. + +The appearance of nested lists can be created by separating the child lists from their parent list by blank lines, and indenting by two spaces. Note that Sphinx renders the reST markup not as nested HTML lists, but instead merely indents the children using ``<blockquote>``. + +.. code-block:: rst + + #. This is a list item in the parent list. + #. This is another list item in the parent list. + + #. This is a list item in the child list. + #. This is another list item in the child list. + + #. This is one more list item in the parent list. + +The above code renders as follows. + +#. This is a list item in the parent list. +#. This is another list item in the parent list. + + #. This is a list item in the child list. + #. This is another list item in the child list. + +#. This is one more list item in the parent list. + + +.. _style-guide-tables: + +Tables +^^^^^^ + +Two forms of tables are supported, `simple <http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#simple-tables>`_ and `grid <http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#grid-tables>`_. + +Simple tables require less markup but have fewer features and some constraints compared to grid tables. The right-most column in simple tables is unbound to the length of the underline in the column header. + +.. code-block:: rst + + ===== ===== + col 1 col 2 + ===== ===== + 1 Second column of row 1. + 2 Second column of row 2. + Second line of paragraph. + 3 * Second column of row 3. + + * Second item in bullet + list (row 3, column 2). + \ Row 4; column 1 will be empty. + ===== ===== + +The above code renders as follows. + +===== ===== +col 1 col 2 +===== ===== +1 Second column of row 1. +2 Second column of row 2. + Second line of paragraph. +3 * Second column of row 3. + + * Second item in bullet + list (row 3, column 2). +\ Row 4; column 1 will be empty. +===== ===== + +Grid tables have much more cumbersome markup, although Emacs' table mode may lessen the tedium. + +.. code-block:: rst + + +------------------------+------------+----------+----------+ + | Header row, column 1 | Header 2 | Header 3 | Header 4 | + | (header rows optional) | | | | + +========================+============+==========+==========+ + | body row 1, column 1 | column 2 | column 3 | column 4 | + +------------------------+------------+----------+----------+ + | body row 2 | Cells may span columns. | + +------------------------+------------+---------------------+ + | body row 3 | Cells may | * Table cells | + +------------------------+ span rows. | * contain | + | body row 4 | | * body elements. | + +------------------------+------------+---------------------+ + +The above code renders as follows. + ++------------------------+------------+----------+----------+ +| Header row, column 1 | Header 2 | Header 3 | Header 4 | +| (header rows optional) | | | | ++========================+============+==========+==========+ +| body row 1, column 1 | column 2 | column 3 | column 4 | ++------------------------+------------+----------+----------+ +| body row 2 | Cells may span columns. | ++------------------------+------------+---------------------+ +| body row 3 | Cells may | * Table cells | ++------------------------+ span rows. | * contain | +| body row 4 | | * body elements. | ++------------------------+------------+---------------------+ + + +.. _style-guide-feature-versioning: + +Feature versioning +^^^^^^^^^^^^^^^^^^ + +Three directives designate the version in which something is added, changed, or deprecated in the project. + + +.. _style-guide-version-added: + +Version added +""""""""""""" + +To indicate the version in which a feature is added to a project, use the ``versionadded`` directive. If the feature is an entire module, then the directive should be placed at the top of the module section before any prose. + +The first argument is the version. An optional second argument must appear upon a subsequent line, without blank lines in between, and indented. + +.. code-block:: rst + + .. versionadded:: 1.1 + :func:`pyramid.paster.bootstrap` + +The above code renders as follows. + +.. versionadded:: 1.1 + :func:`pyramid.paster.bootstrap` + + +.. _style-guide-version-changed: + +Version changed +""""""""""""""" + +To indicate the version in which a feature is changed in a project, use the ``versionchanged`` directive. Its arguments are the same as ``versionadded``. + +.. code-block:: rst + + .. versionchanged:: 1.8 + Added the ability for ``bootstrap`` to cleanup automatically via the ``with`` statement. + +The above code renders as follows. + +.. versionchanged:: 1.8 + Added the ability for ``bootstrap`` to cleanup automatically via the ``with`` statement. + + +.. _style-guide-deprecated: + +Deprecated +"""""""""" + +Similar to ``versionchanged``, ``deprecated`` describes when the feature was deprecated. An explanation can also be given, for example, to inform the reader what should be used instead. + +.. code-block:: rst + + .. deprecated:: 1.7 + Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised. + +The above code renders as follows. + +.. deprecated:: 1.7 + Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised. + + +.. _style-guide-danger: + +Danger +^^^^^^ + +Danger represents critical information related to a topic or concept, and should recommend to the user "don't do this dangerous thing". + +.. code-block:: rst + + .. danger:: + + This is danger or an error. + +The above code renders as follows. + +.. danger:: + + This is danger or an error. + +.. todo:: + + The style for ``danger`` and ``error`` has not yet been created. + + +.. _style-guide-warnings: + +Warnings +^^^^^^^^ + +Warnings represent limitations and advice related to a topic or concept. + +.. code-block:: rst + + .. warning:: + + This is a warning. + +The above code renders as follows. + +.. warning:: + + This is a warning. + + +.. _style-guide-notes: + +Notes +^^^^^ + +Notes represent additional information related to a topic or concept. + +.. code-block:: rst + + .. note:: + + This is a note. + +The above code renders as follows. + +.. note:: + + This is a note. + + +.. _style-guide-see-also: + +See also +^^^^^^^^ + +"See also" messages refer to topics that are related to the current topic, but have a narrative tone to them instead of merely a link without explanation. "See also" is rendered in a block as well, so that it stands out for the reader's attention. + +.. code-block:: rst + + .. seealso:: + + See :ref:`Quick Tutorial section on Requirements <qtut_requirements>`. + +The above code renders as follows. + +.. seealso:: + + See :ref:`Quick Tutorial section on Requirements <qtut_requirements>`. + + +.. _style-guide-todo: + +Todo +^^^^ + +Todo items designated tasks that require further work. + +.. code-block:: rst + + .. todo:: + + This is a todo item. + +The above code renders as follows. + +.. todo:: + + This is a todo item. + +.. todo:: + + The todo style is not yet implemented and needs further work. + + +.. _style-guide-comments: + +Comments +^^^^^^^^ + +Comments of the documentation within the documentation may be generated with two periods ``..``. Comments are not rendered, but provide information to documentation authors. + +.. code-block:: rst + + .. This is an example comment. + + +.. _style-guide-rest-inline-markup: + +reST inline markup +------------------ + +This section contains miscellaneous reST inline markup for items not already covered. Within a block of content, inline markup is useful to apply styles and links to other files. + + +.. _style-guide-italics: + +Italics +^^^^^^^ + +.. code-block:: rst + + This *word* is italicized. + +The above code renders as follows. + +This *word* is italicized. + + +.. _style-guide-strong: + +Strong +^^^^^^ + +.. code-block:: rst + + This **word** is in bold text. + +The above code renders as follows. + +This **word** is in bold text. + +.. seealso:: + + See also the Sphinx documentation for the :ref:`rst-primer`. + + +.. _style-guide-cross-references: + +Cross-references +^^^^^^^^^^^^^^^^ + +To create cross-references to a document, arbitrary location, object, or other items, use variations of the following syntax. + +* ``:role:`target``` creates a link to the item named ``target`` of the type indicated by ``role``, with the link's text as the title of the target. ``target`` may need to be disambiguated between documentation sets linked through intersphinx, in which case the syntax would be ``deform:overview``. +* ``:role:`~target``` displays the link as only the last component of the target. +* ``:role:`title <target>``` creates a custom title, instead of the default title of the target. + + +.. _style-guide-cross-referencing-documents: + +Cross-referencing documents +""""""""""""""""""""""""""" + +To link to pages within this documentation: + +.. code-block:: rst + + :doc:`quick_tour` + +The above code renders as follows. + +:doc:`quick_tour` + + +.. _style-guide-cross-referencing-arbitrary-locations: + +Cross-referencing arbitrary locations +""""""""""""""""""""""""""""""""""""" + +To support cross-referencing to arbitrary locations in any document and between documentation sets via intersphinx, the standard reST labels are used. For this to work, label names must be unique throughout the entire documentation including externally linked intersphinx references. There are two ways in which you can refer to labels, if they are placed directly before a section title, a figure, or table with a caption, or at any other location. The following section has a label with the syntax ``.. _label_name:`` followed by the section title. + +.. code-block:: rst + + .. _i18n_chapter: + + Internationalization and Localization + ===================================== + +To generate a link to that section with its title, use the following syntax. + +.. code-block:: rst + + :ref:`i18n_chapter` + +The above code renders as follows. + +:ref:`i18n_chapter` + +The same syntax works figures and tables with captions. + +For labels that are not placed as mentioned, the link must be given an explicit title, such as ``:ref:`Link title <label-name>```. + +.. seealso:: See also the Sphinx documentation, :ref:`inline-markup`. + + +.. _style-guide-cross-referencing-python: + +Python modules, classes, methods, and functions +""""""""""""""""""""""""""""""""""""""""""""""" + +Python module names use the ``mod`` directive, with the module name as the argument. + +.. code-block:: rst + + :mod:`pyramid.config` + +The above code renders as follows. + +:mod:`pyramid.config` + +Python class names use the ``class`` directive, with the class name as the argument. + +.. code-block:: rst + + :class:`pyramid.config.Configurator` + +The above code renders as follows. + +:class:`pyramid.config.Configurator` + +Python method names use the ``meth`` directive, with the method name as the argument. + +.. code-block:: rst + + :meth:`pyramid.config.Configurator.add_view` + +The above code renders as follows. + +:meth:`pyramid.config.Configurator.add_view` + +Python function names use the ``func`` directive, with the function name as the argument. + +.. code-block:: rst + + :func:`pyramid.renderers.render_to_response` + +The above code renders as follows. + +:func:`pyramid.renderers.render_to_response` + +Note that you can use the ``~`` prefix to show only the last segment of a Python object's name. We prefer not to use the ``.`` prefix, even though it may seem to be a convenience to documentation authors, because Sphinx might generate an error if it cannot disambiguate the reference. + +.. code-block:: rst + + :func:`~pyramid.renderers.render_to_response` + +The above code renders as follows. + +:func:`~pyramid.renderers.render_to_response` + + +.. _style-guide-role-app-pyramid: + +The role ``:app:`Pyramid``` +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We use the special role ``app`` to refer to the application "Pyramid". + +.. code-block:: rst + + :app:`Pyramid` + +The above code renders as follows. + +:app:`Pyramid` + + +.. _style-guide-sphinx-extensions: + +Sphinx extensions +----------------- + +We use several Sphinx extensions to add features to our documentation. Extensions need to be enabled and configured in ``docs/conf.py`` before they can be used. + + +.. _style-guide-sphinx-extension-autodoc: + +:mod:`sphinx.ext.autodoc` +------------------------- + +API documentation uses the Sphinx extension :mod:`sphinx.ext.autodoc` to include documentation from docstrings. + +See the source of any documentation within the ``docs/api/`` directory for conventions and usage, as well as the Sphinx extension's :mod:`documentation <sphinx.ext.autodoc>`. + + +.. _style-guide-sphinx-extension-doctest: + +:mod:`sphinx.ext.doctest` +------------------------- + +:mod:`sphinx.ext.doctest` allows you to test code snippets in the documentation in a natural way. It works by collecting specially-marked up code blocks and running them as doctest tests. We have only a few tests in our Pyramid documentation which can be found in ``narr/sessions.rst`` and ``narr/hooks.rst``. + + +.. _style-guide-sphinx-extension-intersphinx: + +:mod:`sphinx.ext.intersphinx` +----------------------------- + +:mod:`sphinx.ext.intersphinx` generates links to the documentation of objects in other projects. + + +.. _style-guide-sphinx-extension-todo: + +:mod:`sphinx.ext.todo` +---------------------- + +:mod:`sphinx.ext.todo` adds support for todo items. + + +.. _style-guide-sphinx-extension-viewcode: + +:mod:`sphinx.ext.viewcode` +-------------------------- + +:mod:`sphinx.ext.viewcode` looks at your Python object descriptions and tries to find the source files where the objects are contained. When found, a separate HTML page will be output for each module with a highlighted version of the source code, and a link will be added to all object descriptions that leads to the source code of the described object. A link back from the source to the description will also be inserted. + + +.. _style-guide-sphinx-extension-repoze-sphinx-autointerface: + +`repoze.sphinx.autointerface <https://pypi.python.org/pypi/repoze.sphinx.autointerface>`_ +----------------------------------------------------------------------------------------- + +`repoze.sphinx.autointerface <https://pypi.python.org/pypi/repoze.sphinx.autointerface>`_ auto-generates API docs from Zope interfaces. + + +.. _style-guide-script-documentation: + +Script documentation +-------------------- + +We currently use `sphinxcontrib-programoutput <https://pypi.python.org/pypi/sphinxcontrib-programoutput>`_ to generate program output of the p* scripts. It is no longer maintained and may cause future builds of the documentation to fail. + +.. todo:: + + See `issue #2804 <https://github.com/Pylons/pyramid/issues/2804>`_ for further discussion. diff --git a/docs/tutorials/wiki/authorization.rst b/docs/tutorials/wiki/authorization.rst index 44097b35b..67af83b25 100644 --- a/docs/tutorials/wiki/authorization.rst +++ b/docs/tutorials/wiki/authorization.rst @@ -18,6 +18,7 @@ require permission, instead of a default "403 Forbidden" page. We will implement the access control with the following steps: +* Add password hashing dependencies. * Add users and groups (``security.py``, a new module). * Add an :term:`ACL` (``models.py``). * Add an :term:`authentication policy` and an :term:`authorization policy` @@ -38,11 +39,32 @@ Then we will add the login and logout feature: Access control -------------- + +Add dependencies +~~~~~~~~~~~~~~~~ + +Just like in :ref:`wiki_defining_views`, we need a new dependency. We need to add the `bcrypt <https://pypi.python.org/pypi/bcrypt>`_ package, to our tutorial package's ``setup.py`` file by assigning this dependency to the ``requires`` parameter in the ``setup()`` function. + +Open ``setup.py`` and edit it to look like the following: + +.. literalinclude:: src/authorization/setup.py + :linenos: + :emphasize-lines: 21 + :language: python + +Only the highlighted line needs to be added. + +Do not forget to run ``pip install -e .`` just like in :ref:`wiki-running-pip-install`. + +.. note:: + + We are using the ``bcrypt`` package from PyPI to hash our passwords securely. There are other one-way hash algorithms for passwords if bcrypt is an issue on your system. Just make sure that it's an algorithm approved for storing passwords versus a generic one-way hash. + + Add users and groups ~~~~~~~~~~~~~~~~~~~~ -Create a new ``tutorial/security.py`` module with the -following content: +Create a new ``tutorial/security.py`` module with the following content: .. literalinclude:: src/authorization/tutorial/security.py :linenos: @@ -61,7 +83,20 @@ request)`` returns ``None``. We will use ``groupfinder()`` as an :term:`authentication policy` "callback" that will provide the :term:`principal` or principals for a user. -In a production system, user and group data will most often come from a +There are two helper methods that will help us later to authenticate users. +The first is ``hash_password`` which takes a raw password and transforms it using +bcrypt into an irreversible representation, a process known as "hashing". The +second method, ``check_password``, will allow us to compare the hashed value of the +submitted password against the hashed value of the password stored in the user's +record. If the two hashed values match, then the submitted +password is valid, and we can authenticate the user. + +We hash passwords so that it is impossible to decrypt and use them to +authenticate in the application. If we stored passwords foolishly in clear text, +then anyone with access to the database could retrieve any password to authenticate +as any user. + +In a production system, user and group data will most often be saved and come from a database, but here we use "dummy" data to represent user and groups sources. Add an ACL diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index ac94d8059..3859d2cad 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -52,6 +52,7 @@ Open ``setup.py`` and edit it to look like the following: Only the highlighted line needs to be added. +.. _wiki-running-pip-install: Running ``pip install -e .`` ============================ diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index 6172b122b..ec79a4e9c 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -66,7 +66,7 @@ Python 2.7: c:\> c:\Python27\Scripts\virtualenv %VENV% -Python 3.5: +Python 3.6: .. code-block:: doscon @@ -310,13 +310,13 @@ If successful, you will see output something like this: .. code-block:: bash ======================== test session starts ======================== - platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 + platform Python 3.6.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile: plugins: cov-2.2.1 collected 1 items tutorial/tests.py . - ------------------ coverage: platform Python 3.5.1 ------------------ + ------------------ coverage: platform Python 3.6.0 ------------------ Name Stmts Miss Cover Missing ---------------------------------------------------- tutorial/__init__.py 12 7 42% 7-8, 14-18 @@ -370,7 +370,8 @@ coverage. Start the application --------------------- -Start the application. +Start the application. See :ref:`what_is_this_pserve_thing` for more +information on ``pserve``. On UNIX ^^^^^^^ diff --git a/docs/tutorials/wiki/src/authorization/setup.py b/docs/tutorials/wiki/src/authorization/setup.py index beeed75c9..68e3c0abd 100644 --- a/docs/tutorials/wiki/src/authorization/setup.py +++ b/docs/tutorials/wiki/src/authorization/setup.py @@ -18,6 +18,7 @@ requires = [ 'ZODB3', 'waitress', 'docutils', + 'bcrypt', ] tests_require = [ diff --git a/docs/tutorials/wiki/src/authorization/tutorial/security.py b/docs/tutorials/wiki/src/authorization/tutorial/security.py index d88c9c71f..cbb3acd5d 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/security.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/security.py @@ -1,5 +1,18 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} +import bcrypt + + +def hash_password(pw): + hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) + # return unicode instead of bytes because databases handle it better + return hashed_pw.decode('utf-8') + +def check_password(expected_hash, pw): + if expected_hash is not None: + return bcrypt.checkpw(pw.encode('utf-8'), expected_hash.encode('utf-8')) + return False + +USERS = {'editor': hash_password('editor'), + 'viewer': hash_password('viewer')} GROUPS = {'editor':['group:editors']} def groupfinder(userid, request): diff --git a/docs/tutorials/wiki/src/authorization/tutorial/views.py b/docs/tutorials/wiki/src/authorization/tutorial/views.py index c271d2cc1..e4560dfe1 100644 --- a/docs/tutorials/wiki/src/authorization/tutorial/views.py +++ b/docs/tutorials/wiki/src/authorization/tutorial/views.py @@ -14,7 +14,7 @@ from pyramid.security import ( ) -from .security import USERS +from .security import USERS, check_password from .models import Page # regular expression used to find WikiWords @@ -94,7 +94,7 @@ def login(request): if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] - if USERS.get(login) == password: + if check_password(USERS.get(login), password): headers = remember(request, login) return HTTPFound(location=came_from, headers=headers) diff --git a/docs/tutorials/wiki/src/tests/setup.py b/docs/tutorials/wiki/src/tests/setup.py index beeed75c9..68e3c0abd 100644 --- a/docs/tutorials/wiki/src/tests/setup.py +++ b/docs/tutorials/wiki/src/tests/setup.py @@ -18,6 +18,7 @@ requires = [ 'ZODB3', 'waitress', 'docutils', + 'bcrypt', ] tests_require = [ diff --git a/docs/tutorials/wiki/src/tests/tutorial/security.py b/docs/tutorials/wiki/src/tests/tutorial/security.py index d88c9c71f..cbb3acd5d 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/security.py +++ b/docs/tutorials/wiki/src/tests/tutorial/security.py @@ -1,5 +1,18 @@ -USERS = {'editor':'editor', - 'viewer':'viewer'} +import bcrypt + + +def hash_password(pw): + hashed_pw = bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) + # return unicode instead of bytes because databases handle it better + return hashed_pw.decode('utf-8') + +def check_password(expected_hash, pw): + if expected_hash is not None: + return bcrypt.checkpw(pw.encode('utf-8'), expected_hash.encode('utf-8')) + return False + +USERS = {'editor': hash_password('editor'), + 'viewer': hash_password('viewer')} GROUPS = {'editor':['group:editors']} def groupfinder(userid, request): diff --git a/docs/tutorials/wiki/src/tests/tutorial/tests.py b/docs/tutorials/wiki/src/tests/tutorial/tests.py index 04beaea44..098e9c1bd 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/tests.py +++ b/docs/tutorials/wiki/src/tests/tutorial/tests.py @@ -122,6 +122,17 @@ class EditPageTests(unittest.TestCase): self.assertEqual(response.location, 'http://example.com/') self.assertEqual(context.data, 'Hello yo!') +class SecurityTests(unittest.TestCase): + def test_hashing(self): + from .security import hash_password, check_password + password = 'secretpassword' + hashed_password = hash_password(password) + self.assertTrue(check_password(hashed_password, password)) + + self.assertFalse(check_password(hashed_password, 'attackerpassword')) + + self.assertFalse(check_password(None, password)) + class FunctionalTests(unittest.TestCase): viewer_login = '/login?login=viewer&password=viewer' \ diff --git a/docs/tutorials/wiki/src/tests/tutorial/views.py b/docs/tutorials/wiki/src/tests/tutorial/views.py index c271d2cc1..e4560dfe1 100644 --- a/docs/tutorials/wiki/src/tests/tutorial/views.py +++ b/docs/tutorials/wiki/src/tests/tutorial/views.py @@ -14,7 +14,7 @@ from pyramid.security import ( ) -from .security import USERS +from .security import USERS, check_password from .models import Page # regular expression used to find WikiWords @@ -94,7 +94,7 @@ def login(request): if 'form.submitted' in request.params: login = request.params['login'] password = request.params['password'] - if USERS.get(login) == password: + if check_password(USERS.get(login), password): headers = remember(request, login) return HTTPFound(location=came_from, headers=headers) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index 0440c2d1d..fa990fb01 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -66,7 +66,7 @@ Python 2.7: c:\> c:\Python27\Scripts\virtualenv %VENV% -Python 3.5: +Python 3.6: .. code-block:: doscon @@ -327,13 +327,13 @@ If successful, you will see output something like this: .. code-block:: bash ======================== test session starts ======================== - platform Python 3.5.1, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 + platform Python 3.6.0, pytest-2.9.1, py-1.4.31, pluggy-0.3.1 rootdir: /Users/stevepiercy/projects/pyramidtut/tutorial, inifile: plugins: cov-2.2.1 collected 2 items tutorial/tests.py .. - ------------------ coverage: platform Python 3.5.1 ------------------ + ------------------ coverage: platform Python 3.6.0 ------------------ Name Stmts Miss Cover Missing ---------------------------------------------------------------- tutorial/__init__.py 8 6 25% 7-12 @@ -457,7 +457,8 @@ working directory. This is an SQLite database with a single table defined in it Start the application --------------------- -Start the application. +Start the application. See :ref:`what_is_this_pserve_thing` for more +information on ``pserve``. On UNIX ^^^^^^^ diff --git a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py index f3c0a6fef..c860ef8cf 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/scripts/initializedb.py @@ -28,6 +28,7 @@ def usage(argv): def main(argv=sys.argv): if len(argv) < 2: usage(argv) + return config_uri = argv[1] options = parse_vars(argv[2:]) setup_logging(config_uri) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py index 715768b2e..0250e71c9 100644 --- a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_functional.py @@ -11,6 +11,9 @@ class FunctionalTests(unittest.TestCase): basic_wrong_login = ( '/login?login=basic&password=incorrect' '&next=FrontPage&form.submitted=Login') + basic_login_no_next = ( + '/login?login=basic&password=basic' + '&form.submitted=Login') editor_login = ( '/login?login=editor&password=editor' '&next=FrontPage&form.submitted=Login') @@ -68,6 +71,10 @@ class FunctionalTests(unittest.TestCase): res = self.testapp.get(self.basic_login, status=302) self.assertEqual(res.location, 'http://localhost/FrontPage') + def test_successful_log_in_no_next(self): + res = self.testapp.get(self.basic_login_no_next, status=302) + self.assertEqual(res.location, 'http://localhost/') + def test_failed_log_in(self): res = self.testapp.get(self.basic_wrong_login, status=200) self.assertTrue(b'login' in res.body) @@ -120,3 +127,8 @@ class FunctionalTests(unittest.TestCase): self.testapp.get(self.editor_login, status=302) res = self.testapp.get('/FrontPage', status=200) self.assertTrue(b'FrontPage' in res.body) + + def test_redirect_to_edit_for_existing_page(self): + self.testapp.get(self.editor_login, status=302) + res = self.testapp.get('/add_page/FrontPage', status=302) + self.assertTrue(b'FrontPage' in res.body) diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py new file mode 100644 index 000000000..97511d5e8 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_initdb.py @@ -0,0 +1,20 @@ +import mock +import unittest + + +class TestInitializeDB(unittest.TestCase): + + @mock.patch('tutorial.scripts.initializedb.sys') + def test_usage(self, mocked_sys): + from ..scripts.initializedb import main + main(argv=['foo']) + mocked_sys.exit.assert_called_with(1) + + @mock.patch('tutorial.scripts.initializedb.get_tm_session') + @mock.patch('tutorial.scripts.initializedb.sys') + def test_run(self, mocked_sys, mocked_session): + from ..scripts.initializedb import main + main(argv=['foo', 'development.ini']) + mocked_session.assert_called_once() + + diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py new file mode 100644 index 000000000..4c3b72946 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_security.py @@ -0,0 +1,21 @@ +import mock +import unittest + + +class TestMyAuthenticationPolicy(unittest.TestCase): + + def test_no_user(self): + request = mock.Mock() + request.user = None + + from ..security import MyAuthenticationPolicy + policy = MyAuthenticationPolicy(None) + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_user(self): + request = mock.Mock() + request.user.id = 'foo' + + from ..security import MyAuthenticationPolicy + policy = MyAuthenticationPolicy(None) + self.assertEqual(policy.authenticated_userid(request), 'foo') diff --git a/docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py new file mode 100644 index 000000000..9490ac990 --- /dev/null +++ b/docs/tutorials/wiki2/src/tests/tutorial/tests/test_user_model.py @@ -0,0 +1,67 @@ +import unittest +import transaction + +from pyramid import testing + + +class BaseTest(unittest.TestCase): + + def setUp(self): + from ..models import get_tm_session + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('..models') + self.config.include('..routes') + + session_factory = self.config.registry['dbsession_factory'] + self.session = get_tm_session(session_factory, transaction.manager) + + self.init_database() + + def init_database(self): + from ..models.meta import Base + session_factory = self.config.registry['dbsession_factory'] + engine = session_factory.kw['bind'] + Base.metadata.create_all(engine) + + def tearDown(self): + testing.tearDown() + transaction.abort() + + def makeUser(self, name, role): + from ..models import User + return User(name=name, role=role) + + +class TestSetPassword(BaseTest): + + def test_password_hash_saved(self): + user = self.makeUser(name='foo', role='bar') + self.assertFalse(user.password_hash) + + user.set_password('secret') + self.assertTrue(user.password_hash) + + +class TestCheckPassword(BaseTest): + + def test_password_hash_not_set(self): + user = self.makeUser(name='foo', role='bar') + self.assertFalse(user.password_hash) + + self.assertFalse(user.check_password('secret')) + + def test_correct_password(self): + user = self.makeUser(name='foo', role='bar') + user.set_password('secret') + self.assertTrue(user.password_hash) + + self.assertTrue(user.check_password('secret')) + + def test_incorrect_password(self): + user = self.makeUser(name='foo', role='bar') + user.set_password('secret') + self.assertTrue(user.password_hash) + + self.assertFalse(user.check_password('incorrect')) diff --git a/docs/typographical-conventions.rst b/docs/typographical-conventions.rst new file mode 100644 index 000000000..19894775b --- /dev/null +++ b/docs/typographical-conventions.rst @@ -0,0 +1,338 @@ +.. _typographical-conventions: + +Typographical Conventions +========================= + +.. meta:: + :description: This chapter describes typographical conventions used in the Pyramid documentation. + :keywords: Pyramid, Typographical Conventions + + +.. _typographical-conventions-introduction: + +Introduction +------------ + +This chapter describes typographical conventions used in the Pyramid documentation. Documentation authors and contributors should review the :ref:`style-guide`. + + +.. _typographical-conventions-glossary: + +Glossary +-------- + +A glossary defines terms used throughout the documentation. References to glossary terms appear as follows. + +:term:`request` + +Note it is hyperlinked, and when clicked it will take the user to the term in the Glossary and highlight the term. + + +.. _typographical-conventions-links: + +Links +----- + +Links are presented as follows, and may be clickable. + +`TryPyramid <https://TryPyramid.com>`_ + +.. seealso:: See also :ref:`typographical-conventions-cross-references` for other links within the documentation. + + +.. _typographical-conventions-topic: + +Topic +----- + +A topic is similar to a block quote with a title, or a self-contained section with no subsections. A topic indicates a self-contained idea that is separate from the flow of the document. Topics may occur anywhere a section or transition may occur. + +.. topic:: Topic Title + + Subsequent indented lines comprise + the body of the topic, and are + interpreted as body elements. + + +.. _typographical-conventions-displaying-code: + +Code +---- + +Code may be displayed in blocks or inline. Blocks of code may use syntax highlighting, line numbering, and emphasis. + + +.. _typographical-conventions-syntax-highlighting: + +Syntax highlighting +^^^^^^^^^^^^^^^^^^^ + +XML: + +.. code-block:: xml + + <somesnippet>Some XML</somesnippet> + +Unix shell commands are prefixed with a ``$`` character. (See :term:`venv` for the meaning of ``$VENV``.) + +.. code-block:: bash + + $ $VENV/bin/pip install -e . + +Windows commands are prefixed with a drive letter with an optional directory name. (See :term:`venv` for the meaning of ``%VENV%``.) + +.. code-block:: doscon + + c:\> %VENV%\Scripts\pcreate -s starter MyProject + +cfg: + +.. code-block:: cfg + + [some-part] + # A random part in the buildout + recipe = collective.recipe.foo + option = value + +ini: + +.. code-block:: ini + + [nosetests] + match=^test + where=pyramid + nocapture=1 + +Interactive Python: + +.. code-block:: pycon + + >>> class Foo: + ... bar = 100 + ... + >>> f = Foo() + >>> f.bar + 100 + >>> f.bar / 0 + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + ZeroDivisionError: integer division or modulo by zero + + +.. _typographical-conventions-long-commands: + +Displaying long commands +^^^^^^^^^^^^^^^^^^^^^^^^ + +When a command that should be typed on one line is too long to fit on the displayed width of a page, the backslash character ``\`` is used to indicate that the subsequent printed line should be part of the command: + +.. code-block:: bash + + $ $VENV/bin/py.test tutorial/tests.py --cov-report term-missing \ + --cov=tutorial -q + + +.. _typographical-conventions-code-block-options: + +Code block options +^^^^^^^^^^^^^^^^^^ + +To emphasize lines, we give the appearance that a highlighting pen has been used on the code. + +.. code-block:: python + :emphasize-lines: 1,3 + + if "foo" == "bar": + # This is Python code + pass + +A code block with line numbers. + +.. code-block:: python + :linenos: + + if "foo" == "bar": + # This is Python code + pass + +Some code blocks may be given a caption. + +.. code-block:: python + :caption: sample.py + :name: sample-py-typographical-conventions + + if "foo" == "bar": + # This is Python code + pass + + +.. _typographical-conventions-inline-code: + +Inline code +^^^^^^^^^^^ + +Inline code is displayed as follows, where the inline code is 'pip install -e ".[docs]"'. + +Install requirements for building documentation: ``pip install -e ".[docs]"`` + + +.. _typographical-conventions-feature-versioning: + +Feature versioning +------------------ + +We designate the version in which something is added, changed, or deprecated in the project. + + +.. _typographical-conventions-version-added: + +Version added +^^^^^^^^^^^^^ + +The version in which a feature is added to a project is displayed as follows. + +.. versionadded:: 1.1 + :func:`pyramid.paster.bootstrap` + + +.. _typographical-conventions-version-changed: + +Version changed +^^^^^^^^^^^^^^^ + +The version in which a feature is changed in a project is displayed as follows. + +.. versionchanged:: 1.8 + Added the ability for ``bootstrap`` to cleanup automatically via the ``with`` statement. + + +.. _typographical-conventions-deprecated: + +Deprecated +^^^^^^^^^^ + +The version in which a feature is deprecated in a project is displayed as follows. + +.. deprecated:: 1.7 + Use the ``require_csrf`` option or read :ref:`auto_csrf_checking` instead to have :class:`pyramid.exceptions.BadCSRFToken` exceptions raised. + + +.. _typographical-conventions-danger: + +Danger +------ + +Danger represents critical information related to a topic or concept, and should recommend to the user "don't do this dangerous thing". + +.. danger:: + + This is danger or an error. + + +.. _typographical-conventions-warnings: + +Warnings +-------- + +Warnings represent limitations and advice related to a topic or concept. + +.. warning:: + + This is a warning. + + +.. _typographical-conventions-notes: + +Notes +----- + +Notes represent additional information related to a topic or concept. + +.. note:: + + This is a note. + + +.. _typographical-conventions-see-also: + +See also +-------- + +"See also" messages refer to topics that are related to the current topic, but have a narrative tone to them instead of merely a link without explanation. "See also" is rendered in a block as well, so that it stands out for the reader's attention. + +.. seealso:: + + See :ref:`Quick Tutorial section on Requirements <qtut_requirements>`. + + +.. _typographical-conventions-todo: + +Todo +---- + +Todo items designated tasks that require further work. + +.. todo:: + + This is a todo item. + + +.. _typographical-conventions-cross-references: + +Cross-references +---------------- + +Cross-references are links that may be to a document, arbitrary location, object, or other items. + + +.. _typographical-conventions-cross-referencing-documents: + +Cross-referencing documents +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Links to pages within this documentation display as follows. + +:doc:`quick_tour` + + +.. _typographical-conventions-cross-referencing-arbitrary-locations: + +Cross-referencing arbitrary locations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Links to sections, and tables and figures with captions, within this documentation display as follows. + +:ref:`i18n_chapter` + + +.. _typographical-conventions-cross-referencing-python: + +Python modules, classes, methods, and functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All of the following are clickable links to Python modules, classes, methods, and functions. + +Python module names display as follows. + +:mod:`pyramid.config` + +Python class names display as follows. + +:class:`pyramid.config.Configurator` + +Python method names display as follows. + +:meth:`pyramid.config.Configurator.add_view` + +Python function names display as follows. + +:func:`pyramid.renderers.render_to_response` + +Sometimes we show only the last segment of a Python object's name, which displays as follows. + +:func:`~pyramid.renderers.render_to_response` + +The application "Pyramid" itself displays as follows. + +:app:`Pyramid` + diff --git a/docs/whatsnew-1.7.rst b/docs/whatsnew-1.7.rst index 398b12f01..c5f611f04 100644 --- a/docs/whatsnew-1.7.rst +++ b/docs/whatsnew-1.7.rst @@ -126,7 +126,7 @@ Feature Additions - The :attr:`pyramid.tweens.EXCVIEW` tween will now re-raise the original exception if no exception view could be found to handle it. This allows - the exception to be handled upstream by another tween or middelware. + the exception to be handled upstream by another tween or middleware. See https://github.com/Pylons/pyramid/pull/2567 Deprecations diff --git a/pyramid/asset.py b/pyramid/asset.py index e6a145341..9d7a3ee63 100644 --- a/pyramid/asset.py +++ b/pyramid/asset.py @@ -33,7 +33,7 @@ def asset_spec_from_abspath(abspath, package): relpath.replace(os.path.sep, '/')) return abspath -# bw compat only; use pyramid.path.AssetDescriptor.abspath() instead +# bw compat only; use pyramid.path.AssetResolver().resolve(spec).abspath() def abspath_from_asset_spec(spec, pname='__main__'): if pname is None: return spec diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 0b76bbd70..5d99d6564 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -1,301 +1,2 @@ -import re - -from pyramid.exceptions import ConfigurationError - -from pyramid.compat import is_nonstr_iter - -from pyramid.traversal import ( - find_interface, - traversal_path, - resource_path_tuple - ) - -from pyramid.urldispatch import _compile_route -from pyramid.util import object_description -from pyramid.session import check_csrf_token - -from .util import as_sorted_tuple - -_marker = object() - -class XHRPredicate(object): - def __init__(self, val, config): - self.val = bool(val) - - def text(self): - return 'xhr = %s' % self.val - - phash = text - - def __call__(self, context, request): - return bool(request.is_xhr) is self.val - -class RequestMethodPredicate(object): - def __init__(self, val, config): - request_method = as_sorted_tuple(val) - if 'GET' in request_method and 'HEAD' not in request_method: - # GET implies HEAD too - request_method = as_sorted_tuple(request_method + ('HEAD',)) - self.val = request_method - - def text(self): - return 'request_method = %s' % (','.join(self.val)) - - phash = text - - def __call__(self, context, request): - return request.method in self.val - -class PathInfoPredicate(object): - def __init__(self, val, config): - self.orig = val - try: - val = re.compile(val) - except re.error as why: - raise ConfigurationError(why.args[0]) - self.val = val - - def text(self): - return 'path_info = %s' % (self.orig,) - - phash = text - - def __call__(self, context, request): - return self.val.match(request.upath_info) is not None - -class RequestParamPredicate(object): - def __init__(self, val, config): - val = as_sorted_tuple(val) - reqs = [] - for p in val: - k = p - v = None - if p.startswith('='): - if '=' in p[1:]: - k, v = p[1:].split('=', 1) - k = '=' + k - k, v = k.strip(), v.strip() - elif '=' in p: - k, v = p.split('=', 1) - k, v = k.strip(), v.strip() - reqs.append((k, v)) - self.val = val - self.reqs = reqs - - def text(self): - return 'request_param %s' % ','.join( - ['%s=%s' % (x,y) if y else x for x, y in self.reqs] - ) - - phash = text - - def __call__(self, context, request): - for k, v in self.reqs: - actual = request.params.get(k) - if actual is None: - return False - if v is not None and actual != v: - return False - return True - -class HeaderPredicate(object): - def __init__(self, val, config): - name = val - v = None - if ':' in name: - name, val_str = name.split(':', 1) - try: - v = re.compile(val_str) - except re.error as why: - raise ConfigurationError(why.args[0]) - if v is None: - self._text = 'header %s' % (name,) - else: - self._text = 'header %s=%s' % (name, val_str) - self.name = name - self.val = v - - def text(self): - return self._text - - phash = text - - def __call__(self, context, request): - if self.val is None: - return self.name in request.headers - val = request.headers.get(self.name) - if val is None: - return False - return self.val.match(val) is not None - -class AcceptPredicate(object): - def __init__(self, val, config): - self.val = val - - def text(self): - return 'accept = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - return self.val in request.accept - -class ContainmentPredicate(object): - def __init__(self, val, config): - self.val = config.maybe_dotted(val) - - def text(self): - return 'containment = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - ctx = getattr(request, 'context', context) - return find_interface(ctx, self.val) is not None - -class RequestTypePredicate(object): - def __init__(self, val, config): - self.val = val - - def text(self): - return 'request_type = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - return self.val.providedBy(request) - -class MatchParamPredicate(object): - def __init__(self, val, config): - val = as_sorted_tuple(val) - self.val = val - reqs = [ p.split('=', 1) for p in val ] - self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ] - - def text(self): - return 'match_param %s' % ','.join( - ['%s=%s' % (x,y) for x, y in self.reqs] - ) - - phash = text - - def __call__(self, context, request): - if not request.matchdict: - # might be None - return False - for k, v in self.reqs: - if request.matchdict.get(k) != v: - return False - return True - -class CustomPredicate(object): - def __init__(self, func, config): - self.func = func - - def text(self): - return getattr( - self.func, - '__text__', - 'custom predicate: %s' % object_description(self.func) - ) - - def phash(self): - # using hash() here rather than id() is intentional: we - # want to allow custom predicates that are part of - # frameworks to be able to define custom __hash__ - # functions for custom predicates, so that the hash output - # of predicate instances which are "logically the same" - # may compare equal. - return 'custom:%r' % hash(self.func) - - def __call__(self, context, request): - return self.func(context, request) - - -class TraversePredicate(object): - # Can only be used as a *route* "predicate"; it adds 'traverse' to the - # matchdict if it's specified in the routing args. This causes the - # ResourceTreeTraverser to use the resolved traverse pattern as the - # traversal path. - def __init__(self, val, config): - _, self.tgenerate = _compile_route(val) - self.val = val - - def text(self): - return 'traverse matchdict pseudo-predicate' - - def phash(self): - # This isn't actually a predicate, it's just a infodict modifier that - # injects ``traverse`` into the matchdict. As a result, we don't - # need to update the hash. - return '' - - def __call__(self, context, request): - if 'traverse' in context: - return True - m = context['match'] - tvalue = self.tgenerate(m) # tvalue will be urlquoted string - m['traverse'] = traversal_path(tvalue) - # This isn't actually a predicate, it's just a infodict modifier that - # injects ``traverse`` into the matchdict. As a result, we just - # return True. - return True - -class CheckCSRFTokenPredicate(object): - - check_csrf_token = staticmethod(check_csrf_token) # testing - - def __init__(self, val, config): - self.val = val - - def text(self): - return 'check_csrf = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - val = self.val - if val: - if val is True: - val = 'csrf_token' - return self.check_csrf_token(request, val, raises=False) - return True - -class PhysicalPathPredicate(object): - def __init__(self, val, config): - if is_nonstr_iter(val): - self.val = tuple(val) - else: - val = tuple(filter(None, val.split('/'))) - self.val = ('',) + val - - def text(self): - return 'physical_path = %s' % (self.val,) - - phash = text - - def __call__(self, context, request): - if getattr(context, '__name__', _marker) is not _marker: - return resource_path_tuple(context) == self.val - return False - -class EffectivePrincipalsPredicate(object): - def __init__(self, val, config): - if is_nonstr_iter(val): - self.val = set(val) - else: - self.val = set((val,)) - - def text(self): - return 'effective_principals = %s' % sorted(list(self.val)) - - phash = text - - def __call__(self, context, request): - req_principals = request.effective_principals - if is_nonstr_iter(req_principals): - rpset = set(req_principals) - if self.val.issubset(rpset): - return True - return False - +import zope.deprecation +zope.deprecation.moved('pyramid.predicates', 'Pyramid 2.0') diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 90d4d47d2..203baa128 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -13,12 +13,10 @@ from pyramid.registry import predvalseq from pyramid.request import route_request_iface from pyramid.urldispatch import RoutesMapper -from pyramid.config.util import ( - action_method, - as_sorted_tuple, - ) +from pyramid.config.util import action_method +from pyramid.util import as_sorted_tuple -import pyramid.config.predicates +import pyramid.predicates class RoutesConfiguratorMixin(object): @action_method @@ -446,7 +444,7 @@ class RoutesConfiguratorMixin(object): ) def add_default_route_predicates(self): - p = pyramid.config.predicates + p = pyramid.predicates for (name, factory) in ( ('xhr', p.XHRPredicate), ('request_method', p.RequestMethodPredicate), diff --git a/pyramid/config/security.py b/pyramid/config/security.py index e387eade9..33593376b 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -9,9 +9,9 @@ from pyramid.interfaces import ( PHASE2_CONFIG, ) -from pyramid.config.util import as_sorted_tuple from pyramid.exceptions import ConfigurationError from pyramid.util import action_method +from pyramid.util import as_sorted_tuple class SecurityConfiguratorMixin(object): @action_method @@ -169,6 +169,7 @@ class SecurityConfiguratorMixin(object): token='csrf_token', header='X-CSRF-Token', safe_methods=('GET', 'HEAD', 'OPTIONS', 'TRACE'), + callback=None, ): """ Set the default CSRF options used by subsequent view registrations. @@ -192,8 +193,20 @@ class SecurityConfiguratorMixin(object): never be automatically checked for CSRF tokens. Default: ``('GET', 'HEAD', 'OPTIONS', TRACE')``. + If ``callback`` is set, it must be a callable accepting ``(request)`` + and returning ``True`` if the request should be checked for a valid + CSRF token. This callback allows an application to support + alternate authentication methods that do not rely on cookies which + are not subject to CSRF attacks. For example, if a request is + authenticated using the ``Authorization`` header instead of a cookie, + this may return ``False`` for that request so that clients do not + need to send the ``X-CSRF-Token` header. The callback is only tested + for non-safe methods as defined by ``safe_methods``. + """ - options = DefaultCSRFOptions(require_csrf, token, header, safe_methods) + options = DefaultCSRFOptions( + require_csrf, token, header, safe_methods, callback, + ) def register(): self.registry.registerUtility(options, IDefaultCSRFOptions) intr = self.introspectable('default csrf view options', @@ -204,13 +217,15 @@ class SecurityConfiguratorMixin(object): intr['token'] = token intr['header'] = header intr['safe_methods'] = as_sorted_tuple(safe_methods) + intr['callback'] = callback self.action(IDefaultCSRFOptions, register, order=PHASE1_CONFIG, introspectables=(intr,)) @implementer(IDefaultCSRFOptions) class DefaultCSRFOptions(object): - def __init__(self, require_csrf, token, header, safe_methods): + def __init__(self, require_csrf, token, header, safe_methods, callback): self.require_csrf = require_csrf self.token = token self.header = header self.safe_methods = frozenset(safe_methods) + self.callback = callback diff --git a/pyramid/config/settings.py b/pyramid/config/settings.py index f9dbd752e..26eb48951 100644 --- a/pyramid/config/settings.py +++ b/pyramid/config/settings.py @@ -1,15 +1,10 @@ import os -import warnings -from zope.interface import implementer - -from pyramid.interfaces import ISettings - -from pyramid.settings import asbool +from pyramid.settings import asbool, aslist class SettingsConfiguratorMixin(object): def _set_settings(self, mapping): - if not mapping: + if mapping is None: mapping = {} settings = Settings(mapping) self.registry.settings = settings @@ -54,118 +49,58 @@ class SettingsConfiguratorMixin(object): return self.registry.settings -@implementer(ISettings) -class Settings(dict): +def Settings(d=None, _environ_=os.environ, **kw): """ Deployment settings. Update application settings (usually from PasteDeploy keywords) with framework-specific key/value pairs (e.g. find ``PYRAMID_DEBUG_AUTHORIZATION`` in os.environ and jam into keyword args).""" - # _environ_ is dep inj for testing - def __init__(self, d=None, _environ_=os.environ, **kw): - if d is None: - d = {} - dict.__init__(self, d, **kw) - eget = _environ_.get - config_debug_all = self.get('debug_all', '') - config_debug_all = self.get('pyramid.debug_all', config_debug_all) - eff_debug_all = asbool(eget('PYRAMID_DEBUG_ALL', config_debug_all)) - config_reload_all = self.get('reload_all', '') - config_reload_all = self.get('pyramid.reload_all', config_reload_all) - eff_reload_all = asbool(eget('PYRAMID_RELOAD_ALL', config_reload_all)) - config_debug_auth = self.get('debug_authorization', '') - config_debug_auth = self.get('pyramid.debug_authorization', - config_debug_auth) - eff_debug_auth = asbool(eget('PYRAMID_DEBUG_AUTHORIZATION', - config_debug_auth)) - config_debug_notfound = self.get('debug_notfound', '') - config_debug_notfound = self.get('pyramid.debug_notfound', - config_debug_notfound) - eff_debug_notfound = asbool(eget('PYRAMID_DEBUG_NOTFOUND', - config_debug_notfound)) - config_debug_routematch = self.get('debug_routematch', '') - config_debug_routematch = self.get('pyramid.debug_routematch', - config_debug_routematch) - eff_debug_routematch = asbool(eget('PYRAMID_DEBUG_ROUTEMATCH', - config_debug_routematch)) - config_debug_templates = self.get('debug_templates', '') - config_debug_templates = self.get('pyramid.debug_templates', - config_debug_templates) - eff_debug_templates = asbool(eget('PYRAMID_DEBUG_TEMPLATES', - config_debug_templates)) - config_reload_templates = self.get('reload_templates', '') - config_reload_templates = self.get('pyramid.reload_templates', - config_reload_templates) - eff_reload_templates = asbool(eget('PYRAMID_RELOAD_TEMPLATES', - config_reload_templates)) - config_reload_assets = self.get('reload_assets', '') - config_reload_assets = self.get('pyramid.reload_assets', - config_reload_assets) - reload_assets = asbool(eget('PYRAMID_RELOAD_ASSETS', - config_reload_assets)) - config_reload_resources = self.get('reload_resources', '') - config_reload_resources = self.get('pyramid.reload_resources', - config_reload_resources) - reload_resources = asbool(eget('PYRAMID_RELOAD_RESOURCES', - config_reload_resources)) - # reload_resources is an older alias for reload_assets - eff_reload_assets = reload_assets or reload_resources - locale_name = self.get('default_locale_name', 'en') - locale_name = self.get('pyramid.default_locale_name', locale_name) - eff_locale_name = eget('PYRAMID_DEFAULT_LOCALE_NAME', locale_name) - config_prevent_http_cache = self.get('prevent_http_cache', '') - config_prevent_http_cache = self.get('pyramid.prevent_http_cache', - config_prevent_http_cache) - eff_prevent_http_cache = asbool(eget('PYRAMID_PREVENT_HTTP_CACHE', - config_prevent_http_cache)) - config_prevent_cachebust = self.get('prevent_cachebust', '') - config_prevent_cachebust = self.get('pyramid.prevent_cachebust', - config_prevent_cachebust) - eff_prevent_cachebust = asbool(eget('PYRAMID_PREVENT_CACHEBUST', - config_prevent_cachebust)) - csrf_trusted_origins = self.get("pyramid.csrf_trusted_origins", []) - eff_csrf_trusted_origins = csrf_trusted_origins - - update = { - 'debug_authorization': eff_debug_all or eff_debug_auth, - 'debug_notfound': eff_debug_all or eff_debug_notfound, - 'debug_routematch': eff_debug_all or eff_debug_routematch, - 'debug_templates': eff_debug_all or eff_debug_templates, - 'reload_templates': eff_reload_all or eff_reload_templates, - 'reload_resources':eff_reload_all or eff_reload_assets, - 'reload_assets':eff_reload_all or eff_reload_assets, - 'default_locale_name':eff_locale_name, - 'prevent_http_cache':eff_prevent_http_cache, - 'prevent_cachebust':eff_prevent_cachebust, - 'csrf_trusted_origins':eff_csrf_trusted_origins, - - 'pyramid.debug_authorization': eff_debug_all or eff_debug_auth, - 'pyramid.debug_notfound': eff_debug_all or eff_debug_notfound, - 'pyramid.debug_routematch': eff_debug_all or eff_debug_routematch, - 'pyramid.debug_templates': eff_debug_all or eff_debug_templates, - 'pyramid.reload_templates': eff_reload_all or eff_reload_templates, - 'pyramid.reload_resources':eff_reload_all or eff_reload_assets, - 'pyramid.reload_assets':eff_reload_all or eff_reload_assets, - 'pyramid.default_locale_name':eff_locale_name, - 'pyramid.prevent_http_cache':eff_prevent_http_cache, - 'pyramid.prevent_cachebust':eff_prevent_cachebust, - 'pyramid.csrf_trusted_origins':eff_csrf_trusted_origins, - } - - self.update(update) - - def __getattr__(self, name): - try: - val = self[name] - # only deprecate on success; a probing getattr/hasattr should not - # print this warning - warnings.warn( - 'Obtaining settings via attributes of the settings dictionary ' - 'is deprecated as of Pyramid 1.2; use settings["foo"] instead ' - 'of settings.foo', - DeprecationWarning, - 2 - ) - return val - except KeyError: - raise AttributeError(name) - + if d is None: + d = {} + d.update(**kw) + + eget = _environ_.get + def expand_key(key): + keys = [key] + if not key.startswith('pyramid.'): + keys.append('pyramid.' + key) + return keys + def S(settings_key, env_key=None, type_=str, default=False): + value = default + keys = expand_key(settings_key) + for key in keys: + value = d.get(key, value) + if env_key: + value = eget(env_key, value) + value = type_(value) + d.update({k: value for k in keys}) + def O(settings_key, override_key): + for key in expand_key(settings_key): + d[key] = d[key] or d[override_key] + + S('debug_all', 'PYRAMID_DEBUG_ALL', asbool) + S('debug_authorization', 'PYRAMID_DEBUG_AUTHORIZATION', asbool) + O('debug_authorization', 'debug_all') + S('debug_notfound', 'PYRAMID_DEBUG_NOTFOUND', asbool) + O('debug_notfound', 'debug_all') + S('debug_routematch', 'PYRAMID_DEBUG_ROUTEMATCH', asbool) + O('debug_routematch', 'debug_all') + S('debug_templates', 'PYRAMID_DEBUG_TEMPLATES', asbool) + O('debug_templates', 'debug_all') + + S('reload_all', 'PYRAMID_RELOAD_ALL', asbool) + S('reload_templates', 'PYRAMID_RELOAD_TEMPLATES', asbool) + O('reload_templates', 'reload_all') + S('reload_assets', 'PYRAMID_RELOAD_ASSETS', asbool) + O('reload_assets', 'reload_all') + S('reload_resources', 'PYRAMID_RELOAD_RESOURCES', asbool) + O('reload_resources', 'reload_all') + # reload_resources is an older alias for reload_assets + for k in expand_key('reload_assets') + expand_key('reload_resources'): + d[k] = d['reload_assets'] or d['reload_resources'] + + S('default_locale_name', 'PYRAMID_DEFAULT_LOCALE_NAME', str, 'en') + S('prevent_http_cache', 'PYRAMID_PREVENT_HTTP_CACHE', asbool) + S('prevent_cachebust', 'PYRAMID_PREVENT_CACHEBUST', asbool) + S('csrf_trusted_origins', 'PYRAMID_CSRF_TRUSTED_ORIGINS', aslist, []) + + return d diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index 0aeb01fe3..16712ab16 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -17,9 +17,9 @@ from pyramid.tweens import ( from pyramid.config.util import ( action_method, - is_string_or_iterable, TopologicalSorter, ) +from pyramid.util import is_string_or_iterable class TweensConfiguratorMixin(object): def add_tween(self, tween_factory, under=None, over=None): diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 626e8d5fe..67bba9593 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -4,8 +4,7 @@ import inspect from pyramid.compat import ( bytes_, getargspec, - is_nonstr_iter, - string_types, + is_nonstr_iter ) from pyramid.compat import im_func @@ -24,18 +23,6 @@ ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() -def is_string_or_iterable(v): - if isinstance(v, string_types): - return True - if hasattr(v, '__iter__'): - return True - -def as_sorted_tuple(val): - if not is_nonstr_iter(val): - val = (val,) - val = tuple(sorted(val)) - return val - class not_(object): """ diff --git a/pyramid/config/views.py b/pyramid/config/views.py index acdc00704..65c9da585 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -70,10 +70,11 @@ import pyramid.util from pyramid.util import ( viewdefaults, action_method, + as_sorted_tuple, TopologicalSorter, ) -import pyramid.config.predicates +import pyramid.predicates import pyramid.viewderivers from pyramid.viewderivers import ( @@ -89,7 +90,6 @@ from pyramid.viewderivers import ( from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, - as_sorted_tuple, ) urljoin = urlparse.urljoin @@ -444,9 +444,11 @@ class ViewsConfiguratorMixin(object): think about preserving function attributes such as ``__name__`` and ``__module__`` within decorator logic). - All view callables in the decorator chain must return a response - object implementing :class:`pyramid.interfaces.IResponse` or raise - an exception: + An important distinction is that each decorator will receive a + response object implementing :class:`pyramid.interfaces.IResponse` + instead of the raw value returned from the view callable. All + decorators in the chain must return a response object or raise an + exception: .. code-block:: python @@ -1141,7 +1143,7 @@ class ViewsConfiguratorMixin(object): ) def add_default_view_predicates(self): - p = pyramid.config.predicates + p = pyramid.predicates for (name, factory) in ( ('xhr', p.XHRPredicate), ('request_method', p.RequestMethodPredicate), diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 054917dfa..a22b088c6 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -246,7 +246,7 @@ ${body}''') 'title': self.title} def prepare(self, environ): - if not self.body and not self.empty_body: + if not self.has_body and not self.empty_body: html_comment = '' comment = self.comment or '' accept_value = environ.get('HTTP_ACCEPT', '') diff --git a/pyramid/i18n.py b/pyramid/i18n.py index 79209d342..1d11adfe3 100644 --- a/pyramid/i18n.py +++ b/pyramid/i18n.py @@ -22,6 +22,7 @@ from pyramid.threadlocal import get_current_registry TranslationString = TranslationString # PyFlakes TranslationStringFactory = TranslationStringFactory # PyFlakes +DEFAULT_PLURAL = lambda n: int(n != 1) class Localizer(object): """ @@ -233,7 +234,13 @@ class Translations(gettext.GNUTranslations, object): # GNUTranslations._parse (called as a side effect if fileobj is # passed to GNUTranslations.__init__) with a "real" self.plural for # this domain; see https://github.com/Pylons/pyramid/issues/235 - self.plural = lambda n: int(n != 1) + # It is only overridden the first time a new message file is found + # for a given domain, so all message files must have matching plural + # rules if they are in the same domain. We keep track of if we have + # overridden so we can special case the default domain, which is always + # instantiated before a message file is read. + # See also https://github.com/Pylons/pyramid/pull/2102 + self.plural = DEFAULT_PLURAL gettext.GNUTranslations.__init__(self, fp=fileobj) self.files = list(filter(None, [getattr(fileobj, 'name', None)])) self.domain = domain @@ -285,6 +292,9 @@ class Translations(gettext.GNUTranslations, object): :rtype: `Translations` """ domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN) + if domain == self.DEFAULT_DOMAIN and self.plural is DEFAULT_PLURAL: + self.plural = translations.plural + if merge and domain == self.domain: return self.merge(translations) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 114f802aa..c1ddea63f 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -925,6 +925,7 @@ class IDefaultCSRFOptions(Interface): token = Attribute('The key to be matched in the body of the request.') header = Attribute('The header to be matched with the CSRF token.') safe_methods = Attribute('A set of safe methods that skip CSRF checks.') + callback = Attribute('A callback to disable CSRF checks per-request.') class ISessionFactory(Interface): """ An interface representing a factory which accepts a request object and diff --git a/pyramid/paster.py b/pyramid/paster.py index 1b7afb5dc..5429a7860 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -5,9 +5,8 @@ from paste.deploy import ( appconfig, ) -from pyramid.compat import configparser -from logging.config import fileConfig from pyramid.scripting import prepare +from pyramid.scripts.common import setup_logging # noqa, api def get_app(config_uri, name=None, options=None, loadapp=loadapp): """ Return the WSGI application named ``name`` in the PasteDeploy @@ -52,30 +51,6 @@ def get_appsettings(config_uri, name=None, options=None, appconfig=appconfig): relative_to=here_dir, global_conf=options) -def setup_logging(config_uri, global_conf=None, - fileConfig=fileConfig, - configparser=configparser): - """ - Set up logging via :func:`logging.config.fileConfig` with the filename - specified via ``config_uri`` (a string in the form - ``filename#sectionname``). - - ConfigParser defaults are specified for the special ``__file__`` - and ``here`` variables, similar to PasteDeploy config loading. - Extra defaults can optionally be specified as a dict in ``global_conf``. - """ - path, _ = _getpathsec(config_uri, None) - parser = configparser.ConfigParser() - parser.read([path]) - if parser.has_section('loggers'): - config_file = os.path.abspath(path) - full_global_conf = dict( - __file__=config_file, - here=os.path.dirname(config_file)) - if global_conf: - full_global_conf.update(global_conf) - return fileConfig(config_file, full_global_conf) - def _getpathsec(config_uri, name): if '#' in config_uri: path, section = config_uri.split('#', 1) diff --git a/pyramid/predicates.py b/pyramid/predicates.py new file mode 100644 index 000000000..7c3a778ca --- /dev/null +++ b/pyramid/predicates.py @@ -0,0 +1,300 @@ +import re + +from pyramid.exceptions import ConfigurationError + +from pyramid.compat import is_nonstr_iter + +from pyramid.session import check_csrf_token +from pyramid.traversal import ( + find_interface, + traversal_path, + resource_path_tuple + ) + +from pyramid.urldispatch import _compile_route +from pyramid.util import object_description +from pyramid.util import as_sorted_tuple + +_marker = object() + +class XHRPredicate(object): + def __init__(self, val, config): + self.val = bool(val) + + def text(self): + return 'xhr = %s' % self.val + + phash = text + + def __call__(self, context, request): + return bool(request.is_xhr) is self.val + +class RequestMethodPredicate(object): + def __init__(self, val, config): + request_method = as_sorted_tuple(val) + if 'GET' in request_method and 'HEAD' not in request_method: + # GET implies HEAD too + request_method = as_sorted_tuple(request_method + ('HEAD',)) + self.val = request_method + + def text(self): + return 'request_method = %s' % (','.join(self.val)) + + phash = text + + def __call__(self, context, request): + return request.method in self.val + +class PathInfoPredicate(object): + def __init__(self, val, config): + self.orig = val + try: + val = re.compile(val) + except re.error as why: + raise ConfigurationError(why.args[0]) + self.val = val + + def text(self): + return 'path_info = %s' % (self.orig,) + + phash = text + + def __call__(self, context, request): + return self.val.match(request.upath_info) is not None + +class RequestParamPredicate(object): + def __init__(self, val, config): + val = as_sorted_tuple(val) + reqs = [] + for p in val: + k = p + v = None + if p.startswith('='): + if '=' in p[1:]: + k, v = p[1:].split('=', 1) + k = '=' + k + k, v = k.strip(), v.strip() + elif '=' in p: + k, v = p.split('=', 1) + k, v = k.strip(), v.strip() + reqs.append((k, v)) + self.val = val + self.reqs = reqs + + def text(self): + return 'request_param %s' % ','.join( + ['%s=%s' % (x,y) if y else x for x, y in self.reqs] + ) + + phash = text + + def __call__(self, context, request): + for k, v in self.reqs: + actual = request.params.get(k) + if actual is None: + return False + if v is not None and actual != v: + return False + return True + +class HeaderPredicate(object): + def __init__(self, val, config): + name = val + v = None + if ':' in name: + name, val_str = name.split(':', 1) + try: + v = re.compile(val_str) + except re.error as why: + raise ConfigurationError(why.args[0]) + if v is None: + self._text = 'header %s' % (name,) + else: + self._text = 'header %s=%s' % (name, val_str) + self.name = name + self.val = v + + def text(self): + return self._text + + phash = text + + def __call__(self, context, request): + if self.val is None: + return self.name in request.headers + val = request.headers.get(self.name) + if val is None: + return False + return self.val.match(val) is not None + +class AcceptPredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'accept = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return self.val in request.accept + +class ContainmentPredicate(object): + def __init__(self, val, config): + self.val = config.maybe_dotted(val) + + def text(self): + return 'containment = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + ctx = getattr(request, 'context', context) + return find_interface(ctx, self.val) is not None + +class RequestTypePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'request_type = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return self.val.providedBy(request) + +class MatchParamPredicate(object): + def __init__(self, val, config): + val = as_sorted_tuple(val) + self.val = val + reqs = [ p.split('=', 1) for p in val ] + self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ] + + def text(self): + return 'match_param %s' % ','.join( + ['%s=%s' % (x,y) for x, y in self.reqs] + ) + + phash = text + + def __call__(self, context, request): + if not request.matchdict: + # might be None + return False + for k, v in self.reqs: + if request.matchdict.get(k) != v: + return False + return True + +class CustomPredicate(object): + def __init__(self, func, config): + self.func = func + + def text(self): + return getattr( + self.func, + '__text__', + 'custom predicate: %s' % object_description(self.func) + ) + + def phash(self): + # using hash() here rather than id() is intentional: we + # want to allow custom predicates that are part of + # frameworks to be able to define custom __hash__ + # functions for custom predicates, so that the hash output + # of predicate instances which are "logically the same" + # may compare equal. + return 'custom:%r' % hash(self.func) + + def __call__(self, context, request): + return self.func(context, request) + + +class TraversePredicate(object): + # Can only be used as a *route* "predicate"; it adds 'traverse' to the + # matchdict if it's specified in the routing args. This causes the + # ResourceTreeTraverser to use the resolved traverse pattern as the + # traversal path. + def __init__(self, val, config): + _, self.tgenerate = _compile_route(val) + self.val = val + + def text(self): + return 'traverse matchdict pseudo-predicate' + + def phash(self): + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we don't + # need to update the hash. + return '' + + def __call__(self, context, request): + if 'traverse' in context: + return True + m = context['match'] + tvalue = self.tgenerate(m) # tvalue will be urlquoted string + m['traverse'] = traversal_path(tvalue) + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we just + # return True. + return True + +class CheckCSRFTokenPredicate(object): + + check_csrf_token = staticmethod(check_csrf_token) # testing + + def __init__(self, val, config): + self.val = val + + def text(self): + return 'check_csrf = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + val = self.val + if val: + if val is True: + val = 'csrf_token' + return self.check_csrf_token(request, val, raises=False) + return True + +class PhysicalPathPredicate(object): + def __init__(self, val, config): + if is_nonstr_iter(val): + self.val = tuple(val) + else: + val = tuple(filter(None, val.split('/'))) + self.val = ('',) + val + + def text(self): + return 'physical_path = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + if getattr(context, '__name__', _marker) is not _marker: + return resource_path_tuple(context) == self.val + return False + +class EffectivePrincipalsPredicate(object): + def __init__(self, val, config): + if is_nonstr_iter(val): + self.val = set(val) + else: + self.val = set((val,)) + + def text(self): + return 'effective_principals = %s' % sorted(list(self.val)) + + phash = text + + def __call__(self, context, request): + req_principals = request.effective_principals + if is_nonstr_iter(req_principals): + rpset = set(req_principals) + if self.val.issubset(rpset): + return True + return False + diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 9b3f19510..47705d5d9 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -194,7 +194,7 @@ class JSON(object): Once this renderer is registered as above, you can use ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or - :meth:`~pyramid.config.Configurator.add_view``: + :meth:`~pyramid.config.Configurator.add_view`: .. code-block:: python diff --git a/pyramid/response.py b/pyramid/response.py index 892e5dfff..1d9daae7d 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -54,16 +54,7 @@ class FileResponse(Response): def __init__(self, path, request=None, cache_max_age=None, content_type=None, content_encoding=None): if content_type is None: - content_type, content_encoding = mimetypes.guess_type( - path, - strict=False - ) - if content_type is None: - content_type = 'application/octet-stream' - # str-ifying content_type is a workaround for a bug in Python 2.7.7 - # on Windows where mimetypes.guess_type returns unicode for the - # content_type. - content_type = str(content_type) + content_type, content_encoding = _guess_type(path) super(FileResponse, self).__init__( conditional_response=True, content_type=content_type, @@ -180,3 +171,17 @@ def _get_response_factory(registry): ) return response_factory + + +def _guess_type(path): + content_type, content_encoding = mimetypes.guess_type( + path, + strict=False + ) + if content_type is None: + content_type = 'application/octet-stream' + # str-ifying content_type is a workaround for a bug in Python 2.7.7 + # on Windows where mimetypes.guess_type returns unicode for the + # content_type. + content_type = str(content_type) + return content_type, content_encoding diff --git a/pyramid/scripts/common.py b/pyramid/scripts/common.py index cbc172e9b..fc141f6e2 100644 --- a/pyramid/scripts/common.py +++ b/pyramid/scripts/common.py @@ -17,20 +17,26 @@ def parse_vars(args): result[name] = value return result -def logging_file_config(config_file, fileConfig=fileConfig, - configparser=configparser): +def setup_logging(config_uri, global_conf=None, + fileConfig=fileConfig, + configparser=configparser): """ - Setup logging via the logging module's fileConfig function with the - specified ``config_file``, if applicable. + Set up logging via :func:`logging.config.fileConfig` with the filename + specified via ``config_uri`` (a string in the form + ``filename#sectionname``). ConfigParser defaults are specified for the special ``__file__`` and ``here`` variables, similar to PasteDeploy config loading. + Extra defaults can optionally be specified as a dict in ``global_conf``. """ + path = config_uri.split('#', 1)[0] parser = configparser.ConfigParser() - parser.read([config_file]) + parser.read([path]) if parser.has_section('loggers'): - config_file = os.path.abspath(config_file) - return fileConfig( - config_file, - dict(__file__=config_file, here=os.path.dirname(config_file)) - ) + config_file = os.path.abspath(path) + full_global_conf = dict( + __file__=config_file, + here=os.path.dirname(config_file)) + if global_conf: + full_global_conf.update(global_conf) + return fileConfig(config_file, full_global_conf) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index 825d8741b..595fd8d77 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -51,6 +51,14 @@ https://github.com/Pylons/?query=cookiecutter action='store_true', help=("A backwards compatibility alias for -l/--list. " "List all available scaffold names.")) + parser.add_option('--package-name', + dest='package_name', + action='store', + type='string', + help='Package name to use. The name provided is assumed ' + 'to be a valid Python package name, and will not ' + 'be validated. By default the package name is ' + 'derived from the value of output_directory.') parser.add_option('--simulate', dest='simulate', action='store_true', @@ -106,9 +114,13 @@ https://github.com/Pylons/?query=cookiecutter def project_vars(self): output_dir = self.output_path project_name = os.path.basename(os.path.split(output_dir)[1]) - pkg_name = _bad_chars_re.sub( - '', project_name.lower().replace('-', '_')) - safe_name = pkg_resources.safe_name(project_name) + if self.options.package_name is None: + pkg_name = _bad_chars_re.sub( + '', project_name.lower().replace('-', '_')) + safe_name = pkg_resources.safe_name(project_name) + else: + pkg_name = self.options.package_name + safe_name = pkg_name egg_name = pkg_resources.to_filename(safe_name) # get pyramid package version diff --git a/pyramid/scripts/prequest.py b/pyramid/scripts/prequest.py index e07f9d10e..14a132bdb 100644 --- a/pyramid/scripts/prequest.py +++ b/pyramid/scripts/prequest.py @@ -5,8 +5,9 @@ import textwrap from pyramid.compat import url_unquote from pyramid.request import Request -from pyramid.paster import get_app, setup_logging +from pyramid.paster import get_app from pyramid.scripts.common import parse_vars +from pyramid.scripts.common import setup_logging def main(argv=sys.argv, quiet=False): command = PRequestCommand(argv, quiet) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index ec7f31704..b8776d44f 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -8,50 +8,32 @@ # Code taken also from QP: http://www.mems-exchange.org/software/qp/ From # lib/site.py -import atexit -import ctypes import optparse import os -import py_compile import re -import subprocess import sys -import tempfile import textwrap import threading import time -import traceback import webbrowser -from paste.deploy import loadserver -from paste.deploy import loadapp -from paste.deploy.loadwsgi import loadcontext, SERVER +import hupper +from paste.deploy import ( + loadapp, + loadserver, +) +from paste.deploy.loadwsgi import ( + SERVER, + loadcontext, +) from pyramid.compat import PY2 -from pyramid.compat import WIN - -from pyramid.paster import setup_logging +from pyramid.compat import configparser from pyramid.scripts.common import parse_vars - -MAXFD = 1024 - -try: - import termios -except ImportError: # pragma: no cover - termios = None - -if WIN and not hasattr(os, 'kill'): # pragma: no cover - # py 2.6 on windows - def kill(pid, sig=None): - """kill function for Win32""" - # signal is ignored, semibogus raise message - kernel32 = ctypes.windll.kernel32 - handle = kernel32.OpenProcess(1, 0, pid) - if (0 == kernel32.TerminateProcess(handle, 0)): - raise OSError('No such process %s' % pid) -else: - kill = os.kill +from pyramid.scripts.common import setup_logging +from pyramid.path import AssetResolver +from pyramid.settings import aslist def main(argv=sys.argv, quiet=False): command = PServeCommand(argv, quiet=quiet) @@ -118,15 +100,17 @@ class PServeCommand(object): dest='verbose', help="Suppress verbose output") - _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I) + ConfigParser = configparser.ConfigParser # testing + loadapp = staticmethod(loadapp) # testing + loadserver = staticmethod(loadserver) # testing - _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN' - _monitor_environ_key = 'PASTE_MONITOR_SHOULD_RUN' + _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I) def __init__(self, argv, quiet=False): self.options, self.args = self.parser.parse_args(argv[1:]) if quiet: self.options.verbose = 0 + self.watch_files = [] def out(self, msg): # pragma: no cover if self.options.verbose > 0: @@ -136,28 +120,47 @@ class PServeCommand(object): restvars = self.args[1:] return parse_vars(restvars) + def pserve_file_config(self, filename, global_conf=None): + here = os.path.abspath(os.path.dirname(filename)) + defaults = {} + if global_conf: + defaults.update(global_conf) + defaults['here'] = here + + config = self.ConfigParser(defaults=defaults) + config.optionxform = str + config.read(filename) + try: + items = dict(config.items('pserve')) + except configparser.NoSectionError: + return + + watch_files = aslist(items.get('watch_files', ''), flatten=False) + + # track file paths relative to the ini file + resolver = AssetResolver(package=None) + for file in watch_files: + if ':' in file: + file = resolver.resolve(file).abspath() + elif not os.path.isabs(file): + file = os.path.join(here, file) + self.watch_files.append(os.path.abspath(file)) + def run(self): # pragma: no cover if not self.args: self.out('You must give a config file') return 2 app_spec = self.args[0] - if self.options.reload: - if os.environ.get(self._reloader_environ_key): - if self.options.verbose > 1: - self.out('Running reloading file monitor') - install_reloader(int(self.options.reload_interval), [app_spec]) - # if self.requires_config_file: - # watch_file(self.args[0]) - else: - return self.restart_with_reloader() - - app_name = self.options.app_name - vars = self.get_options() + app_name = self.options.app_name + base = os.getcwd() if not self._scheme_re.search(app_spec): + config_path = os.path.join(base, app_spec) app_spec = 'config:' + app_spec + else: + config_path = None server_name = self.options.server_name if self.options.server: server_spec = 'egg:pyramid' @@ -165,46 +168,13 @@ class PServeCommand(object): server_name = self.options.server else: server_spec = app_spec - base = os.getcwd() - - log_fn = app_spec - if log_fn.startswith('config:'): - log_fn = app_spec[len('config:'):] - elif log_fn.startswith('egg:'): - log_fn = None - if log_fn: - log_fn = os.path.join(base, log_fn) - setup_logging(log_fn, global_conf=vars) - - server = self.loadserver(server_spec, name=server_name, - relative_to=base, global_conf=vars) - - app = self.loadapp(app_spec, name=app_name, relative_to=base, - global_conf=vars) - if self.options.verbose > 0: - if hasattr(os, 'getpid'): - msg = 'Starting server in PID %i.' % os.getpid() - else: - msg = 'Starting server.' - self.out(msg) - - def serve(): - try: - server(app) - except (SystemExit, KeyboardInterrupt) as e: - if self.options.verbose > 1: - raise - if str(e): - msg = ' ' + str(e) - else: - msg = '' - self.out('Exiting%s (-v to see traceback)' % msg) - - if self.options.browser: + # do not open the browser on each reload so check hupper first + if self.options.browser and not hupper.is_active(): def open_browser(): - context = loadcontext(SERVER, app_spec, name=server_name, relative_to=base, - global_conf=vars) + context = loadcontext( + SERVER, app_spec, name=server_name, relative_to=base, + global_conf=vars) url = 'http://127.0.0.1:{port}/'.format(**context.config()) time.sleep(1) webbrowser.open(url) @@ -212,402 +182,48 @@ class PServeCommand(object): t.setDaemon(True) t.start() - serve() - - def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover - return loadapp(app_spec, name=name, relative_to=relative_to, **kw) - - def loadserver(self, server_spec, name, relative_to, **kw):# pragma:no cover - return loadserver( - server_spec, name=name, relative_to=relative_to, **kw) - - def quote_first_command_arg(self, arg): # pragma: no cover - """ - There's a bug in Windows when running an executable that's - located inside a path with a space in it. This method handles - that case, or on non-Windows systems or an executable with no - spaces, it just leaves well enough alone. - """ - if (sys.platform != 'win32' or ' ' not in arg): - # Problem does not apply: - return arg - try: - import win32api - except ImportError: - raise ValueError( - "The executable %r contains a space, and in order to " - "handle this issue you must have the win32api module " - "installed" % arg) - arg = win32api.GetShortPathName(arg) - return arg - - def find_script_path(self, name): # pragma: no cover - """ - Return the path to the script being invoked by the python interpreter. - - There's an issue on Windows when running the executable from - a console_script causing the script name (sys.argv[0]) to - not end with .exe or .py and thus cannot be run via popen. - """ - if sys.platform == 'win32': - if not name.endswith('.exe') and not name.endswith('.py'): - name += '.exe' - return name - - def restart_with_reloader(self): # pragma: no cover - self.restart_with_monitor(reloader=True) - - def restart_with_monitor(self, reloader=False): # pragma: no cover - if self.options.verbose > 0: - if reloader: - self.out('Starting subprocess with file monitor') - else: - self.out('Starting subprocess with monitor parent') - while 1: - args = [ - self.quote_first_command_arg(sys.executable), - self.find_script_path(sys.argv[0]), - ] + sys.argv[1:] - new_environ = os.environ.copy() - if reloader: - new_environ[self._reloader_environ_key] = 'true' - else: - new_environ[self._monitor_environ_key] = 'true' - proc = None - try: - try: - _turn_sigterm_into_systemexit() - proc = subprocess.Popen(args, env=new_environ) - exit_code = proc.wait() - proc = None - except KeyboardInterrupt: - self.out('^C caught in monitor process') - if self.options.verbose > 1: - raise - return 1 - finally: - if proc is not None: - import signal - try: - kill(proc.pid, signal.SIGTERM) - except (OSError, IOError): - pass - - if reloader: - # Reloader always exits with code 3; but if we are - # a monitor, any exit code will restart - if exit_code != 3: - return exit_code - if self.options.verbose > 0: - self.out('%s %s %s' % ('-' * 20, 'Restarting', '-' * 20)) - -class LazyWriter(object): - - """ - File-like object that opens a file lazily when it is first written - to. - """ - - def __init__(self, filename, mode='w'): - self.filename = filename - self.fileobj = None - self.lock = threading.Lock() - self.mode = mode - - def open(self): - if self.fileobj is None: - with self.lock: - self.fileobj = open(self.filename, self.mode) - return self.fileobj - - def close(self): - fileobj = self.fileobj - if fileobj is not None: - fileobj.close() - - def __del__(self): - self.close() - - def write(self, text): - fileobj = self.open() - fileobj.write(text) - fileobj.flush() - - def writelines(self, text): - fileobj = self.open() - fileobj.writelines(text) - fileobj.flush() - - def flush(self): - self.open().flush() - -def ensure_port_cleanup( - bound_addresses, maxtries=30, sleeptime=2): # pragma: no cover - """ - This makes sure any open ports are closed. - - Does this by connecting to them until they give connection - refused. Servers should call like:: - - ensure_port_cleanup([80, 443]) - """ - atexit.register(_cleanup_ports, bound_addresses, maxtries=maxtries, - sleeptime=sleeptime) - -def _cleanup_ports( - bound_addresses, maxtries=30, sleeptime=2): # pragma: no cover - # Wait for the server to bind to the port. - import socket - import errno - for bound_address in bound_addresses: - for attempt in range(maxtries): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - sock.connect(bound_address) - except socket.error as e: - if e.args[0] != errno.ECONNREFUSED: - raise - break - else: - time.sleep(sleeptime) - else: - raise SystemExit('Timeout waiting for port.') - sock.close() - -def _turn_sigterm_into_systemexit(): # pragma: no cover - """ - Attempts to turn a SIGTERM exception into a SystemExit exception. - """ - try: - import signal - except ImportError: - return - def handle_term(signo, frame): - raise SystemExit - signal.signal(signal.SIGTERM, handle_term) - -def ensure_echo_on(): # pragma: no cover - if termios: - fd = sys.stdin - if fd.isatty(): - attr_list = termios.tcgetattr(fd) - if not attr_list[3] & termios.ECHO: - attr_list[3] |= termios.ECHO - termios.tcsetattr(fd, termios.TCSANOW, attr_list) - -def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover - """ - Install the reloading monitor. - - On some platforms server threads may not terminate when the main - thread does, causing ports to remain open/locked. - """ - ensure_echo_on() - mon = Monitor(poll_interval=poll_interval) - if extra_files is None: - extra_files = [] - mon.extra_files.extend(extra_files) - t = threading.Thread(target=mon.periodic_reload) - t.setDaemon(True) - t.start() - -class classinstancemethod(object): - """ - Acts like a class method when called from a class, like an - instance method when called by an instance. The method should - take two arguments, 'self' and 'cls'; one of these will be None - depending on how the method was called. - """ - - def __init__(self, func): - self.func = func - self.__doc__ = func.__doc__ - - def __get__(self, obj, type=None): - return _methodwrapper(self.func, obj=obj, type=type) - -class _methodwrapper(object): - - def __init__(self, func, obj, type): - self.func = func - self.obj = obj - self.type = type - - def __call__(self, *args, **kw): - assert 'self' not in kw and 'cls' not in kw, ( - "You cannot use 'self' or 'cls' arguments to a " - "classinstancemethod") - return self.func(*((self.obj, self.type) + args), **kw) - - -class Monitor(object): # pragma: no cover - """ - A file monitor and server restarter. - - Use this like: + if self.options.reload and not hupper.is_active(): + if self.options.verbose > 1: + self.out('Running reloading file monitor') + hupper.start_reloader( + 'pyramid.scripts.pserve.main', + reload_interval=int(self.options.reload_interval), + verbose=self.options.verbose, + ) + return 0 - ..code-block:: Python + if config_path: + setup_logging(config_path, global_conf=vars) + self.pserve_file_config(config_path, global_conf=vars) + self.watch_files.append(config_path) - install_reloader() + if hupper.is_active(): + reloader = hupper.get_reloader() + reloader.watch_files(self.watch_files) - Then make sure your server is installed with a shell script like:: + server = self.loadserver( + server_spec, name=server_name, relative_to=base, global_conf=vars) - err=3 - while test "$err" -eq 3 ; do - python server.py - err="$?" - done + app = self.loadapp( + app_spec, name=app_name, relative_to=base, global_conf=vars) - or is run from this .bat file (if you use Windows):: - - @echo off - :repeat - python server.py - if %errorlevel% == 3 goto repeat - - or run a monitoring process in Python (``pserve --reload`` does - this). - - Use the ``watch_file(filename)`` function to cause a reload/restart for - other non-Python files (e.g., configuration files). If you have - a dynamic set of files that grows over time you can use something like:: - - def watch_config_files(): - return CONFIG_FILE_CACHE.keys() - add_file_callback(watch_config_files) + if self.options.verbose > 0: + if hasattr(os, 'getpid'): + msg = 'Starting server in PID %i.' % os.getpid() + else: + msg = 'Starting server.' + self.out(msg) - Then every time the reloader polls files it will call - ``watch_config_files`` and check all the filenames it returns. - """ - instances = [] - global_extra_files = [] - global_file_callbacks = [] - - def __init__(self, poll_interval): - self.module_mtimes = {} - self.keep_running = True - self.poll_interval = poll_interval - self.extra_files = list(self.global_extra_files) - self.instances.append(self) - self.syntax_error_files = set() - self.pending_reload = False - self.file_callbacks = list(self.global_file_callbacks) - temp_pyc_fp = tempfile.NamedTemporaryFile(delete=False) - self.temp_pyc = temp_pyc_fp.name - temp_pyc_fp.close() - - def _exit(self): try: - os.unlink(self.temp_pyc) - except IOError: - # not worried if the tempfile can't be removed - pass - # use os._exit() here and not sys.exit() since within a - # thread sys.exit() just closes the given thread and - # won't kill the process; note os._exit does not call - # any atexit callbacks, nor does it do finally blocks, - # flush open files, etc. In otherwords, it is rude. - os._exit(3) - - def periodic_reload(self): - while True: - if not self.check_reload(): - self._exit() - break - time.sleep(self.poll_interval) - - def check_reload(self): - filenames = list(self.extra_files) - for file_callback in self.file_callbacks: - try: - filenames.extend(file_callback()) - except: - print( - "Error calling reloader callback %r:" % file_callback) - traceback.print_exc() - for module in list(sys.modules.values()): - try: - filename = module.__file__ - except (AttributeError, ImportError): - continue - if filename is not None: - filenames.append(filename) - new_changes = False - for filename in filenames: - try: - stat = os.stat(filename) - if stat: - mtime = stat.st_mtime - else: - mtime = 0 - except (OSError, IOError): - continue - if filename.endswith('.pyc') and os.path.exists(filename[:-1]): - mtime = max(os.stat(filename[:-1]).st_mtime, mtime) - pyc = True + server(app) + except (SystemExit, KeyboardInterrupt) as e: + if self.options.verbose > 1: + raise + if str(e): + msg = ' ' + str(e) else: - pyc = False - old_mtime = self.module_mtimes.get(filename) - self.module_mtimes[filename] = mtime - if old_mtime is not None and old_mtime < mtime: - new_changes = True - if pyc: - filename = filename[:-1] - is_valid = True - if filename.endswith('.py'): - is_valid = self.check_syntax(filename) - if is_valid: - print("%s changed ..." % filename) - if new_changes: - self.pending_reload = True - if self.syntax_error_files: - for filename in sorted(self.syntax_error_files): - print("%s has a SyntaxError; NOT reloading." % filename) - if self.pending_reload and not self.syntax_error_files: - self.pending_reload = False - return False - return True - - def check_syntax(self, filename): - # check if a file has syntax errors. - # If so, track it until it's fixed. - try: - py_compile.compile(filename, cfile=self.temp_pyc, doraise=True) - except py_compile.PyCompileError as ex: - print(ex.msg) - self.syntax_error_files.add(filename) - return False - else: - if filename in self.syntax_error_files: - self.syntax_error_files.remove(filename) - return True - - def watch_file(self, cls, filename): - """Watch the named file for changes""" - filename = os.path.abspath(filename) - if self is None: - for instance in cls.instances: - instance.watch_file(filename) - cls.global_extra_files.append(filename) - else: - self.extra_files.append(filename) - - watch_file = classinstancemethod(watch_file) - - def add_file_callback(self, cls, callback): - """Add a callback -- a function that takes no parameters -- that will - return a list of filenames to watch for changes.""" - if self is None: - for instance in cls.instances: - instance.add_file_callback(callback) - cls.global_file_callbacks.append(callback) - else: - self.file_callbacks.append(callback) - - add_file_callback = classinstancemethod(add_file_callback) - -watch_file = Monitor.watch_file -add_file_callback = Monitor.add_file_callback + msg = '' + self.out('Exiting%s (-v to see traceback)' % msg) # For paste.deploy server instantiation (egg:pyramid#wsgiref) def wsgiref_server_runner(wsgi_app, global_conf, **kw): # pragma: no cover diff --git a/pyramid/scripts/pshell.py b/pyramid/scripts/pshell.py index 0a7cfbbe5..56b1a15fa 100644 --- a/pyramid/scripts/pshell.py +++ b/pyramid/scripts/pshell.py @@ -10,11 +10,10 @@ from pyramid.compat import exec_ from pyramid.util import DottedNameResolver from pyramid.paster import bootstrap -from pyramid.paster import setup_logging - from pyramid.settings import aslist from pyramid.scripts.common import parse_vars +from pyramid.scripts.common import setup_logging def main(argv=sys.argv, quiet=False): command = PShellCommand(argv, quiet) diff --git a/pyramid/session.py b/pyramid/session.py index a3cbe5172..47b80f617 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -215,9 +215,9 @@ def check_csrf_token(request, supplied by ``request.session.get_csrf_token()``, and ``raises`` is ``True``, this function will raise an :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. + If the values differ and ``raises`` is ``False``, this function will + return ``False``. If the CSRF check is successful, this function will + return ``True`` unconditionally. Note that using this function requires that a :term:`session factory` is configured. diff --git a/pyramid/static.py b/pyramid/static.py index 0965be95c..a8088129e 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -32,7 +32,12 @@ from pyramid.httpexceptions import ( ) from pyramid.path import caller_package -from pyramid.response import FileResponse + +from pyramid.response import ( + _guess_type, + FileResponse, +) + from pyramid.traversal import traversal_path_info slash = text_('/') @@ -83,7 +88,7 @@ class static_view(object): """ def __init__(self, root_dir, cache_max_age=3600, package_name=None, - use_subpath=False, index='index.html', cachebust_match=None): + use_subpath=False, index='index.html'): # package_name is for bw compat; it is preferred to pass in a # package-relative path as root_dir # (e.g. ``anotherpackage:foo/static``). @@ -96,15 +101,12 @@ class static_view(object): self.docroot = docroot self.norm_docroot = normcase(normpath(docroot)) self.index = index - self.cachebust_match = cachebust_match def __call__(self, context, request): if self.use_subpath: path_tuple = request.subpath else: path_tuple = traversal_path_info(request.environ['PATH_INFO']) - if self.cachebust_match: - path_tuple = self.cachebust_match(path_tuple) path = _secure_path(path_tuple) if path is None: @@ -134,7 +136,10 @@ class static_view(object): if not exists(filepath): raise HTTPNotFound(request.url) - return FileResponse(filepath, request, self.cache_max_age) + content_type, content_encoding = _guess_type(filepath) + return FileResponse( + filepath, request, self.cache_max_age, + content_type, content_encoding=None) def add_slash_redirect(self, request): url = request.path_url + '/' diff --git a/pyramid/tests/test_config/test_security.py b/pyramid/tests/test_config/test_security.py index e461bfd4a..5db8e21fc 100644 --- a/pyramid/tests/test_config/test_security.py +++ b/pyramid/tests/test_config/test_security.py @@ -108,14 +108,18 @@ class ConfiguratorSecurityMethodsTests(unittest.TestCase): self.assertEqual(result.header, 'X-CSRF-Token') self.assertEqual(list(sorted(result.safe_methods)), ['GET', 'HEAD', 'OPTIONS', 'TRACE']) + self.assertTrue(result.callback is None) def test_changing_set_default_csrf_options(self): from pyramid.interfaces import IDefaultCSRFOptions config = self._makeOne(autocommit=True) + def callback(request): return True config.set_default_csrf_options( - require_csrf=False, token='DUMMY', header=None, safe_methods=('PUT',)) + require_csrf=False, token='DUMMY', header=None, + safe_methods=('PUT',), callback=callback) result = config.registry.getUtility(IDefaultCSRFOptions) self.assertEqual(result.require_csrf, False) self.assertEqual(result.token, 'DUMMY') self.assertEqual(result.header, None) self.assertEqual(list(sorted(result.safe_methods)), ['PUT']) + self.assertTrue(result.callback is callback) diff --git a/pyramid/tests/test_config/test_settings.py b/pyramid/tests/test_config/test_settings.py index d2a98b347..2dbe9b1bb 100644 --- a/pyramid/tests/test_config/test_settings.py +++ b/pyramid/tests/test_config/test_settings.py @@ -11,6 +11,13 @@ class TestSettingsConfiguratorMixin(unittest.TestCase): settings = config._set_settings(None) self.assertTrue(settings) + def test__set_settings_uses_original_dict(self): + config = self._makeOne() + dummy = {} + result = config._set_settings(dummy) + self.assertTrue(dummy is result) + self.assertEqual(dummy['pyramid.debug_all'], False) + def test__set_settings_as_dictwithvalues(self): config = self._makeOne() settings = config._set_settings({'a':'1'}) @@ -68,26 +75,6 @@ class TestSettings(unittest.TestCase): klass = self._getTargetClass() return klass(d, _environ_=environ) - def test_getattr_success(self): - import warnings - with warnings.catch_warnings(record=True) as w: - warnings.filterwarnings('always') - settings = self._makeOne({'reload_templates':False}) - self.assertEqual(settings.reload_templates, False) - self.assertEqual(len(w), 1) - - def test_getattr_fail(self): - import warnings - with warnings.catch_warnings(record=True) as w: - warnings.filterwarnings('always') - settings = self._makeOne({}) - self.assertRaises(AttributeError, settings.__getattr__, 'wontexist') - self.assertEqual(len(w), 0) - - def test_getattr_raises_attribute_error(self): - settings = self._makeOne() - self.assertRaises(AttributeError, settings.__getattr__, 'mykey') - def test_noargs(self): settings = self._makeOne() self.assertEqual(settings['debug_authorization'], False) @@ -557,6 +544,18 @@ class TestSettings(unittest.TestCase): self.assertEqual(result['default_locale_name'], 'abc') self.assertEqual(result['pyramid.default_locale_name'], 'abc') + def test_csrf_trusted_origins(self): + result = self._makeOne({}) + self.assertEqual(result['pyramid.csrf_trusted_origins'], []) + result = self._makeOne({'pyramid.csrf_trusted_origins': 'example.com'}) + self.assertEqual(result['pyramid.csrf_trusted_origins'], ['example.com']) + result = self._makeOne({'pyramid.csrf_trusted_origins': ['example.com']}) + self.assertEqual(result['pyramid.csrf_trusted_origins'], ['example.com']) + result = self._makeOne({'pyramid.csrf_trusted_origins': ( + 'example.com foo.example.com\nasdf.example.com')}) + self.assertEqual(result['pyramid.csrf_trusted_origins'], [ + 'example.com', 'foo.example.com', 'asdf.example.com']) + def test_originals_kept(self): result = self._makeOne({'a':'i am so a'}) self.assertEqual(result['a'], 'i am so a') diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index ccf7fa260..398b6fba8 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -5,7 +5,7 @@ class TestPredicateList(unittest.TestCase): def _makeOne(self): from pyramid.config.util import PredicateList - from pyramid.config import predicates + from pyramid import predicates inst = PredicateList() for name, factory in ( ('xhr', predicates.XHRPredicate), @@ -594,6 +594,15 @@ class TestNotted(unittest.TestCase): self.assertEqual(inst.phash(), '') self.assertEqual(inst(None, None), True) + +class TestDeprecatedPredicates(unittest.TestCase): + def test_it(self): + import warnings + with warnings.catch_warnings(record=True) as w: + warnings.filterwarnings('always') + from pyramid.config.predicates import XHRPredicate + self.assertEqual(len(w), 1) + class DummyPredicate(object): def __init__(self, result): self.result = result diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index f020485de..211632730 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -2309,9 +2309,9 @@ class TestViewsConfigurationMixin(unittest.TestCase): # Since Python 3 has to be all cool and fancy and different... def _assertBody(self, response, value): from pyramid.compat import text_type - if isinstance(value, text_type): # pragma: nocover + if isinstance(value, text_type): # pragma: no cover self.assertEqual(response.text, value) - else: # pragma: nocover + else: # pragma: no cover self.assertEqual(response.body, value) def test_add_notfound_view_with_renderer(self): diff --git a/pyramid/tests/test_httpexceptions.py b/pyramid/tests/test_httpexceptions.py index 6c6e16d55..224fa4cf0 100644 --- a/pyramid/tests/test_httpexceptions.py +++ b/pyramid/tests/test_httpexceptions.py @@ -348,7 +348,7 @@ class TestHTTPException(unittest.TestCase): exc = cls(body_template='${REQUEST_METHOD}') environ = _makeEnviron() class Choke(object): - def __str__(self): # pragma nocover + def __str__(self): # pragma no cover raise ValueError environ['gardentheory.user'] = Choke() start_response = DummyStartResponse() diff --git a/pyramid/tests/test_i18n.py b/pyramid/tests/test_i18n.py index 67b2ac356..d72d0d480 100644 --- a/pyramid/tests/test_i18n.py +++ b/pyramid/tests/test_i18n.py @@ -357,6 +357,36 @@ class TestTranslations(unittest.TestCase): inst.add(inst2) self.assertEqual(inst._catalog['a'], 'b') + def test_add_default_domain_replaces_plural_first_time(self): + # Create three empty message catalogs in the default domain + inst = self._getTargetClass()(None, domain='messages') + inst2 = self._getTargetClass()(None, domain='messages') + inst3 = self._getTargetClass()(None, domain='messages') + inst._catalog = {} + inst2._catalog = {} + inst3._catalog = {} + + # The default plural scheme is the germanic one + self.assertEqual(inst.plural(0), 1) + self.assertEqual(inst.plural(1), 0) + self.assertEqual(inst.plural(2), 1) + + # inst2 represents a message file that declares french plurals + inst2.plural = lambda n: n > 1 + inst.add(inst2) + # that plural rule should now apply to inst + self.assertEqual(inst.plural(0), 0) + self.assertEqual(inst.plural(1), 0) + self.assertEqual(inst.plural(2), 1) + + # We load a second message file with different plural rules + inst3.plural = lambda n: n > 0 + inst.add(inst3) + # It doesn't override the previously loaded rule + self.assertEqual(inst.plural(0), 0) + self.assertEqual(inst.plural(1), 0) + self.assertEqual(inst.plural(2), 1) + def test_dgettext(self): t = self._makeOne() self.assertEqual(t.dgettext('messages', 'foo'), 'Voh') diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_predicates.py index 9cd8f2734..8a002c24e 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_predicates.py @@ -6,7 +6,7 @@ from pyramid.compat import text_ class TestXHRPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import XHRPredicate + from pyramid.predicates import XHRPredicate return XHRPredicate(val, None) def test___call___true(self): @@ -33,7 +33,7 @@ class TestXHRPredicate(unittest.TestCase): class TestRequestMethodPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import RequestMethodPredicate + from pyramid.predicates import RequestMethodPredicate return RequestMethodPredicate(val, None) def test_ctor_get_but_no_head(self): @@ -71,7 +71,7 @@ class TestRequestMethodPredicate(unittest.TestCase): class TestPathInfoPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import PathInfoPredicate + from pyramid.predicates import PathInfoPredicate return PathInfoPredicate(val, None) def test_ctor_compilefail(self): @@ -102,7 +102,7 @@ class TestPathInfoPredicate(unittest.TestCase): class TestRequestParamPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import RequestParamPredicate + from pyramid.predicates import RequestParamPredicate return RequestParamPredicate(val, None) def test___call___true_exists(self): @@ -174,7 +174,7 @@ class TestRequestParamPredicate(unittest.TestCase): class TestMatchParamPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import MatchParamPredicate + from pyramid.predicates import MatchParamPredicate return MatchParamPredicate(val, None) def test___call___true_single(self): @@ -216,7 +216,7 @@ class TestMatchParamPredicate(unittest.TestCase): class TestCustomPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import CustomPredicate + from pyramid.predicates import CustomPredicate return CustomPredicate(val, None) def test___call___true(self): @@ -255,7 +255,7 @@ class TestCustomPredicate(unittest.TestCase): class TestTraversePredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import TraversePredicate + from pyramid.predicates import TraversePredicate return TraversePredicate(val, None) def test___call__traverse_has_remainder_already(self): @@ -297,7 +297,7 @@ class TestTraversePredicate(unittest.TestCase): class Test_CheckCSRFTokenPredicate(unittest.TestCase): def _makeOne(self, val, config): - from pyramid.config.predicates import CheckCSRFTokenPredicate + from pyramid.predicates import CheckCSRFTokenPredicate return CheckCSRFTokenPredicate(val, config) def test_text(self): @@ -340,7 +340,7 @@ class Test_CheckCSRFTokenPredicate(unittest.TestCase): class TestHeaderPredicate(unittest.TestCase): def _makeOne(self, val): - from pyramid.config.predicates import HeaderPredicate + from pyramid.predicates import HeaderPredicate return HeaderPredicate(val, None) def test___call___true_exists(self): @@ -404,7 +404,7 @@ class TestHeaderPredicate(unittest.TestCase): class Test_PhysicalPathPredicate(unittest.TestCase): def _makeOne(self, val, config): - from pyramid.config.predicates import PhysicalPathPredicate + from pyramid.predicates import PhysicalPathPredicate return PhysicalPathPredicate(val, config) def test_text(self): @@ -468,7 +468,7 @@ class Test_EffectivePrincipalsPredicate(unittest.TestCase): testing.tearDown() def _makeOne(self, val, config): - from pyramid.config.predicates import EffectivePrincipalsPredicate + from pyramid.predicates import EffectivePrincipalsPredicate return EffectivePrincipalsPredicate(val, config) def test_text(self): diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py index f3aa20e7c..ced09d0b0 100644 --- a/pyramid/tests/test_scripts/dummy.py +++ b/pyramid/tests/test_scripts/dummy.py @@ -82,8 +82,9 @@ class DummyMultiView(object): self.__request_attrs__ = attrs class DummyConfigParser(object): - def __init__(self, result): + def __init__(self, result, defaults=None): self.result = result + self.defaults = defaults def read(self, filename): self.filename = filename @@ -98,8 +99,9 @@ class DummyConfigParser(object): class DummyConfigParserFactory(object): items = None - def __call__(self): - self.parser = DummyConfigParser(self.items) + def __call__(self, defaults=None): + self.defaults = defaults + self.parser = DummyConfigParser(self.items, defaults) return self.parser class DummyCloser(object): diff --git a/pyramid/tests/test_scripts/test_common.py b/pyramid/tests/test_scripts/test_common.py index 13ab0ae6a..60741db92 100644 --- a/pyramid/tests/test_scripts/test_common.py +++ b/pyramid/tests/test_scripts/test_common.py @@ -1,22 +1,5 @@ -import os import unittest -class Test_logging_file_config(unittest.TestCase): - def _callFUT(self, config_file): - from pyramid.scripts.common import logging_file_config - dummy_cp = DummyConfigParserModule - return logging_file_config(config_file, self.fileConfig, dummy_cp) - - def test_it(self): - config_file, dict = self._callFUT('/abc') - # use of os.path.abspath here is a sop to Windows - self.assertEqual(config_file, os.path.abspath('/abc')) - self.assertEqual(dict['__file__'], os.path.abspath('/abc')) - self.assertEqual(dict['here'], os.path.abspath('/')) - - def fileConfig(self, config_file, dict): - return config_file, dict - class TestParseVars(unittest.TestCase): def test_parse_vars_good(self): from pyramid.scripts.common import parse_vars @@ -28,16 +11,3 @@ class TestParseVars(unittest.TestCase): from pyramid.scripts.common import parse_vars vars = ['a'] self.assertRaises(ValueError, parse_vars, vars) - - -class DummyConfigParser(object): - def read(self, x): - pass - - def has_section(self, name): - return True - -class DummyConfigParserModule(object): - ConfigParser = DummyConfigParser - - diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 8e6cc2847..0286614ce 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -80,6 +80,27 @@ class TestPCreateCommand(unittest.TestCase): {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + def test_scaffold_with_package_name(self): + import os + cmd = self._makeOne('-s', 'dummy', '--package-name', 'dummy_package', + 'Distro') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.1") + result = cmd.run() + + self.assertEqual(result, 0) + self.assertEqual( + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'dummy_package', + 'package': 'dummy_package', 'pyramid_version': '0.1', + 'pyramid_docs_branch':'0.1-branch'}) + + def test_scaffold_with_hyphen_in_project_name(self): import os cmd = self._makeOne('-s', 'dummy', 'Distro-') diff --git a/pyramid/tests/test_scripts/test_prequest.py b/pyramid/tests/test_scripts/test_prequest.py index 95cec0518..45db0dbaf 100644 --- a/pyramid/tests/test_scripts/test_prequest.py +++ b/pyramid/tests/test_scripts/test_prequest.py @@ -34,7 +34,8 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._out, ['You must provide at least two arguments']) def test_command_two_args(self): - command = self._makeOne(['', 'development.ini', '/']) + command = self._makeOne(['', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._path_info, '/') self.assertEqual(self._spec, 'development.ini') @@ -42,7 +43,8 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._out, ['abc']) def test_command_path_doesnt_start_with_slash(self): - command = self._makeOne(['', 'development.ini', 'abc']) + command = self._makeOne(['', 'development.ini', 'abc'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._path_info, '/abc') self.assertEqual(self._spec, 'development.ini') @@ -60,7 +62,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_has_good_header_var(self): command = self._makeOne( - ['', '--header=name:value','development.ini', '/']) + ['', '--header=name:value','development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['HTTP_NAME'], 'value') self.assertEqual(self._path_info, '/') @@ -71,7 +74,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_w_basic_auth(self): command = self._makeOne( ['', '--login=user:password', - '--header=name:value','development.ini', '/']) + '--header=name:value','development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['HTTP_NAME'], 'value') self.assertEqual(self._environ['HTTP_AUTHORIZATION'], @@ -83,7 +87,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_has_content_type_header_var(self): command = self._makeOne( - ['', '--header=content-type:app/foo','development.ini', '/']) + ['', '--header=content-type:app/foo','development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['CONTENT_TYPE'], 'app/foo') self.assertEqual(self._path_info, '/') @@ -97,7 +102,9 @@ class TestPRequestCommand(unittest.TestCase): '--header=name:value', '--header=name2:value2', 'development.ini', - '/']) + '/'], + [('Content-Type', 'text/html; charset=UTF-8')] + ) command.run() self.assertEqual(self._environ['HTTP_NAME'], 'value') self.assertEqual(self._environ['HTTP_NAME2'], 'value2') @@ -107,7 +114,8 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._out, ['abc']) def test_command_method_get(self): - command = self._makeOne(['', '--method=GET', 'development.ini', '/']) + command = self._makeOne(['', '--method=GET', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['REQUEST_METHOD'], 'GET') self.assertEqual(self._path_info, '/') @@ -117,7 +125,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_post(self): from pyramid.compat import NativeIO - command = self._makeOne(['', '--method=POST', 'development.ini', '/']) + command = self._makeOne(['', '--method=POST', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -131,7 +140,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_put(self): from pyramid.compat import NativeIO - command = self._makeOne(['', '--method=PUT', 'development.ini', '/']) + command = self._makeOne(['', '--method=PUT', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -145,7 +155,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_patch(self): from pyramid.compat import NativeIO - command = self._makeOne(['', '--method=PATCH', 'development.ini', '/']) + command = self._makeOne(['', '--method=PATCH', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -160,7 +171,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_propfind(self): from pyramid.compat import NativeIO command = self._makeOne(['', '--method=PROPFIND', 'development.ini', - '/']) + '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -173,7 +185,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_method_options(self): from pyramid.compat import NativeIO command = self._makeOne(['', '--method=OPTIONS', 'development.ini', - '/']) + '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) stdin = NativeIO() command.stdin = stdin command.run() @@ -184,7 +197,8 @@ class TestPRequestCommand(unittest.TestCase): self.assertEqual(self._out, ['abc']) def test_command_with_query_string(self): - command = self._makeOne(['', 'development.ini', '/abc?a=1&b=2&c']) + command = self._makeOne(['', 'development.ini', '/abc?a=1&b=2&c'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._environ['QUERY_STRING'], 'a=1&b=2&c') self.assertEqual(self._path_info, '/abc') @@ -194,7 +208,8 @@ class TestPRequestCommand(unittest.TestCase): def test_command_display_headers(self): command = self._makeOne( - ['', '--display-headers', 'development.ini', '/']) + ['', '--display-headers', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) command.run() self.assertEqual(self._path_info, '/') self.assertEqual(self._spec, 'development.ini') diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py index bf4763602..18f7c8c2f 100644 --- a/pyramid/tests/test_scripts/test_pserve.py +++ b/pyramid/tests/test_scripts/test_pserve.py @@ -1,11 +1,14 @@ import os -import tempfile import unittest +from pyramid.tests.test_scripts import dummy + +here = os.path.abspath(os.path.dirname(__file__)) class TestPServeCommand(unittest.TestCase): def setUp(self): from pyramid.compat import NativeIO self.out_ = NativeIO() + self.config_factory = dummy.DummyConfigParserFactory() def out(self, msg): self.out_.write(msg) @@ -13,7 +16,6 @@ class TestPServeCommand(unittest.TestCase): def _get_server(*args, **kwargs): def server(app): return '' - return server def _getTargetClass(self): @@ -25,6 +27,7 @@ class TestPServeCommand(unittest.TestCase): effargs.extend(args) cmd = self._getTargetClass()(effargs) cmd.out = self.out + cmd.ConfigParser = self.config_factory return cmd def test_run_no_args(self): @@ -40,20 +43,15 @@ class TestPServeCommand(unittest.TestCase): self.assertEqual(result, {'a': '1', 'b': '2'}) def test_parse_vars_good(self): - from pyramid.tests.test_scripts.dummy import DummyApp - inst = self._makeOne('development.ini', 'a=1', 'b=2') inst.loadserver = self._get_server - - app = DummyApp() - + app = dummy.DummyApp() def get_app(*args, **kwargs): app.global_conf = kwargs.get('global_conf', None) - inst.loadapp = get_app - inst.run() + inst.run() self.assertEqual(app.global_conf, {'a': '1', 'b': '2'}) def test_parse_vars_bad(self): @@ -61,6 +59,23 @@ class TestPServeCommand(unittest.TestCase): inst.loadserver = self._get_server self.assertRaises(ValueError, inst.run) + def test_config_file_finds_watch_files(self): + inst = self._makeOne('development.ini') + self.config_factory.items = [( + 'watch_files', + 'foo\n/baz\npyramid.tests.test_scripts:*.py', + )] + inst.pserve_file_config('/base/path.ini', global_conf={'a': '1'}) + self.assertEqual(self.config_factory.defaults, { + 'a': '1', + 'here': os.path.abspath('/base'), + }) + self.assertEqual(inst.watch_files, [ + os.path.abspath('/base/foo'), + os.path.abspath('/baz'), + os.path.abspath(os.path.join(here, '*.py')), + ]) + class Test_main(unittest.TestCase): def _callFUT(self, argv): from pyramid.scripts.pserve import main @@ -69,71 +84,3 @@ class Test_main(unittest.TestCase): def test_it(self): result = self._callFUT(['pserve']) self.assertEqual(result, 2) - -class TestLazyWriter(unittest.TestCase): - def _makeOne(self, filename, mode='w'): - from pyramid.scripts.pserve import LazyWriter - return LazyWriter(filename, mode) - - def test_open(self): - filename = tempfile.mktemp() - try: - inst = self._makeOne(filename) - fp = inst.open() - self.assertEqual(fp.name, filename) - finally: - fp.close() - os.remove(filename) - - def test_write(self): - filename = tempfile.mktemp() - try: - inst = self._makeOne(filename) - inst.write('hello') - finally: - with open(filename) as f: - data = f.read() - self.assertEqual(data, 'hello') - inst.close() - os.remove(filename) - - def test_writeline(self): - filename = tempfile.mktemp() - try: - inst = self._makeOne(filename) - inst.writelines('hello') - finally: - with open(filename) as f: - data = f.read() - self.assertEqual(data, 'hello') - inst.close() - os.remove(filename) - - def test_flush(self): - filename = tempfile.mktemp() - try: - inst = self._makeOne(filename) - inst.flush() - fp = inst.fileobj - self.assertEqual(fp.name, filename) - finally: - fp.close() - os.remove(filename) - -class Test__methodwrapper(unittest.TestCase): - def _makeOne(self, func, obj, type): - from pyramid.scripts.pserve import _methodwrapper - return _methodwrapper(func, obj, type) - - def test___call__succeed(self): - def foo(self, cls, a=1): return 1 - class Bar(object): pass - wrapper = self._makeOne(foo, Bar, None) - result = wrapper(a=1) - self.assertEqual(result, 1) - - def test___call__fail(self): - def foo(self, cls, a=1): return 1 - class Bar(object): pass - wrapper = self._makeOne(foo, Bar, None) - self.assertRaises(AssertionError, wrapper, cls=1) diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 2ca86bc44..f76cc5067 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -113,14 +113,6 @@ class Test_static_view_use_subpath_False(unittest.TestCase): response = inst(context, request) self.assertTrue(b'<html>static</html>' in response.body) - def test_cachebust_match(self): - inst = self._makeOne('pyramid.tests:fixtures/static') - inst.cachebust_match = lambda subpath: subpath[1:] - request = self._makeRequest({'PATH_INFO':'/foo/index.html'}) - context = DummyContext() - response = inst(context, request) - self.assertTrue(b'<html>static</html>' in response.body) - def test_resource_is_file_with_wsgi_file_wrapper(self): from pyramid.response import _BLOCK_SIZE inst = self._makeOne('pyramid.tests:fixtures/static') @@ -186,14 +178,14 @@ class Test_static_view_use_subpath_False(unittest.TestCase): from pyramid.httpexceptions import HTTPNotFound self.assertRaises(HTTPNotFound, inst, context, request) - def test_resource_with_content_encoding(self): + def test_gz_resource_no_content_encoding(self): inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest({'PATH_INFO':'/arcs.svg.tgz'}) context = DummyContext() response = inst(context, request) self.assertEqual(response.status, '200 OK') self.assertEqual(response.content_type, 'application/x-tar') - self.assertEqual(response.content_encoding, 'gzip') + self.assertEqual(response.content_encoding, None) response.app_iter.close() def test_resource_no_content_encoding(self): diff --git a/pyramid/tests/test_traversal.py b/pyramid/tests/test_traversal.py index 0decd04d6..5fc878a32 100644 --- a/pyramid/tests/test_traversal.py +++ b/pyramid/tests/test_traversal.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import unittest import warnings @@ -839,7 +840,7 @@ class QuotePathSegmentTests(unittest.TestCase): def test_string(self): s = '/ hello!' result = self._callFUT(s) - self.assertEqual(result, '%2F%20hello%21') + self.assertEqual(result, '%2F%20hello!') def test_int(self): s = 12345 @@ -1299,6 +1300,15 @@ class Test__join_path_tuple(unittest.TestCase): result = self._callFUT(('x',)) self.assertEqual(result, 'x') + def test_segments_with_unsafes(self): + safe_segments = tuple(u"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~!$&'()*+,;=:@") + result = self._callFUT(safe_segments) + self.assertEqual(result, u'/'.join(safe_segments)) + unsafe_segments = tuple(chr(i) for i in range(0x20, 0x80) if not chr(i) in safe_segments) + (u'あ',) + result = self._callFUT(unsafe_segments) + self.assertEqual(result, u'/'.join(''.join('%%%02X' % (ord(c) if isinstance(c, str) else c) for c in unsafe_segment.encode('utf-8')) for unsafe_segment in unsafe_segments)) + + def make_traverser(result): class DummyTraverser(object): def __init__(self, context): diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index 2d20b24c3..06f4ad793 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -485,11 +485,15 @@ class TestCompileRouteFunctional(unittest.TestCase): def test_generator_functional_newstyle(self): self.generates('/{x}', {'x':''}, '/') self.generates('/{x}', {'x':'a'}, '/a') + self.generates('/{x}', {'x':'a/b/c'}, '/a/b/c') + self.generates('/{x}', {'x':':@&+$,'}, '/:@&+$,') self.generates('zzz/{x}', {'x':'abc'}, '/zzz/abc') self.generates('zzz/{x}*traverse', {'x':'abc', 'traverse':''}, '/zzz/abc') self.generates('zzz/{x}*traverse', {'x':'abc', 'traverse':'/def/g'}, '/zzz/abc/def/g') + self.generates('zzz/{x}*traverse', {'x':':@&+$,', 'traverse':'/:@&+$,'}, + '/zzz/:@&+$,/:@&+$,') self.generates('/{x}', {'x':text_(b'/La Pe\xc3\xb1a', 'utf-8')}, '//La%20Pe%C3%B1a') self.generates('/{x}*y', {'x':text_(b'/La Pe\xc3\xb1a', 'utf-8'), diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index 676c6f66a..51d0bd367 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -1291,6 +1291,34 @@ class TestDeriveView(unittest.TestCase): view = self.config._derive_view(inner_view) self.assertRaises(BadCSRFToken, lambda: view(None, request)) + def test_csrf_view_enabled_via_callback(self): + def callback(request): + return True + from pyramid.exceptions import BadCSRFToken + def inner_view(request): pass + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + self.config.set_default_csrf_options(require_csrf=True, callback=callback) + view = self.config._derive_view(inner_view) + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_csrf_view_disabled_via_callback(self): + def callback(request): + return False + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + self.config.set_default_csrf_options(require_csrf=True, callback=callback) + view = self.config._derive_view(inner_view) + result = view(None, request) + self.assertTrue(result is response) + def test_csrf_view_uses_custom_csrf_token(self): response = DummyResponse() def inner_view(request): diff --git a/pyramid/traversal.py b/pyramid/traversal.py index 963a76bb5..1ca52692a 100644 --- a/pyramid/traversal.py +++ b/pyramid/traversal.py @@ -35,6 +35,9 @@ with warnings.catch_warnings(): warnings.filterwarnings('ignore') from pyramid.interfaces import IContextURL +PATH_SEGMENT_SAFE = "~!$&'()*+,;=:@" # from webob +PATH_SAFE = PATH_SEGMENT_SAFE + "/" + empty = text_('') def find_root(resource): @@ -577,7 +580,7 @@ the ``safe`` argument to this function. This corresponds to the if PY2: # special-case on Python 2 for speed? unchecked - def quote_path_segment(segment, safe=''): + def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE): """ %s """ % quote_path_segment_doc # The bit of this code that deals with ``_segment_cache`` is an # optimization: we cache all the computation of URL path segments @@ -596,7 +599,7 @@ if PY2: _segment_cache[(segment, safe)] = result return result else: - def quote_path_segment(segment, safe=''): + def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE): """ %s """ % quote_path_segment_doc # The bit of this code that deals with ``_segment_cache`` is an # optimization: we cache all the computation of URL path segments diff --git a/pyramid/url.py b/pyramid/url.py index fd62f0057..d6587e783 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -25,10 +25,11 @@ from pyramid.threadlocal import get_current_registry from pyramid.traversal import ( ResourceURL, quote_path_segment, + PATH_SAFE, + PATH_SEGMENT_SAFE, ) -PATH_SAFE = '/:@&+$,' # from webob -QUERY_SAFE = '/?:@!$&\'()*+,;=' # RFC 3986 +QUERY_SAFE = "/?:@!$&'()*+,;=" # RFC 3986 ANCHOR_SAFE = QUERY_SAFE def parse_url_overrides(kw): @@ -364,7 +365,7 @@ class URLMethodsMixin(object): of ``query`` may be a sequence of two-tuples *or* a data structure with an ``.items()`` method that returns a sequence of two-tuples (presumably a dictionary). This data structure will be turned into a - query string per the documentation of :func:``pyramid.url.urlencode`` + query string per the documentation of :func:`pyramid.url.urlencode` function. This will produce a query string in the ``x-www-form-urlencoded`` encoding. A non-``x-www-form-urlencoded`` query string may be used by passing a *string* value as ``query`` in @@ -454,7 +455,7 @@ class URLMethodsMixin(object): ``resource_url(someresource, 'element1', 'element2', query={'a':1}, route_name='blogentry')`` is roughly equivalent to doing:: - remainder_path = request.resource_path(someobject) + traversal_path = request.resource_path(someobject) url = request.route_url( 'blogentry', 'element1', @@ -486,7 +487,7 @@ class URLMethodsMixin(object): 'element2', route_name='blogentry', route_kw={'id':'4'}, _query={'a':'1'})`` is roughly equivalent to:: - remainder_path = request.resource_path_tuple(someobject) + traversal_path = request.resource_path_tuple(someobject) kw = {'id':'4', '_query':{'a':'1'}, 'traverse':traversal_path} url = request.route_url( 'blogentry', @@ -947,4 +948,4 @@ def current_route_path(request, *elements, **kw): @lru_cache(1000) def _join_elements(elements): - return '/'.join([quote_path_segment(s, safe=':@&+$,') for s in elements]) + return '/'.join([quote_path_segment(s, safe=PATH_SEGMENT_SAFE) for s in elements]) diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index c88ad9590..a61071845 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -22,6 +22,7 @@ from pyramid.exceptions import URLDecodeError from pyramid.traversal import ( quote_path_segment, split_path_info, + PATH_SAFE, ) _marker = object() @@ -207,6 +208,10 @@ def _compile_route(route): return d gen = ''.join(gen) + + def q(v): + return quote_path_segment(v, safe=PATH_SAFE) + def generator(dict): newdict = {} for k, v in dict.items(): @@ -223,17 +228,17 @@ def _compile_route(route): # a stararg argument if is_nonstr_iter(v): v = '/'.join( - [quote_path_segment(x, safe='/') for x in v] + [q(x) for x in v] ) # native else: if v.__class__ not in string_types: v = str(v) - v = quote_path_segment(v, safe='/') + v = q(v) else: if v.__class__ not in string_types: v = str(v) # v may be bytes (py2) or native string (py3) - v = quote_path_segment(v, safe='/') + v = q(v) # at this point, the value will be a native string newdict[k] = v diff --git a/pyramid/util.py b/pyramid/util.py index 4936dcb24..3337d410d 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -3,7 +3,7 @@ import functools try: # py2.7.7+ and py3.3+ have native comparison support from hmac import compare_digest -except ImportError: # pragma: nocover +except ImportError: # pragma: no cover compare_digest = None import inspect import traceback @@ -28,13 +28,24 @@ from pyramid.compat import ( from pyramid.interfaces import IActionInfo from pyramid.path import DottedNameResolver as _DottedNameResolver +_marker = object() + class DottedNameResolver(_DottedNameResolver): def __init__(self, package=None): # default to package = None for bw compat _DottedNameResolver.__init__(self, package) -_marker = object() - +def is_string_or_iterable(v): + if isinstance(v, string_types): + return True + if hasattr(v, '__iter__'): + return True + +def as_sorted_tuple(val): + if not is_nonstr_iter(val): + val = (val,) + val = tuple(sorted(val)) + return val class InstancePropertyHelper(object): """A helper object for assigning properties and descriptors to instances. diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 513ddf022..4eb0ce704 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -481,11 +481,13 @@ def csrf_view(view, info): token = 'csrf_token' header = 'X-CSRF-Token' safe_methods = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"]) + callback = None else: default_val = defaults.require_csrf token = defaults.token header = defaults.header safe_methods = defaults.safe_methods + callback = defaults.callback enabled = ( explicit_val is True or @@ -501,7 +503,10 @@ def csrf_view(view, info): wrapped_view = view if enabled: def csrf_view(context, request): - if request.method not in safe_methods: + if ( + request.method not in safe_methods and + (callback is None or callback(request)) + ): check_csrf_origin(request, raises=True) check_csrf_token(request, token, header, raises=True) return view(context, request) diff --git a/scaffoldtests.sh b/scaffoldtests.sh index 1de398991..317bff8b5 100755 --- a/scaffoldtests.sh +++ b/scaffoldtests.sh @@ -1,3 +1,2 @@ #!/bin/bash tox -e{py27,py34,py35,pypy}-scaffolds, - @@ -14,6 +14,52 @@ docs = develop easy_install pyramid[docs] universal = 1 [flake8] -ignore = E301,E302,E731,E261,E123,E121,E128,E129,E125,W291,E501,W293,E303,W391,E266,E231,E201,E202,E127,E262,E265 +ignore = + # E121: continuation line under-indented for hanging indent + E121, + # E123: closing bracket does not match indentation of opening bracket's line + E123, + # E125: continuation line with same indent as next logical line + E125, + # E127: continuation line over-indented for visual indent + E127, + # E128: continuation line under-indented for visual indent + E128, + # E129: visually indented line with same indent as next logical line + E129, + # E201: whitespace after '(' + E201, + # E202: whitespace before ')' + E202, + # E231: missing whitespace after ',', ';', or ':' + E231, + # E261: at least two spaces before inline comment + E261, + # E262: inline comment should start with '# ' + E262, + # E265: block comment should start with '# ' + E265, + # E266: too many leading '#' for block comment + E266, + # E301: expected 1 blank line, found 0 + E301, + # E302: expected 2 blank lines, found 0 + E302, + # E303: too many blank lines (3) + E303, + # E305: expected 2 blank lines after class or function definition, found 1 + E305, + # E306: expected 1 blank line before a nested definition, found 0 + E306, + # E501: line too long (82 > 79 characters) + E501, + # E731: do not assign a lambda expression, use a def + E731, + # W291: trailing whitespace + W291, + # W293: blank line contains whitespace + W293, + # W391: blank line at end of file + W391 exclude = pyramid/tests/,pyramid/compat.py,pyramid/resource.py show-source = True @@ -14,19 +14,21 @@ import os import sys +import warnings from setuptools import setup, find_packages py_version = sys.version_info[:2] -PY3 = py_version[0] == 3 +PY2 = py_version[0] == 2 -if PY3: - if py_version < (3, 4): - raise RuntimeError('On Python 3, Pyramid requires Python 3.4 or better') -else: - if py_version < (2, 7): - raise RuntimeError('On Python 2, Pyramid requires Python 2.7 or better') +if (3, 0) <= py_version < (3, 4): + warnings.warn( + 'On Python 3, Pyramid only supports Python 3.4 or better', + UserWarning, + ) +elif py_version < (2, 7): + raise RuntimeError('On Python 2, Pyramid requires Python 2.7 or better') here = os.path.abspath(os.path.dirname(__file__)) try: @@ -39,20 +41,21 @@ except IOError: install_requires = [ 'setuptools', - 'WebOb >= 1.3.1', # request.domain and CookieProfile + 'WebOb >= 1.7.0rc2', # Response.has_body 'repoze.lru >= 0.4', # py3 compat 'zope.interface >= 3.8.0', # has zope.interface.registry 'zope.deprecation >= 3.5.0', # py3 compat 'venusian >= 1.0a3', # ``ignore`` 'translationstring >= 0.4', # py3 compat 'PasteDeploy >= 1.5.0', # py3 compat + 'hupper', ] tests_require = [ 'WebTest >= 1.3.1', # py3 compat ] -if not PY3: +if PY2: tests_require.append('zope.component>=3.11.0') docs_extras = [ @@ -82,6 +85,7 @@ setup(name='pyramid', "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: Pyramid", @@ -1,9 +1,9 @@ [tox] envlist = - py27,py34,py35,py36,pypy, + py27,py34,py35,py36,py37,pypy, docs,pep8, {py2,py3}-cover,coverage, -skip-missing-interpreters = True +skip_missing_interpreters = True [testenv] # Most of these are defaults but if you specify any you can't fall back @@ -13,12 +13,13 @@ basepython = py34: python3.4 py35: python3.5 py36: python3.6 + py37: python3.7 pypy: pypy py2: python2.7 py3: python3.5 commands = - pip install pyramid[testing] + pip install -q pyramid[testing] nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:} [testenv:py27-scaffolds] @@ -71,7 +72,7 @@ commands = # combination of versions of coverage and nosexcover that i can find. [testenv:py2-cover] commands = - pip install pyramid[testing] + pip install -q pyramid[testing] coverage run --source=pyramid {envbindir}/nosetests coverage xml -o coverage-py2.xml setenv = @@ -79,7 +80,7 @@ setenv = [testenv:py3-cover] commands = - pip install pyramid[testing] + pip install -q pyramid[testing] coverage run --source=pyramid {envbindir}/nosetests coverage xml -o coverage-py3.xml setenv = |
