diff options
41 files changed, 2027 insertions, 193 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 65df2c7c5..8c50e4d63 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -122,6 +122,16 @@ Features 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 --------- @@ -152,11 +162,18 @@ 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 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 90da9e074..d5c178418 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -285,6 +285,8 @@ Contributors - Keith Yang, 2016/07/22 +- Hannah Krager, 2016/10/22 + - Moriyoshi Koizumi, 2016/11/20 - Mikko Ohtamaa, 2016/12/6 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 84fd5cf1b..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: 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/project.rst b/docs/narr/project.rst index 9b0d54ec7..62c4723df 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -1067,3 +1067,25 @@ 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/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/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/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/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/scaffolds/starter/+package+/__init__.py b/pyramid/scaffolds/starter/+package+/__init__.py index ad5ecbc6f..49dde36d4 100644 --- a/pyramid/scaffolds/starter/+package+/__init__.py +++ b/pyramid/scaffolds/starter/+package+/__init__.py @@ -5,7 +5,7 @@ def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ config = Configurator(settings=settings) - config.include('pyramid_chameleon') + config.include('pyramid_jinja2') config.add_static_view('static', 'static', cache_max_age=3600) config.add_route('home', '/') config.scan() diff --git a/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt_tmpl b/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl index 87fae3817..6f0cfd2fa 100644 --- a/pyramid/scaffolds/starter/+package+/templates/mytemplate.pt_tmpl +++ b/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl @@ -1,12 +1,12 @@ <!DOCTYPE html> -<html lang="${request.locale_name}"> +<html lang="\{\{request.locale_name\}\}"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="pyramid web application"> <meta name="author" content="Pylons Project"> - <link rel="shortcut icon" href="${request.static_url('{{package}}:static/pyramid-16x16.png')}"> + <link rel="shortcut icon" href="\{\{request.static_url('{{package}}:static/pyramid-16x16.png')\}\}"> <title>Starter Scaffold for The Pyramid Web Framework</title> @@ -14,7 +14,7 @@ <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet"> <!-- Custom styles for this scaffold --> - <link href="${request.static_url('{{package}}:static/theme.css')}" rel="stylesheet"> + <link href="\{\{request.static_url('{{package}}:static/theme.css')\}\}" rel="stylesheet"> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> @@ -29,13 +29,12 @@ <div class="container"> <div class="row"> <div class="col-md-2"> - <img class="logo img-responsive" src="${request.static_url('{{package}}:static/pyramid.png')}" alt="pyramid web framework"> + <img class="logo img-responsive" src="\{\{request.static_url('{{package}}:static/pyramid.png')\}\}" alt="pyramid web framework"> </div> <div class="col-md-10"> - <div class="content"> - <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter scaffold</span></h1> - <p class="lead">Welcome to <span class="font-normal">${project}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework {{pyramid_version}}</span>.</p> - </div> + {% block content %} + <p>No content</p> + {% endblock content %} </div> </div> <div class="row"> diff --git a/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl b/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl new file mode 100644 index 000000000..f826ff9e7 --- /dev/null +++ b/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content%} +<div class="content"> + <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter scaffold</span></h1> + <p class="lead">Welcome to <span class="font-normal">\{\{project\}\}</span>, an application generated by<br>the <span class="font-normal">Pyramid Web Framework {{pyramid_version}}</span>.</p> +</div> +{% endblock content %} diff --git a/pyramid/scaffolds/starter/+package+/views.py_tmpl b/pyramid/scaffolds/starter/+package+/views.py_tmpl index ad9af7292..01b9d0130 100644 --- a/pyramid/scaffolds/starter/+package+/views.py_tmpl +++ b/pyramid/scaffolds/starter/+package+/views.py_tmpl @@ -1,6 +1,6 @@ from pyramid.view import view_config -@view_config(route_name='home', renderer='templates/mytemplate.pt') +@view_config(route_name='home', renderer='templates/mytemplate.jinja2') def my_view(request): return {'project': '{{project}}'} diff --git a/pyramid/scaffolds/starter/MANIFEST.in_tmpl b/pyramid/scaffolds/starter/MANIFEST.in_tmpl index 0ff6eb7a0..4d1c86b44 100644 --- a/pyramid/scaffolds/starter/MANIFEST.in_tmpl +++ b/pyramid/scaffolds/starter/MANIFEST.in_tmpl @@ -1,2 +1,2 @@ include *.txt *.ini *.cfg *.rst -recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml +recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 diff --git a/pyramid/scaffolds/starter/setup.py_tmpl b/pyramid/scaffolds/starter/setup.py_tmpl index 2e5ce92c7..7f50bbbc2 100644 --- a/pyramid/scaffolds/starter/setup.py_tmpl +++ b/pyramid/scaffolds/starter/setup.py_tmpl @@ -10,7 +10,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: requires = [ 'pyramid', - 'pyramid_chameleon', + 'pyramid_jinja2', 'pyramid_debugtoolbar', 'waitress', ] diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index a954d3be6..b3e3b65fa 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -22,7 +22,13 @@ def main(argv=sys.argv, quiet=False): class PCreateCommand(object): verbosity = 1 # required - description = "Render Pyramid scaffolding to an output directory" + description = """\ +Render Pyramid scaffolding to an output directory. + +Note: As of Pyramid 1.8, this command is deprecated. Use a specific +cookiecutter instead: +https://github.com/Pylons/?q=cookiecutter +""" usage = "usage: %prog [options] -s <scaffold> output_directory" parser = optparse.OptionParser(usage, description=description) parser.add_option('-s', '--scaffold', @@ -85,6 +91,7 @@ class PCreateCommand(object): self.scaffolds = self.all_scaffolds() def run(self): + self._warn_pcreate_deprecated() if self.options.list: return self.show_scaffolds() if not self.options.scaffold_name and not self.args: @@ -224,5 +231,12 @@ class PCreateCommand(object): answer = input_('{0} [y|N]: '.format(prompt)) return answer.strip().lower() == 'y' + def _warn_pcreate_deprecated(self): + self.out('''\ +Note: As of Pyramid 1.8, this command is deprecated. Use a specific +cookiecutter instead: +https://github.com/pylons/?query=cookiecutter +''') + if __name__ == '__main__': # pragma: no cover sys.exit(main() or 0) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 969bc07f1..b8776d44f 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -28,9 +28,12 @@ from paste.deploy.loadwsgi import ( ) from pyramid.compat import PY2 +from pyramid.compat import configparser from pyramid.scripts.common import parse_vars 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) @@ -97,12 +100,17 @@ class PServeCommand(object): dest='verbose', help="Suppress verbose output") + ConfigParser = configparser.ConfigParser # testing + loadapp = staticmethod(loadapp) # testing + loadserver = staticmethod(loadserver) # testing + _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: @@ -112,6 +120,32 @@ 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') @@ -121,8 +155,12 @@ class PServeCommand(object): 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' @@ -130,7 +168,6 @@ class PServeCommand(object): server_name = self.options.server else: server_spec = app_spec - base = os.getcwd() # do not open the browser on each reload so check hupper first if self.options.browser and not hupper.is_active(): @@ -155,22 +192,17 @@ class PServeCommand(object): ) return 0 + 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) + if hupper.is_active(): reloader = hupper.get_reloader() - if app_spec.startswith('config:'): - reloader.watch_files([app_spec[len('config:'):]]) - - log_fn = app_spec - if log_fn.startswith('config:'): - log_fn = app_spec[len('config:'):] - 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) + reloader.watch_files(self.watch_files) - server = self.loadserver(server_spec, name=server_name, - relative_to=base, 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) @@ -182,26 +214,16 @@ class PServeCommand(object): 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) - - 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) + 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) # For paste.deploy server instantiation (egg:pyramid#wsgiref) def wsgiref_server_runner(wsgi_app, global_conf, **kw): # pragma: no cover 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_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_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index b7013bc73..0286614ce 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -26,7 +26,7 @@ class TestPCreateCommand(unittest.TestCase): result = cmd.run() self.assertEqual(result, 0) out = self.out_.getvalue() - self.assertTrue(out.startswith('Available scaffolds')) + self.assertTrue(out.count('Available scaffolds')) def test_run_show_scaffolds_none_exist(self): cmd = self._makeOne('-l') @@ -34,7 +34,7 @@ class TestPCreateCommand(unittest.TestCase): result = cmd.run() self.assertEqual(result, 0) out = self.out_.getvalue() - self.assertTrue(out.startswith('No scaffolds available')) + self.assertTrue(out.count('No scaffolds available')) def test_run_no_scaffold_no_args(self): cmd = self._makeOne(quiet=True) @@ -46,7 +46,7 @@ class TestPCreateCommand(unittest.TestCase): result = cmd.run() self.assertEqual(result, 2) out = self.out_.getvalue() - self.assertTrue(out.startswith( + self.assertTrue(out.count( 'You must provide at least one scaffold name')) def test_no_project_name(self): @@ -54,14 +54,14 @@ class TestPCreateCommand(unittest.TestCase): result = cmd.run() self.assertEqual(result, 2) out = self.out_.getvalue() - self.assertTrue(out.startswith('You must provide a project name')) + self.assertTrue(out.count('You must provide a project name')) def test_unknown_scaffold_name(self): cmd = self._makeOne('-s', 'dummyXX', 'distro') result = cmd.run() self.assertEqual(result, 2) out = self.out_.getvalue() - self.assertTrue(out.startswith('Unavailable scaffolds')) + self.assertTrue(out.count('Unavailable scaffolds')) def test_known_scaffold_single_rendered(self): import os diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py index e84de92d4..18f7c8c2f 100644 --- a/pyramid/tests/test_scripts/test_pserve.py +++ b/pyramid/tests/test_scripts/test_pserve.py @@ -1,9 +1,14 @@ +import os 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) @@ -11,7 +16,6 @@ class TestPServeCommand(unittest.TestCase): def _get_server(*args, **kwargs): def server(app): return '' - return server def _getTargetClass(self): @@ -23,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): @@ -38,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): @@ -59,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 diff --git a/scaffoldtests.sh b/scaffoldtests.sh index 69d8ad561..317bff8b5 100755 --- a/scaffoldtests.sh +++ b/scaffoldtests.sh @@ -1,3 +1,2 @@ #!/bin/bash -tox -e{py27,py33,py34,pypy}-scaffolds - +tox -e{py27,py34,py35,pypy}-scaffolds, @@ -41,7 +41,7 @@ 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 |
