diff options
| author | Michael Merickel <michael@merickel.org> | 2020-01-10 00:50:03 -0600 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2020-01-10 01:07:04 -0600 |
| commit | 095eb560dc17dc591d43144758adaf2e4c780e72 (patch) | |
| tree | 400867fe71d121e65e40469231616d1386d6200d /docs/tutorials | |
| parent | 7adc44fa2b4bfa5b4230d8646e734ba262ec1ce2 (diff) | |
| download | pyramid-095eb560dc17dc591d43144758adaf2e4c780e72.tar.gz pyramid-095eb560dc17dc591d43144758adaf2e4c780e72.tar.bz2 pyramid-095eb560dc17dc591d43144758adaf2e4c780e72.zip | |
sync wiki installation, basiclayout, models and views chapters with new cookiecutter
Diffstat (limited to 'docs/tutorials')
35 files changed, 669 insertions, 147 deletions
diff --git a/docs/tutorials/wiki/basiclayout.rst b/docs/tutorials/wiki/basiclayout.rst index 4eb5c4283..c1c762ae4 100644 --- a/docs/tutorials/wiki/basiclayout.rst +++ b/docs/tutorials/wiki/basiclayout.rst @@ -57,7 +57,7 @@ Next in ``main``, construct a :term:`Configurator` object using a context manage See also :term:`Deployment settings`. .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 14 + :lines: 15 :lineno-match: :language: py @@ -65,35 +65,28 @@ See also :term:`Deployment settings`. This will be a dictionary of settings parsed from the ``.ini`` file, which contains deployment-related values, such as ``pyramid.reload_templates``, ``zodbconn.uri``, and so on. -Next include support for ``pyramid_tm``, allowing Pyramid requests to join the active transaction as provided by the `transaction <https://pypi.org/project/transaction/>`_ package. - -.. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 15 - :lineno-match: - :language: py - -Next include support for ``pyramid_retry`` to retry a request when transient exceptions occur. +Next include support for the :term:`Chameleon` template rendering bindings, allowing us to use the ``.pt`` templates. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 16 :lineno-match: :language: py -Next include support for ``pyramid_zodbconn``, providing integration between :term:`ZODB` and a Pyramid application. +Next include support for ``pyramid_tm``, allowing Pyramid requests to join the active transaction as provided by the `transaction <https://pypi.org/project/transaction/>`_ package. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 17 :lineno-match: :language: py -Next set a root factory using our function named ``root_factory``. +Next include support for ``pyramid_retry`` to retry a request when transient exceptions occur. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 18 :lineno-match: :language: py -Next include support for the :term:`Chameleon` template rendering bindings, allowing us to use the ``.pt`` templates. +Next include support for ``pyramid_zodbconn``, providing integration between :term:`ZODB` and a Pyramid application. .. literalinclude:: src/basiclayout/tutorial/__init__.py :lines: 19 @@ -107,6 +100,13 @@ Next include routes from the ``.routes`` module. :lineno-match: :language: py +Next set a root factory using our function named ``root_factory``. + +.. literalinclude:: src/basiclayout/tutorial/__init__.py + :lines: 21 + :lineno-match: + :language: py + The included module contains the following function. .. literalinclude:: src/basiclayout/tutorial/routes.py @@ -130,7 +130,7 @@ The third argument is an optional ``cache_max_age`` which specifies the number o Back into our ``__init__.py``, next perform a :term:`scan`. .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 21 + :lines: 22 :lineno-match: :language: py @@ -142,7 +142,7 @@ The cookiecutter could have equivalently said ``config.scan('tutorial')``, but i Finally use the :meth:`pyramid.config.Configurator.make_wsgi_app` method to return a :term:`WSGI` application. .. literalinclude:: src/basiclayout/tutorial/__init__.py - :lines: 22 + :lines: 23 :lineno-match: :language: py @@ -262,3 +262,10 @@ The ``development.ini`` (in the ``tutorial`` :term:`project` directory, as oppos Note the existence of a ``[app:main]`` section which specifies our WSGI application. Our ZODB database settings are specified as the ``zodbconn.uri`` setting within this section. When the server is started via ``pserve``, the values within this section are passed as ``**settings`` to the ``main`` function defined in ``__init__.py``. + + +Tests +----- + +The project contains a basic structure for a test suite using ``pytest``. +The structure is covered later in :ref:`wiki_adding_tests`. diff --git a/docs/tutorials/wiki/definingviews.rst b/docs/tutorials/wiki/definingviews.rst index 2e4d009a1..5aafd68d6 100644 --- a/docs/tutorials/wiki/definingviews.rst +++ b/docs/tutorials/wiki/definingviews.rst @@ -39,8 +39,9 @@ We need to add a dependency on the ``docutils`` package to our ``tutorial`` pack Open ``setup.py`` and edit it to look like the following: .. literalinclude:: src/views/setup.py - :linenos: - :emphasize-lines: 22 + :lines: 11-29 + :lineno-match: + :emphasize-lines: 2 :language: python Only the highlighted line needs to be added. @@ -91,7 +92,7 @@ We added some imports and created a regular expression to find "WikiWords". We got rid of the ``my_view`` view function and its decorator that was added when originally rendered after we selected the ``zodb`` backend option in the cookiecutter. It was only an example and is not relevant to our application. -Then we added four :term:`view callable` functions to our ``views.py`` module: +Then we added four :term:`view callable` functions to our ``default.py`` module: * ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL. * ``view_page()`` - Displays an individual page. @@ -102,7 +103,7 @@ We will describe each one briefly in the following sections. .. note:: - There is nothing special about the filename ``views.py``. + There is nothing special about the filename ``default.py``. A project may have many view callables throughout its codebase in arbitrarily named files. Files that implement view callables often have ``view`` in their names (or may live in a Python subpackage of your application package named ``views``), but this is only by convention. @@ -113,7 +114,7 @@ The ``view_wiki`` view function Following is the code for the ``view_wiki`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 13-15 + :lines: 12-14 :lineno-match: :language: python @@ -133,9 +134,9 @@ The view configuration associated with ``view_wiki`` does not use a ``renderer`` No renderer is necessary when a view returns a response object. The ``view_wiki`` view callable always redirects to the URL of a ``Page`` resource named ``FrontPage``. -To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPFound` class. +To do so, it returns an instance of the :class:`pyramid.httpexceptions.HTTPSeeOther` class. Instances of this class implement the :class:`pyramid.interfaces.IResponse` interface, similar to :class:`pyramid.response.Response`. -It uses the :meth:`pyramid.request.Request.route_url` API to construct an URL to the ``FrontPage`` page resource (in other words, ``http://localhost:6543/FrontPage``), and uses it as the ``location`` of the ``HTTPFound`` response, forming an HTTP redirect. +It uses the :meth:`pyramid.request.Request.route_url` API to construct an URL to the ``FrontPage`` page resource (in other words, ``http://localhost:6543/FrontPage``), and uses it as the ``location`` of the ``HTTPSeeOther`` response, forming an HTTP redirect. The ``view_page`` view function @@ -144,7 +145,7 @@ The ``view_page`` view function Here is the code for the ``view_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 18-35 + :lines: 17-34 :lineno-match: :language: python @@ -183,7 +184,7 @@ The ``add_page`` view function Here is the code for the ``add_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 38-53 + :lines: 37-52 :lineno-match: :language: python @@ -231,7 +232,7 @@ The ``edit_page`` view function Here is the code for the ``edit_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py - :lines: 56-64 + :lines: 55- :lineno-match: :language: python @@ -260,7 +261,7 @@ Open ``tutorial/views/notfound.py`` and make the changes shown by the emphasized .. literalinclude:: src/views/tutorial/views/notfound.py :linenos: :language: python - :emphasize-lines: 3-4, 9-12 + :emphasize-lines: 3, 9-12 We need to import the ``Page`` from our models. We eventually return a ``Page`` object as ``page`` into the template ``layout.pt`` to display its name in the title tag. @@ -282,7 +283,7 @@ Update ``tutorial/templates/layout.pt`` with the following content, as indicated .. literalinclude:: src/views/tutorial/templates/layout.pt :linenos: - :emphasize-lines: 11-12, 37-41 + :emphasize-lines: 11, 36-40 :language: html Since we are using a templating engine, we can factor common boilerplate out of our page templates into reusable components. @@ -291,11 +292,10 @@ We can do this via :term:`METAL` macros and slots. - The cookiecutter defined a macro named ``layout`` (line 1). This macro consists of the entire template. - We changed the ``title`` tag to use the ``name`` attribute of a ``page`` object, or if it does not exist then the page title (lines 11-12). -- The cookiecutter defined a macro customization point or `slot` (line 36). +- The cookiecutter defined a macro customization point or `slot` (line 35). This slot is inside the macro ``layout``. Therefore it can be replaced by content, customizing the macro. -- We added a ``div`` element with a link to allow the user to return to the front page (lines 37-41). -- We removed the row of icons and links from the original cookiecutter. +- We added a ``div`` element with a link to allow the user to return to the front page (lines 36-40). .. seealso:: diff --git a/docs/tutorials/wiki/installation.rst b/docs/tutorials/wiki/installation.rst index 6088f577d..392441eae 100644 --- a/docs/tutorials/wiki/installation.rst +++ b/docs/tutorials/wiki/installation.rst @@ -243,8 +243,8 @@ For a successful test run, you should see output that ends like this: .. code-block:: bash - .. - 2 passed in 0.49 seconds + .... + 4 passed in 0.49 seconds Expose test coverage information diff --git a/docs/tutorials/wiki/src/basiclayout/.gitignore b/docs/tutorials/wiki/src/basiclayout/.gitignore index 1853d983c..c612e59f2 100644 --- a/docs/tutorials/wiki/src/basiclayout/.gitignore +++ b/docs/tutorials/wiki/src/basiclayout/.gitignore @@ -19,3 +19,4 @@ Data.fs* .DS_Store coverage test +*.sqlite diff --git a/docs/tutorials/wiki/src/basiclayout/testing.ini b/docs/tutorials/wiki/src/basiclayout/testing.ini new file mode 100644 index 000000000..9298354ac --- /dev/null +++ b/docs/tutorials/wiki/src/basiclayout/testing.ini @@ -0,0 +1,60 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +zodbconn.uri = file://%(here)s/Data.testing.fs?connection_cache_size=20000 + +retry.attempts = 3 + +[pshell] +setup = tutorial.pshell.setup + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/basiclayout/tests/conftest.py b/docs/tutorials/wiki/src/basiclayout/tests/conftest.py new file mode 100644 index 000000000..12e75d8e9 --- /dev/null +++ b/docs/tutorials/wiki/src/basiclayout/tests/conftest.py @@ -0,0 +1,69 @@ +import os +from pyramid.paster import get_appsettings +from pyramid.scripting import prepare +from pyramid.testing import DummyRequest +import pytest +import webtest + +from tutorial import main + + +def pytest_addoption(parser): + parser.addoption('--ini', action='store', metavar='INI_FILE') + +@pytest.fixture(scope='session') +def ini_file(request): + # potentially grab this path from a pytest option + return os.path.abspath(request.config.option.ini or 'testing.ini') + +@pytest.fixture(scope='session') +def app_settings(ini_file): + return get_appsettings(ini_file) + +@pytest.fixture(scope='session') +def app(app_settings): + return main({}, **app_settings) + +@pytest.fixture +def testapp(app): + testapp = webtest.TestApp(app, extra_environ={ + 'HTTP_HOST': 'example.com', + }) + + return testapp + +@pytest.fixture +def app_request(app): + """ + A real request. + + This request is almost identical to a real request but it has some + drawbacks in tests as it's harder to mock data and is heavier. + + """ + env = prepare(registry=app.registry) + request = env['request'] + request.host = 'example.com' + + yield request + env['closer']() + +@pytest.fixture +def dummy_request(app): + """ + A lightweight dummy request. + + This request is ultra-lightweight and should be used only when the + request itself is not a large focus in the call-stack. + + It is way easier to mock and control side-effects using this object. + + - It does not have request extensions applied. + - Threadlocals are not properly pushed. + + """ + request = DummyRequest() + request.registry = app.registry + request.host = 'example.com' + + return request diff --git a/docs/tutorials/wiki/src/basiclayout/tests/test_functional.py b/docs/tutorials/wiki/src/basiclayout/tests/test_functional.py new file mode 100644 index 000000000..bac5d63f4 --- /dev/null +++ b/docs/tutorials/wiki/src/basiclayout/tests/test_functional.py @@ -0,0 +1,7 @@ +def test_root(testapp): + res = testapp.get('/', status=200) + assert b'Pyramid' in res.body + +def test_notfound(testapp): + res = testapp.get('/badurl', status=404) + assert res.status_code == 404 diff --git a/docs/tutorials/wiki/src/basiclayout/tests/test_it.py b/docs/tutorials/wiki/src/basiclayout/tests/test_it.py deleted file mode 100644 index 6c72bcc62..000000000 --- a/docs/tutorials/wiki/src/basiclayout/tests/test_it.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from pyramid import testing - - -class ViewTests(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - testing.tearDown() - - def test_my_view(self): - from tutorial.views.default import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info['project'], 'myproj') - - def test_notfound_view(self): - from tutorial.views.notfound import notfound_view - request = testing.DummyRequest() - info = notfound_view(request) - self.assertEqual(info, {}) - diff --git a/docs/tutorials/wiki/src/basiclayout/tests/test_views.py b/docs/tutorials/wiki/src/basiclayout/tests/test_views.py new file mode 100644 index 000000000..2b4201955 --- /dev/null +++ b/docs/tutorials/wiki/src/basiclayout/tests/test_views.py @@ -0,0 +1,13 @@ +from tutorial.views.default import my_view +from tutorial.views.notfound import notfound_view + + +def test_my_view(app_request): + info = my_view(app_request) + assert app_request.response.status_int == 200 + assert info['project'] == 'myproj' + +def test_notfound_view(app_request): + info = notfound_view(app_request) + assert app_request.response.status_int == 404 + assert info == {} diff --git a/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py index 830a607f3..e40451339 100644 --- a/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/basiclayout/tutorial/__init__.py @@ -1,5 +1,6 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection + from .models import appmaker @@ -12,11 +13,11 @@ def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ with Configurator(settings=settings) as config: + config.include('pyramid_chameleon') config.include('pyramid_tm') config.include('pyramid_retry') config.include('pyramid_zodbconn') - config.set_root_factory(root_factory) - config.include('pyramid_chameleon') config.include('.routes') + config.set_root_factory(root_factory) config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/installation/.gitignore b/docs/tutorials/wiki/src/installation/.gitignore index 1853d983c..c612e59f2 100644 --- a/docs/tutorials/wiki/src/installation/.gitignore +++ b/docs/tutorials/wiki/src/installation/.gitignore @@ -19,3 +19,4 @@ Data.fs* .DS_Store coverage test +*.sqlite diff --git a/docs/tutorials/wiki/src/installation/testing.ini b/docs/tutorials/wiki/src/installation/testing.ini new file mode 100644 index 000000000..9298354ac --- /dev/null +++ b/docs/tutorials/wiki/src/installation/testing.ini @@ -0,0 +1,60 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +zodbconn.uri = file://%(here)s/Data.testing.fs?connection_cache_size=20000 + +retry.attempts = 3 + +[pshell] +setup = tutorial.pshell.setup + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/installation/tests/conftest.py b/docs/tutorials/wiki/src/installation/tests/conftest.py new file mode 100644 index 000000000..12e75d8e9 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tests/conftest.py @@ -0,0 +1,69 @@ +import os +from pyramid.paster import get_appsettings +from pyramid.scripting import prepare +from pyramid.testing import DummyRequest +import pytest +import webtest + +from tutorial import main + + +def pytest_addoption(parser): + parser.addoption('--ini', action='store', metavar='INI_FILE') + +@pytest.fixture(scope='session') +def ini_file(request): + # potentially grab this path from a pytest option + return os.path.abspath(request.config.option.ini or 'testing.ini') + +@pytest.fixture(scope='session') +def app_settings(ini_file): + return get_appsettings(ini_file) + +@pytest.fixture(scope='session') +def app(app_settings): + return main({}, **app_settings) + +@pytest.fixture +def testapp(app): + testapp = webtest.TestApp(app, extra_environ={ + 'HTTP_HOST': 'example.com', + }) + + return testapp + +@pytest.fixture +def app_request(app): + """ + A real request. + + This request is almost identical to a real request but it has some + drawbacks in tests as it's harder to mock data and is heavier. + + """ + env = prepare(registry=app.registry) + request = env['request'] + request.host = 'example.com' + + yield request + env['closer']() + +@pytest.fixture +def dummy_request(app): + """ + A lightweight dummy request. + + This request is ultra-lightweight and should be used only when the + request itself is not a large focus in the call-stack. + + It is way easier to mock and control side-effects using this object. + + - It does not have request extensions applied. + - Threadlocals are not properly pushed. + + """ + request = DummyRequest() + request.registry = app.registry + request.host = 'example.com' + + return request diff --git a/docs/tutorials/wiki/src/installation/tests/test_functional.py b/docs/tutorials/wiki/src/installation/tests/test_functional.py new file mode 100644 index 000000000..bac5d63f4 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tests/test_functional.py @@ -0,0 +1,7 @@ +def test_root(testapp): + res = testapp.get('/', status=200) + assert b'Pyramid' in res.body + +def test_notfound(testapp): + res = testapp.get('/badurl', status=404) + assert res.status_code == 404 diff --git a/docs/tutorials/wiki/src/installation/tests/test_it.py b/docs/tutorials/wiki/src/installation/tests/test_it.py deleted file mode 100644 index 6c72bcc62..000000000 --- a/docs/tutorials/wiki/src/installation/tests/test_it.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from pyramid import testing - - -class ViewTests(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - testing.tearDown() - - def test_my_view(self): - from tutorial.views.default import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info['project'], 'myproj') - - def test_notfound_view(self): - from tutorial.views.notfound import notfound_view - request = testing.DummyRequest() - info = notfound_view(request) - self.assertEqual(info, {}) - diff --git a/docs/tutorials/wiki/src/installation/tests/test_views.py b/docs/tutorials/wiki/src/installation/tests/test_views.py new file mode 100644 index 000000000..2b4201955 --- /dev/null +++ b/docs/tutorials/wiki/src/installation/tests/test_views.py @@ -0,0 +1,13 @@ +from tutorial.views.default import my_view +from tutorial.views.notfound import notfound_view + + +def test_my_view(app_request): + info = my_view(app_request) + assert app_request.response.status_int == 200 + assert info['project'] == 'myproj' + +def test_notfound_view(app_request): + info = notfound_view(app_request) + assert app_request.response.status_int == 404 + assert info == {} diff --git a/docs/tutorials/wiki/src/installation/tutorial/__init__.py b/docs/tutorials/wiki/src/installation/tutorial/__init__.py index 830a607f3..e40451339 100644 --- a/docs/tutorials/wiki/src/installation/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/installation/tutorial/__init__.py @@ -1,5 +1,6 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection + from .models import appmaker @@ -12,11 +13,11 @@ def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ with Configurator(settings=settings) as config: + config.include('pyramid_chameleon') config.include('pyramid_tm') config.include('pyramid_retry') config.include('pyramid_zodbconn') - config.set_root_factory(root_factory) - config.include('pyramid_chameleon') config.include('.routes') + config.set_root_factory(root_factory) config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/models/.gitignore b/docs/tutorials/wiki/src/models/.gitignore index 1853d983c..c612e59f2 100644 --- a/docs/tutorials/wiki/src/models/.gitignore +++ b/docs/tutorials/wiki/src/models/.gitignore @@ -19,3 +19,4 @@ Data.fs* .DS_Store coverage test +*.sqlite diff --git a/docs/tutorials/wiki/src/models/testing.ini b/docs/tutorials/wiki/src/models/testing.ini new file mode 100644 index 000000000..9298354ac --- /dev/null +++ b/docs/tutorials/wiki/src/models/testing.ini @@ -0,0 +1,60 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +zodbconn.uri = file://%(here)s/Data.testing.fs?connection_cache_size=20000 + +retry.attempts = 3 + +[pshell] +setup = tutorial.pshell.setup + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/models/tests/conftest.py b/docs/tutorials/wiki/src/models/tests/conftest.py new file mode 100644 index 000000000..12e75d8e9 --- /dev/null +++ b/docs/tutorials/wiki/src/models/tests/conftest.py @@ -0,0 +1,69 @@ +import os +from pyramid.paster import get_appsettings +from pyramid.scripting import prepare +from pyramid.testing import DummyRequest +import pytest +import webtest + +from tutorial import main + + +def pytest_addoption(parser): + parser.addoption('--ini', action='store', metavar='INI_FILE') + +@pytest.fixture(scope='session') +def ini_file(request): + # potentially grab this path from a pytest option + return os.path.abspath(request.config.option.ini or 'testing.ini') + +@pytest.fixture(scope='session') +def app_settings(ini_file): + return get_appsettings(ini_file) + +@pytest.fixture(scope='session') +def app(app_settings): + return main({}, **app_settings) + +@pytest.fixture +def testapp(app): + testapp = webtest.TestApp(app, extra_environ={ + 'HTTP_HOST': 'example.com', + }) + + return testapp + +@pytest.fixture +def app_request(app): + """ + A real request. + + This request is almost identical to a real request but it has some + drawbacks in tests as it's harder to mock data and is heavier. + + """ + env = prepare(registry=app.registry) + request = env['request'] + request.host = 'example.com' + + yield request + env['closer']() + +@pytest.fixture +def dummy_request(app): + """ + A lightweight dummy request. + + This request is ultra-lightweight and should be used only when the + request itself is not a large focus in the call-stack. + + It is way easier to mock and control side-effects using this object. + + - It does not have request extensions applied. + - Threadlocals are not properly pushed. + + """ + request = DummyRequest() + request.registry = app.registry + request.host = 'example.com' + + return request diff --git a/docs/tutorials/wiki/src/models/tests/test_functional.py b/docs/tutorials/wiki/src/models/tests/test_functional.py new file mode 100644 index 000000000..bac5d63f4 --- /dev/null +++ b/docs/tutorials/wiki/src/models/tests/test_functional.py @@ -0,0 +1,7 @@ +def test_root(testapp): + res = testapp.get('/', status=200) + assert b'Pyramid' in res.body + +def test_notfound(testapp): + res = testapp.get('/badurl', status=404) + assert res.status_code == 404 diff --git a/docs/tutorials/wiki/src/models/tests/test_it.py b/docs/tutorials/wiki/src/models/tests/test_it.py deleted file mode 100644 index 6c72bcc62..000000000 --- a/docs/tutorials/wiki/src/models/tests/test_it.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from pyramid import testing - - -class ViewTests(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - testing.tearDown() - - def test_my_view(self): - from tutorial.views.default import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info['project'], 'myproj') - - def test_notfound_view(self): - from tutorial.views.notfound import notfound_view - request = testing.DummyRequest() - info = notfound_view(request) - self.assertEqual(info, {}) - diff --git a/docs/tutorials/wiki/src/models/tests/test_views.py b/docs/tutorials/wiki/src/models/tests/test_views.py new file mode 100644 index 000000000..2b4201955 --- /dev/null +++ b/docs/tutorials/wiki/src/models/tests/test_views.py @@ -0,0 +1,13 @@ +from tutorial.views.default import my_view +from tutorial.views.notfound import notfound_view + + +def test_my_view(app_request): + info = my_view(app_request) + assert app_request.response.status_int == 200 + assert info['project'] == 'myproj' + +def test_notfound_view(app_request): + info = notfound_view(app_request) + assert app_request.response.status_int == 404 + assert info == {} diff --git a/docs/tutorials/wiki/src/models/tutorial/__init__.py b/docs/tutorials/wiki/src/models/tutorial/__init__.py index 830a607f3..e40451339 100644 --- a/docs/tutorials/wiki/src/models/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/models/tutorial/__init__.py @@ -1,5 +1,6 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection + from .models import appmaker @@ -12,11 +13,11 @@ def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ with Configurator(settings=settings) as config: + config.include('pyramid_chameleon') config.include('pyramid_tm') config.include('pyramid_retry') config.include('pyramid_zodbconn') - config.set_root_factory(root_factory) - config.include('pyramid_chameleon') config.include('.routes') + config.set_root_factory(root_factory) config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/views/.gitignore b/docs/tutorials/wiki/src/views/.gitignore index 1853d983c..c612e59f2 100644 --- a/docs/tutorials/wiki/src/views/.gitignore +++ b/docs/tutorials/wiki/src/views/.gitignore @@ -19,3 +19,4 @@ Data.fs* .DS_Store coverage test +*.sqlite diff --git a/docs/tutorials/wiki/src/views/setup.py b/docs/tutorials/wiki/src/views/setup.py index 439bb7759..86c778bf2 100644 --- a/docs/tutorials/wiki/src/views/setup.py +++ b/docs/tutorials/wiki/src/views/setup.py @@ -9,6 +9,7 @@ with open(os.path.join(here, 'CHANGES.txt')) as f: CHANGES = f.read() requires = [ + 'docutils', 'plaster_pastedeploy', 'pyramid', 'pyramid_chameleon', @@ -19,7 +20,6 @@ requires = [ 'pyramid_zodbconn', 'transaction', 'ZODB3', - 'docutils', ] tests_require = [ diff --git a/docs/tutorials/wiki/src/views/testing.ini b/docs/tutorials/wiki/src/views/testing.ini new file mode 100644 index 000000000..9298354ac --- /dev/null +++ b/docs/tutorials/wiki/src/views/testing.ini @@ -0,0 +1,60 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:tutorial + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +zodbconn.uri = file://%(here)s/Data.testing.fs?connection_cache_size=20000 + +retry.attempts = 3 + +[pshell] +setup = tutorial.pshell.setup + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, tutorial + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_tutorial] +level = DEBUG +handlers = +qualname = tutorial + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/docs/tutorials/wiki/src/views/tests/conftest.py b/docs/tutorials/wiki/src/views/tests/conftest.py new file mode 100644 index 000000000..12e75d8e9 --- /dev/null +++ b/docs/tutorials/wiki/src/views/tests/conftest.py @@ -0,0 +1,69 @@ +import os +from pyramid.paster import get_appsettings +from pyramid.scripting import prepare +from pyramid.testing import DummyRequest +import pytest +import webtest + +from tutorial import main + + +def pytest_addoption(parser): + parser.addoption('--ini', action='store', metavar='INI_FILE') + +@pytest.fixture(scope='session') +def ini_file(request): + # potentially grab this path from a pytest option + return os.path.abspath(request.config.option.ini or 'testing.ini') + +@pytest.fixture(scope='session') +def app_settings(ini_file): + return get_appsettings(ini_file) + +@pytest.fixture(scope='session') +def app(app_settings): + return main({}, **app_settings) + +@pytest.fixture +def testapp(app): + testapp = webtest.TestApp(app, extra_environ={ + 'HTTP_HOST': 'example.com', + }) + + return testapp + +@pytest.fixture +def app_request(app): + """ + A real request. + + This request is almost identical to a real request but it has some + drawbacks in tests as it's harder to mock data and is heavier. + + """ + env = prepare(registry=app.registry) + request = env['request'] + request.host = 'example.com' + + yield request + env['closer']() + +@pytest.fixture +def dummy_request(app): + """ + A lightweight dummy request. + + This request is ultra-lightweight and should be used only when the + request itself is not a large focus in the call-stack. + + It is way easier to mock and control side-effects using this object. + + - It does not have request extensions applied. + - Threadlocals are not properly pushed. + + """ + request = DummyRequest() + request.registry = app.registry + request.host = 'example.com' + + return request diff --git a/docs/tutorials/wiki/src/views/tests/test_functional.py b/docs/tutorials/wiki/src/views/tests/test_functional.py new file mode 100644 index 000000000..bac5d63f4 --- /dev/null +++ b/docs/tutorials/wiki/src/views/tests/test_functional.py @@ -0,0 +1,7 @@ +def test_root(testapp): + res = testapp.get('/', status=200) + assert b'Pyramid' in res.body + +def test_notfound(testapp): + res = testapp.get('/badurl', status=404) + assert res.status_code == 404 diff --git a/docs/tutorials/wiki/src/views/tests/test_it.py b/docs/tutorials/wiki/src/views/tests/test_it.py deleted file mode 100644 index 6c72bcc62..000000000 --- a/docs/tutorials/wiki/src/views/tests/test_it.py +++ /dev/null @@ -1,24 +0,0 @@ -import unittest - -from pyramid import testing - - -class ViewTests(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - testing.tearDown() - - def test_my_view(self): - from tutorial.views.default import my_view - request = testing.DummyRequest() - info = my_view(request) - self.assertEqual(info['project'], 'myproj') - - def test_notfound_view(self): - from tutorial.views.notfound import notfound_view - request = testing.DummyRequest() - info = notfound_view(request) - self.assertEqual(info, {}) - diff --git a/docs/tutorials/wiki/src/views/tests/test_views.py b/docs/tutorials/wiki/src/views/tests/test_views.py new file mode 100644 index 000000000..2b4201955 --- /dev/null +++ b/docs/tutorials/wiki/src/views/tests/test_views.py @@ -0,0 +1,13 @@ +from tutorial.views.default import my_view +from tutorial.views.notfound import notfound_view + + +def test_my_view(app_request): + info = my_view(app_request) + assert app_request.response.status_int == 200 + assert info['project'] == 'myproj' + +def test_notfound_view(app_request): + info = notfound_view(app_request) + assert app_request.response.status_int == 404 + assert info == {} diff --git a/docs/tutorials/wiki/src/views/tutorial/__init__.py b/docs/tutorials/wiki/src/views/tutorial/__init__.py index 830a607f3..e40451339 100644 --- a/docs/tutorials/wiki/src/views/tutorial/__init__.py +++ b/docs/tutorials/wiki/src/views/tutorial/__init__.py @@ -1,5 +1,6 @@ from pyramid.config import Configurator from pyramid_zodbconn import get_connection + from .models import appmaker @@ -12,11 +13,11 @@ def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ with Configurator(settings=settings) as config: + config.include('pyramid_chameleon') config.include('pyramid_tm') config.include('pyramid_retry') config.include('pyramid_zodbconn') - config.set_root_factory(root_factory) - config.include('pyramid_chameleon') config.include('.routes') + config.set_root_factory(root_factory) config.scan() return config.make_wsgi_app() diff --git a/docs/tutorials/wiki/src/views/tutorial/templates/layout.pt b/docs/tutorials/wiki/src/views/tutorial/templates/layout.pt index 06a3c8157..1e8b808d4 100644 --- a/docs/tutorials/wiki/src/views/tutorial/templates/layout.pt +++ b/docs/tutorials/wiki/src/views/tutorial/templates/layout.pt @@ -8,8 +8,7 @@ <meta name="author" content="Pylons Project"> <link rel="shortcut icon" href="${request.static_url('tutorial:static/pyramid-16x16.png')}"> - <title><span tal:replace="page.__name__ | title"></span> - Pyramid tutorial wiki (based on - TurboGears 20-Minute Wiki)</title> + <title><span tal:replace="page.__name__ | title"></span> - Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title> <!-- Bootstrap core CSS --> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> @@ -42,6 +41,15 @@ </div> </div> <div class="row"> + <div class="links"> + <ul> + <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li> + <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li> + <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li> + </ul> + </div> + </div> + <div class="row"> <div class="copyright"> Copyright © Pylons Project </div> diff --git a/docs/tutorials/wiki/src/views/tutorial/views/default.py b/docs/tutorials/wiki/src/views/tutorial/views/default.py index e7921cf2f..7ea54bf51 100644 --- a/docs/tutorials/wiki/src/views/tutorial/views/default.py +++ b/docs/tutorials/wiki/src/views/tutorial/views/default.py @@ -1,18 +1,17 @@ from docutils.core import publish_parts -import re - -from pyramid.httpexceptions import HTTPFound +from pyramid.httpexceptions import HTTPSeeOther from pyramid.view import view_config +import re from ..models import Page + # regular expression used to find WikiWords wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)") - @view_config(context='..models.Wiki') def view_wiki(context, request): - return HTTPFound(location=request.resource_url(context, 'FrontPage')) + return HTTPSeeOther(location=request.resource_url(context, 'FrontPage')) @view_config(context='..models.Page', renderer='tutorial:templates/view.pt') @@ -45,7 +44,7 @@ def add_page(context, request): page.__name__ = pagename page.__parent__ = context context[pagename] = page - return HTTPFound(location=request.resource_url(page)) + return HTTPSeeOther(location=request.resource_url(page)) save_url = request.resource_url(context, 'add_page', pagename) page = Page('') page.__name__ = pagename @@ -58,7 +57,7 @@ def add_page(context, request): def edit_page(context, request): if 'form.submitted' in request.params: context.data = request.params['body'] - return HTTPFound(location=request.resource_url(context)) + return HTTPSeeOther(location=request.resource_url(context)) return dict(page=context, save_url=request.resource_url(context, 'edit_page')) diff --git a/docs/tutorials/wiki2/installation.rst b/docs/tutorials/wiki2/installation.rst index b144fc4e0..9defef31a 100644 --- a/docs/tutorials/wiki2/installation.rst +++ b/docs/tutorials/wiki2/installation.rst @@ -315,8 +315,8 @@ For a successful test run, you should see output that ends like this: .. code-block:: bash - .. - 2 passed in 0.44 seconds + ..... + 5 passed in 0.44 seconds Expose test coverage information |
